diff --git a/.betterer.results b/.betterer.results index 01a8e96aa9f..8a98e82ff57 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1687,15 +1687,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "4"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "5"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "6"] - ], "public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -2547,10 +2538,10 @@ exports[`better eslint`] = { "public/app/features/plugins/loader/sharedDependencies.ts:5381": [ [0, 0, 0, "* import is invalid because \'Layout,HorizontalGroup,VerticalGroup\' from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], - "public/app/features/plugins/sandbox/distortion_map.ts:5381": [ + "public/app/features/plugins/sandbox/distortions.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/plugins/sandbox/sandbox_plugin_loader.ts:5381": [ + "public/app/features/plugins/sandbox/sandboxPluginLoader.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"] @@ -3130,9 +3121,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloud-monitoring/types/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/cloud-monitoring/webpack.config.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] - ], "public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -3299,9 +3287,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] - ], "public/app/plugins/datasource/grafana/components/AnnotationQueryEditor.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -3477,8 +3462,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "0"] ], "public/app/plugins/datasource/jaeger/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/datasource/loki/LanguageProvider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -3653,9 +3637,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], - "public/app/plugins/datasource/tempo/webpack.config.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] - ], "public/app/plugins/datasource/zipkin/QueryField.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -3666,9 +3647,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/zipkin/utils/transforms.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/zipkin/webpack.config.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] - ], "public/app/plugins/panel/annolist/AnnoListPanel.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -4012,9 +3990,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/types/acl.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`OrgRole\`)", "0"] - ], "public/app/types/alerting.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -4036,29 +4011,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"] ], - "public/app/types/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`CoreEvents\`)", "0"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "3"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "4"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "5"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "6"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "7"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "8"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "9"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "10"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "11"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "12"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "13"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "14"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "15"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "16"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "17"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "18"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "19"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "20"] - ], "public/app/types/jquery/jquery.d.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/workflows/frontend-lint.yml b/.github/workflows/frontend-lint.yml index e5ceb117eea..fb0362bb580 100644 --- a/.github/workflows/frontend-lint.yml +++ b/.github/workflows/frontend-lint.yml @@ -16,6 +16,7 @@ jobs: contents: read outputs: changed: ${{ steps.detect-changes.outputs.frontend }} + prettier: ${{ steps.detect-changes.outputs.frontend == 'true' || steps.detect-changes.outputs.docs == 'true' }} steps: - uses: actions/checkout@v4 with: @@ -34,7 +35,7 @@ jobs: id-token: write # Run this workflow only for PRs from forks; if it gets merged into `main` or `release-*`, # the `lint-frontend-prettier-enterprise` workflow will run instead - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.changed == 'true' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.prettier == 'true' name: Lint runs-on: ubuntu-latest steps: @@ -55,7 +56,7 @@ jobs: contents: read id-token: write # Run this workflow for non-PR events (like pushes to `main` or `release-*`) OR for internal PRs (PRs not from forks) - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.changed == 'true' + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.prettier == 'true' name: Lint runs-on: ubuntu-latest steps: diff --git a/.github/workflows/i18n-crowdin-download.yml b/.github/workflows/i18n-crowdin-download.yml index 1c1f87efc40..e8e04c22029 100644 --- a/.github/workflows/i18n-crowdin-download.yml +++ b/.github/workflows/i18n-crowdin-download.yml @@ -7,6 +7,7 @@ on: jobs: download-sources-from-crowdin: + if: github.repository == 'grafana/grafana' uses: grafana/grafana-github-actions/.github/workflows/crowdin-download.yml@main with: crowdin_project_id: 5 diff --git a/.github/workflows/i18n-crowdin-upload.yml b/.github/workflows/i18n-crowdin-upload.yml index c3a48f97481..b61a7538bf0 100644 --- a/.github/workflows/i18n-crowdin-upload.yml +++ b/.github/workflows/i18n-crowdin-upload.yml @@ -14,6 +14,7 @@ on: jobs: upload-sources-to-crowdin: + if: github.repository == 'grafana/grafana' uses: grafana/grafana-github-actions/.github/workflows/crowdin-upload.yml@main with: crowdin_project_id: 5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4527ac3487d..12a43e07e98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,56 @@ -# Contributing to Grafana +# Contribute to Grafana Thank you for your interest in contributing to Grafana! We welcome all people who want to contribute in a healthy and constructive manner within our community. To help us create a safe and positive community experience for all, we require all participants to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md). -This document is a guide to help you through the process of contributing to Grafana. +This document is a guide to help you through the process of making technical contributions to Grafana. -## Become a contributor +Whether you're a new contributer or a seasoned veteran we hope these resources help you connect with the community: -You can contribute to Grafana in several ways. Here are some examples: +Interact and be heard: -- Contribute to the Grafana codebase. -- Report and triage bugs. -- Develop community plugins and dashboards. -- Write technical documentation and blog posts, for users and contributors. -- Organize meetups and user groups in your local area. -- Help others by answering questions about Grafana. +- Forums: Do you have a problem, question, or curiosity? Visit our [forums](https://gra.fan/fromgithubtoforums) for a reservoir of knowledge- submit your own questions and answers! +- Meetups: Craving in-person connections without the long journeys? [Join your local Grafana & Friends meetup group](https://gra.fan/githubtomeetup)! +- Community Slack: Eager for real-time connections with fellow users? Begin a conversation on [Slack](https://gra.fan/githubtoslack). + Learn: +- YouTube: From getting started to exploring newer projects like Pyroscope and Beyla, the [Grafana YouTube channel](https://gra.fan/githubtoyoutube) has what you need to get started! +- Meetups: Join a [group near you](https://gra.fan/githubtomeetup) to learn from local experts and ask questions in real time. + Share your story: +- Meetups and blogs: We’d love to feature your OSS Grafana Labs use case or story at an upcoming Grafana & Friends meetup or on the Grafana blog! Submit your idea [here](https://gra.fan/githubtocca) and we’ll connect with you on next steps if accepted. + +## Make technical contributions + +We welcome your technical contributions! Here are some examples: + +- Contribute to the Grafana codebase- check out these [help-wanted issues](<(https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)>) +- Develop community [plugins](https://grafana.com/developers/plugin-tools) +- Report [bugs](https://github.com/grafana/grafana/issues/new?template=0-bug-report.yaml) +- [Triage issues](https://github.com/grafana/grafana/blob/4414b92e93440cc9ed0f281989ee71dc16216a15/contribute/triage-issues.md) +- Report [security vulnerabilities](https://github.com/grafana/grafana/security/policy) +- Submit a [feature request](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md) +- Write [technical documentation](https://github.com/grafana/grafana/blob/4414b92e93440cc9ed0f281989ee71dc16216a15/contribute/documentation/README.md) **Please note:** We do not currently accept contributions for translations. Please do not submit pull requests translating grafana.json files - they will be rejected. We do accept contributions to mark up phrases for translation. See [Internationalization](contribute/internationalization.md). -For more ways to contribute, check out the [Open Source Guides](https://opensource.guide/how-to-contribute/). +### Your first contribution + +Unsure where to begin contributing to Grafana? Start by browsing issues labeled `beginner friendly` or `help wanted`. + +- [Beginner-friendly](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) issues are generally straightforward to complete. +- [Help wanted](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) issues are problems we would like the community to help us with regardless of complexity. + +If you're looking to make a code change, see how to set up your environment for [local development](contribute/developer-guide.md). + +When you're ready to contribute, it's time to [create a pull request](/contribute/create-pull-request.md). + +### Develop a plugin + +Developing a Grafana plugin is a fantastic way to share your unique ideas with the community, extend the platform’s capabilities, and make a real impact on how people visualize and understand their data. Check out our guide to creating [plugins](https://grafana.com/developers/plugin-tools) ### Report bugs Before submitting a new issue, try to make sure someone hasn't already reported the problem. Look through the [existing issues](https://github.com/grafana/grafana/issues) for similar issues. -Report a bug by submitting a [bug report](https://github.com/grafana/grafana/issues/new?labels=type%3A+bug&template=1-bug_report.md). Make sure that you provide as much information as possible on how to reproduce the bug. +Report a bug by submitting a [bug report](https://github.com/grafana/grafana/issues/new?template=0-bug-report.yaml). Make sure that you provide as much information as possible on how to reproduce the bug. Follow the issue template and add additional information that will help us replicate the problem. @@ -38,42 +65,25 @@ For a dashboard related issues: For authentication and alerting Grafana server logs are useful. -#### Security issues - -If you believe you've found a security vulnerability, please read our [security policy](https://github.com/grafana/grafana/security/policy) for more details. - -### Suggest enhancements - -If you have an idea of how to improve Grafana, submit a [feature request](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request&projects=&template=1-feature_requests.md). - -We want to make Grafana accessible to even more people. Submit an [accessibility issue](https://github.com/grafana/grafana/issues/new?labels=type%3A+accessibility&template=3-accessibility.md) to help us understand what we can improve. - -### Write documentation - -To edit or write technical content, refer to [Contribute to our documentation](/contribute/documentation/README.md). We welcome your expertise and input as our body of technical content grows. - ### Triage issues If you don't have the knowledge or time to code, consider helping with _issue triage_. The community will thank you for saving them time by spending some of yours. Read more about the ways you can [Triage issues](/contribute/triage-issues.md). -### Answering questions +#### Security issues -If you have a question and you can't find the answer in the [documentation](https://grafana.com/docs/), the next step is to ask it on the [community site](https://community.grafana.com/). +If you believe you've found a security vulnerability, please read our [security policy](https://github.com/grafana/grafana/security/policy) for more details on reporting. -It's important to us to help these users, and we'd love your help. Sign up to our [community site](https://community.grafana.com/), and start helping other Grafana users by answering their questions. +### Suggest enhancements -### Your first contribution +If you have an idea of how to improve Grafana, submit a [feature request](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md). -Unsure where to begin contributing to Grafana? Start by browsing issues labeled `beginner friendly` or `help wanted`. +We want to make Grafana accessible to even more people. Submit an [accessibility issue](https://github.com/grafana/grafana/issues/new?template=2-accessibility.md) to help us understand what we can improve. -- [Beginner-friendly](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) issues are generally straightforward to complete. -- [Help wanted](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) issues are problems we would like the community to help us with regardless of complexity. +### Write documentation -If you're looking to make a code change, see how to set up your environment for [local development](contribute/developer-guide.md). - -When you're ready to contribute, it's time to [create a pull request](/contribute/create-pull-request.md). +To edit or write technical content, refer to [Contribute to our documentation](/contribute/documentation/README.md). We welcome your expertise and input as our body of technical content grows. #### Contributor License Agreement (CLA) diff --git a/apps/advisor/Makefile b/apps/advisor/Makefile index 7a5f1c99365..b8b335bfa68 100644 --- a/apps/advisor/Makefile +++ b/apps/advisor/Makefile @@ -1,3 +1,22 @@ +APP_SDK_VERSION := v0.39.2 +APP_SDK_DIR := $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) +APP_SDK_BIN := $(APP_SDK_DIR)/grafana-app-sdk + +.PHONY: install-app-sdk +install-app-sdk: $(APP_SDK_BIN) ## Install the Grafana App SDK + +$(APP_SDK_BIN): + @echo "Installing Grafana App SDK version $(APP_SDK_VERSION)" + @mkdir -p $(APP_SDK_DIR) + # The only way to install specific versions of binaries using `go install` + # is by setting GOBIN to the directory you want to install the binary to. + GOBIN=$(APP_SDK_DIR) go install github.com/grafana/grafana-app-sdk/cmd/grafana-app-sdk@$(APP_SDK_VERSION) + @touch $@ + +.PHONY: update-app-sdk +update-app-sdk: ## Update the Grafana App SDK dependency in go.mod + go get github.com/grafana/grafana-app-sdk@$(APP_SDK_VERSION) + .PHONY: generate -generate: - @grafana-app-sdk generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none +generate: ## Run Grafana App SDK code generation + @$(APP_SDK_BIN) generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 45a7ed0b51e..65832aabb4b 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/advisor go 1.24.4 require ( - github.com/grafana/grafana-app-sdk v0.39.0 - k8s.io/apimachinery v0.33.1 + github.com/grafana/grafana-app-sdk v0.39.2 + k8s.io/apimachinery v0.33.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) @@ -33,7 +33,7 @@ require ( github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb // indirect - github.com/grafana/grafana-app-sdk/logging v0.38.2 // indirect + github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -79,9 +79,9 @@ require ( google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 // indirect - k8s.io/apiextensions-apiserver v0.33.1 // indirect - k8s.io/client-go v0.33.1 // indirect + k8s.io/api v0.33.2 // indirect + k8s.io/apiextensions-apiserver v0.33.2 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index e7629867703..90b34992392 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -63,11 +63,14 @@ github.com/grafana/grafana-app-sdk v0.30.0/go.mod h1:jhfqNIovb+Mes2vdMf9iMCWQkp1 github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI= github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= github.com/grafana/grafana-app-sdk/logging v0.29.0 h1:mgbXaAf33aFwqwGVeaX30l8rkeAJH0iACgX5Rn6YkN4= github.com/grafana/grafana-app-sdk/logging v0.29.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= github.com/grafana/grafana-app-sdk/logging v0.30.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= github.com/grafana/grafana-app-sdk/logging v0.35.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= @@ -302,21 +305,25 @@ k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_metadata_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_metadata_gen.go index 4b385514d97..0150b059719 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_metadata_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_metadata_gen.go @@ -24,5 +24,8 @@ type CheckMetadata struct { // NewCheckMetadata creates a new CheckMetadata object. func NewCheckMetadata() *CheckMetadata { - return &CheckMetadata{} + return &CheckMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_object_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_object_gen.go index aca8c2876d6..5708bba88d6 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_object_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_object_gen.go @@ -18,8 +18,11 @@ import ( type Check struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ObjectMeta `json:"metadata" yaml:"metadata"` - Spec CheckSpec `json:"spec" yaml:"spec"` - CheckStatus CheckStatus `json:"status" yaml:"status"` + + // Spec is the spec of the Check + Spec CheckSpec `json:"spec" yaml:"spec"` + + Status CheckStatus `json:"status" yaml:"status"` } func (o *Check) GetSpec() any { @@ -37,14 +40,14 @@ func (o *Check) SetSpec(spec any) error { func (o *Check) GetSubresources() map[string]any { return map[string]any{ - "status": o.CheckStatus, + "status": o.Status, } } func (o *Check) GetSubresource(name string) (any, bool) { switch name { case "status": - return o.CheckStatus, true + return o.Status, true default: return nil, false } @@ -57,7 +60,7 @@ func (o *Check) SetSubresource(name string, value any) error { if !ok { return fmt.Errorf("cannot set status type %#v, not of type CheckStatus", value) } - o.CheckStatus = cast + o.Status = cast return nil default: return fmt.Errorf("subresource '%s' does not exist", name) @@ -219,6 +222,20 @@ func (o *Check) DeepCopyObject() runtime.Object { return o.Copy() } +func (o *Check) DeepCopy() *Check { + cpy := &Check{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Check) DeepCopyInto(dst *Check) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + // Interface compliance compile-time check var _ resource.Object = &Check{} @@ -262,5 +279,41 @@ func (o *CheckList) SetItems(items []resource.Object) { } } +func (o *CheckList) DeepCopy() *CheckList { + cpy := &CheckList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *CheckList) DeepCopyInto(dst *CheckList) { + resource.CopyObjectInto(dst, o) +} + // Interface compliance compile-time check var _ resource.ListObject = &CheckList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *CheckSpec) DeepCopy() *CheckSpec { + cpy := &CheckSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *CheckSpec) DeepCopyInto(dst *CheckSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of CheckStatus +func (s *CheckStatus) DeepCopy() *CheckStatus { + cpy := &CheckStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies CheckStatus into another CheckStatus object +func (s *CheckStatus) DeepCopyInto(dst *CheckStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go index b77811fefc2..8716958ce02 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go @@ -3,16 +3,18 @@ package v0alpha1 // +k8s:openapi-gen=true -type CheckErrorLink struct { - // URL to a page with more information about the error - Url string `json:"url"` - // Human readable error message - Message string `json:"message"` +type CheckReport struct { + // Number of elements analyzed + Count int64 `json:"count"` + // List of failures + Failures []CheckReportFailure `json:"failures"` } -// NewCheckErrorLink creates a new CheckErrorLink object. -func NewCheckErrorLink() *CheckErrorLink { - return &CheckErrorLink{} +// NewCheckReport creates a new CheckReport object. +func NewCheckReport() *CheckReport { + return &CheckReport{ + Failures: []CheckReportFailure{}, + } } // +k8s:openapi-gen=true @@ -33,7 +35,22 @@ type CheckReportFailure struct { // NewCheckReportFailure creates a new CheckReportFailure object. func NewCheckReportFailure() *CheckReportFailure { - return &CheckReportFailure{} + return &CheckReportFailure{ + Links: []CheckErrorLink{}, + } +} + +// +k8s:openapi-gen=true +type CheckErrorLink struct { + // URL to a page with more information about the error + Url string `json:"url"` + // Human readable error message + Message string `json:"message"` +} + +// NewCheckErrorLink creates a new CheckErrorLink object. +func NewCheckErrorLink() *CheckErrorLink { + return &CheckErrorLink{} } // +k8s:openapi-gen=true @@ -56,7 +73,7 @@ func NewCheckstatusOperatorState() *CheckstatusOperatorState { // +k8s:openapi-gen=true type CheckStatus struct { - Report CheckV0alpha1StatusReport `json:"report"` + Report CheckReport `json:"report"` // operatorStates is a map of operator ID to operator state evaluations. // Any operator which consumes this kind SHOULD add its state evaluation information to this field. OperatorStates map[string]CheckstatusOperatorState `json:"operatorStates,omitempty"` @@ -67,7 +84,7 @@ type CheckStatus struct { // NewCheckStatus creates a new CheckStatus object. func NewCheckStatus() *CheckStatus { return &CheckStatus{ - Report: *NewCheckV0alpha1StatusReport(), + Report: *NewCheckReport(), } } @@ -87,16 +104,3 @@ const ( CheckStatusOperatorStateStateInProgress CheckStatusOperatorStateState = "in_progress" CheckStatusOperatorStateStateFailed CheckStatusOperatorStateState = "failed" ) - -// +k8s:openapi-gen=true -type CheckV0alpha1StatusReport struct { - // Number of elements analyzed - Count int64 `json:"count"` - // List of failures - Failures []CheckReportFailure `json:"failures"` -} - -// NewCheckV0alpha1StatusReport creates a new CheckV0alpha1StatusReport object. -func NewCheckV0alpha1StatusReport() *CheckV0alpha1StatusReport { - return &CheckV0alpha1StatusReport{} -} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go index 9998cffb37d..05e019ec5a7 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go @@ -24,5 +24,8 @@ type CheckTypeMetadata struct { // NewCheckTypeMetadata creates a new CheckTypeMetadata object. func NewCheckTypeMetadata() *CheckTypeMetadata { - return &CheckTypeMetadata{} + return &CheckTypeMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go index 80bd02b1cc8..22d4962c730 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go @@ -18,8 +18,11 @@ import ( type CheckType struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ObjectMeta `json:"metadata" yaml:"metadata"` - Spec CheckTypeSpec `json:"spec" yaml:"spec"` - CheckTypeStatus CheckTypeStatus `json:"status" yaml:"status"` + + // Spec is the spec of the CheckType + Spec CheckTypeSpec `json:"spec" yaml:"spec"` + + Status CheckTypeStatus `json:"status" yaml:"status"` } func (o *CheckType) GetSpec() any { @@ -37,14 +40,14 @@ func (o *CheckType) SetSpec(spec any) error { func (o *CheckType) GetSubresources() map[string]any { return map[string]any{ - "status": o.CheckTypeStatus, + "status": o.Status, } } func (o *CheckType) GetSubresource(name string) (any, bool) { switch name { case "status": - return o.CheckTypeStatus, true + return o.Status, true default: return nil, false } @@ -57,7 +60,7 @@ func (o *CheckType) SetSubresource(name string, value any) error { if !ok { return fmt.Errorf("cannot set status type %#v, not of type CheckTypeStatus", value) } - o.CheckTypeStatus = cast + o.Status = cast return nil default: return fmt.Errorf("subresource '%s' does not exist", name) @@ -219,6 +222,20 @@ func (o *CheckType) DeepCopyObject() runtime.Object { return o.Copy() } +func (o *CheckType) DeepCopy() *CheckType { + cpy := &CheckType{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *CheckType) DeepCopyInto(dst *CheckType) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + // Interface compliance compile-time check var _ resource.Object = &CheckType{} @@ -262,5 +279,41 @@ func (o *CheckTypeList) SetItems(items []resource.Object) { } } +func (o *CheckTypeList) DeepCopy() *CheckTypeList { + cpy := &CheckTypeList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *CheckTypeList) DeepCopyInto(dst *CheckTypeList) { + resource.CopyObjectInto(dst, o) +} + // Interface compliance compile-time check var _ resource.ListObject = &CheckTypeList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *CheckTypeSpec) DeepCopy() *CheckTypeSpec { + cpy := &CheckTypeSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *CheckTypeSpec) DeepCopyInto(dst *CheckTypeSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of CheckTypeStatus +func (s *CheckTypeStatus) DeepCopy() *CheckTypeStatus { + cpy := &CheckTypeStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies CheckTypeStatus into another CheckTypeStatus object +func (s *CheckTypeStatus) DeepCopyInto(dst *CheckTypeStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go index 528643b69dd..d4d3f2d4f86 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go @@ -23,5 +23,7 @@ type CheckTypeSpec struct { // NewCheckTypeSpec creates a new CheckTypeSpec object. func NewCheckTypeSpec() *CheckTypeSpec { - return &CheckTypeSpec{} + return &CheckTypeSpec{ + Steps: []CheckTypeStep{}, + } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/constants.go b/apps/advisor/pkg/apis/advisor/v0alpha1/constants.go index 213249b4c58..80c8a0f0620 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/constants.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/constants.go @@ -3,16 +3,16 @@ package v0alpha1 import "k8s.io/apimachinery/pkg/runtime/schema" const ( - // Group is the API group used by all kinds in this package - Group = "advisor.grafana.app" - // Version is the API version used by all kinds in this package - Version = "v0alpha1" + // APIGroup is the API group used by all kinds in this package + APIGroup = "advisor.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v0alpha1" ) var ( // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package GroupVersion = schema.GroupVersion{ - Group: Group, - Version: Version, + Group: APIGroup, + Version: APIVersion, } ) diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go index 84e85e7f5ba..21e61de7f45 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go @@ -15,6 +15,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.Check": schema_pkg_apis_advisor_v0alpha1_Check(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckErrorLink": schema_pkg_apis_advisor_v0alpha1_CheckErrorLink(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckList": schema_pkg_apis_advisor_v0alpha1_CheckList(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport": schema_pkg_apis_advisor_v0alpha1_CheckReport(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure": schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec": schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckStatus": schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref), @@ -24,7 +25,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStatus": schema_pkg_apis_advisor_v0alpha1_CheckTypeStatus(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStep": schema_pkg_apis_advisor_v0alpha1_CheckTypeStep(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypestatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckTypestatusOperatorState(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport": schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref), } } @@ -57,8 +57,9 @@ func schema_pkg_apis_advisor_v0alpha1_Check(ref common.ReferenceCallback) common }, "spec": { SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec"), + Description: "Spec is the spec of the Check", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec"), }, }, "status": { @@ -153,6 +154,43 @@ func schema_pkg_apis_advisor_v0alpha1_CheckList(ref common.ReferenceCallback) co } } +func schema_pkg_apis_advisor_v0alpha1_CheckReport(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "count": { + SchemaProps: spec.SchemaProps{ + Description: "Number of elements analyzed", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "failures": { + SchemaProps: spec.SchemaProps{ + Description: "List of failures", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"), + }, + }, + }, + }, + }, + }, + Required: []string{"count", "failures"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"}, + } +} + func schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -258,7 +296,7 @@ func schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref common.ReferenceCallback) "report": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport"), + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport"), }, }, "operatorStates": { @@ -296,7 +334,7 @@ func schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport", "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState"}, + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport", "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState"}, } } @@ -328,8 +366,9 @@ func schema_pkg_apis_advisor_v0alpha1_CheckType(ref common.ReferenceCallback) co }, "spec": { SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec"), + Description: "Spec is the spec of the CheckType", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec"), }, }, "status": { @@ -566,43 +605,6 @@ func schema_pkg_apis_advisor_v0alpha1_CheckTypestatusOperatorState(ref common.Re } } -func schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "count": { - SchemaProps: spec.SchemaProps{ - Description: "Number of elements analyzed", - Default: 0, - Type: []string{"integer"}, - Format: "int64", - }, - }, - "failures": { - SchemaProps: spec.SchemaProps{ - Description: "List of failures", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"), - }, - }, - }, - }, - }, - }, - Required: []string{"count", "failures"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"}, - } -} - func schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index 6f5d472dc54..eb596a274e2 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -7,15 +7,19 @@ package apis import ( "encoding/json" + "fmt" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + + v0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" ) var ( - rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) + rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object"}}`) versionSchemaCheckv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaCheckv0alpha1, &versionSchemaCheckv0alpha1) - rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) + rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaCheckTypev0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaCheckTypev0alpha1, &versionSchemaCheckTypev0alpha1) ) @@ -58,12 +62,6 @@ var appManifestData = app.ManifestData{ }, } -func jsonToMap(j string) map[string]any { - m := make(map[string]any) - json.Unmarshal([]byte(j), &j) - return m -} - func LocalManifest() app.Manifest { return app.NewEmbeddedManifest(appManifestData) } @@ -71,3 +69,15 @@ func LocalManifest() app.Manifest { func RemoteManifest() app.Manifest { return app.NewAPIServerManifest("advisor") } + +var kindVersionToGoType = map[string]resource.Kind{ + "Check/v0alpha1": v0alpha1.CheckKind(), + "CheckType/v0alpha1": v0alpha1.CheckTypeKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} diff --git a/apps/advisor/pkg/app/checks/utils.go b/apps/advisor/pkg/app/checks/utils.go index 0abcb8a603b..e58375ee64b 100644 --- a/apps/advisor/pkg/app/checks/utils.go +++ b/apps/advisor/pkg/app/checks/utils.go @@ -106,3 +106,25 @@ func SetStatusAnnotation(ctx context.Context, client resource.Client, obj resour }}, }, resource.PatchOptions{}, obj) } + +func SetAnnotations(ctx context.Context, client resource.Client, obj resource.Object, annotations map[string]string) error { + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/metadata/annotations", + Value: annotations, + }}, + }, resource.PatchOptions{}, obj) +} + +func SetStatus(ctx context.Context, client resource.Client, obj resource.Object, status any) error { + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/status", + Value: status, + }}, + }, resource.PatchOptions{ + Subresource: "status", + }, obj) +} diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go index 83dba0cf42a..cf090b67260 100644 --- a/apps/advisor/pkg/app/utils.go +++ b/apps/advisor/pkg/app/utils.go @@ -78,28 +78,21 @@ func processCheck(ctx context.Context, log logging.Logger, client resource.Clien return fmt.Errorf("error running steps: %w", err) } - report := &advisorv0alpha1.CheckV0alpha1StatusReport{ + report := &advisorv0alpha1.CheckReport{ Failures: failures, Count: int64(len(items)), } + c.Status.Report = *report + err = checks.SetStatus(ctx, client, obj, c.Status) + if err != nil { + return err + } // Set the status annotation to processed and annotate the steps ignored annotations := checks.AddAnnotations(ctx, obj, map[string]string{ checks.StatusAnnotation: checks.StatusAnnotationProcessed, checks.IgnoreStepsAnnotationList: checkType.GetAnnotations()[checks.IgnoreStepsAnnotationList], }) - return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ - Operations: []resource.PatchOperation{ - { - Operation: resource.PatchOpAdd, - Path: "/status/report", - Value: *report, - }, { - Operation: resource.PatchOpAdd, - Path: "/metadata/annotations", - Value: annotations, - }, - }, - }, resource.PatchOptions{}, obj) + return checks.SetAnnotations(ctx, client, obj, annotations) } func processCheckRetry(ctx context.Context, log logging.Logger, client resource.Client, typesClient resource.Client, obj resource.Object, check checks.Check) error { @@ -157,7 +150,7 @@ func processCheckRetry(ctx context.Context, log logging.Logger, client resource. } } // Pull failures from the report for the items to retry - c.CheckStatus.Report.Failures = slices.DeleteFunc(c.CheckStatus.Report.Failures, func(f advisorv0alpha1.CheckReportFailure) bool { + c.Status.Report.Failures = slices.DeleteFunc(c.Status.Report.Failures, func(f advisorv0alpha1.CheckReportFailure) bool { if f.ItemID == itemToRetry { for _, newFailure := range failures { if newFailure.StepID == f.StepID { @@ -171,19 +164,13 @@ func processCheckRetry(ctx context.Context, log logging.Logger, client resource. // Failure not in the list of items to retry, keep it return false }) + err = checks.SetStatus(ctx, client, obj, c.Status) + if err != nil { + return err + } // Delete the retry annotation to mark the check as processed annotations := checks.DeleteAnnotations(ctx, obj, []string{checks.RetryAnnotation}) - return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ - Operations: []resource.PatchOperation{{ - Operation: resource.PatchOpAdd, - Path: "/status/report", - Value: c.CheckStatus.Report, - }, { - Operation: resource.PatchOpAdd, - Path: "/metadata/annotations", - Value: annotations, - }}, - }, resource.PatchOptions{}, obj) + return checks.SetAnnotations(ctx, client, obj, annotations) } func runStepsInParallel(ctx context.Context, log logging.Logger, spec *advisorv0alpha1.CheckSpec, steps []checks.Step, items []any) ([]advisorv0alpha1.CheckReportFailure, error) { diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go index dc8c635f6e8..df4b016db93 100644 --- a/apps/advisor/pkg/app/utils_test.go +++ b/apps/advisor/pkg/app/utils_test.go @@ -95,9 +95,9 @@ func TestProcessMultipleCheckItems(t *testing.T) { err = processCheck(ctx, logging.DefaultLogger, client, typesClient, obj, check) assert.NoError(t, err) assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation]) - r := client.lastValue.(advisorv0alpha1.CheckV0alpha1StatusReport) - assert.Equal(t, r.Count, int64(100)) - assert.Len(t, r.Failures, 50) + r := client.values[0].(advisorv0alpha1.CheckStatus) + assert.Equal(t, r.Report.Count, int64(100)) + assert.Len(t, r.Report.Failures, 50) } func TestProcessCheck_AlreadyProcessed(t *testing.T) { @@ -231,7 +231,7 @@ func TestProcessCheckRetry_SkipMissingItem(t *testing.T) { checks.RetryAnnotation: "item", checks.StatusAnnotation: checks.StatusAnnotationProcessed, }) - obj.CheckStatus.Report.Failures = []advisorv0alpha1.CheckReportFailure{ + obj.Status.Report.Failures = []advisorv0alpha1.CheckReportFailure{ { ItemID: "item", StepID: "step", @@ -254,7 +254,7 @@ func TestProcessCheckRetry_SkipMissingItem(t *testing.T) { assert.NoError(t, err) assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation]) assert.Empty(t, obj.GetAnnotations()[checks.RetryAnnotation]) - assert.Empty(t, obj.CheckStatus.Report.Failures) + assert.Empty(t, obj.Status.Report.Failures) } func TestProcessCheckRetry_Success(t *testing.T) { @@ -263,7 +263,7 @@ func TestProcessCheckRetry_Success(t *testing.T) { checks.RetryAnnotation: "item", checks.StatusAnnotation: checks.StatusAnnotationProcessed, }) - obj.CheckStatus.Report.Failures = []advisorv0alpha1.CheckReportFailure{ + obj.Status.Report.Failures = []advisorv0alpha1.CheckReportFailure{ { ItemID: "item", StepID: "step", @@ -286,16 +286,16 @@ func TestProcessCheckRetry_Success(t *testing.T) { assert.NoError(t, err) assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation]) assert.Empty(t, obj.GetAnnotations()[checks.RetryAnnotation]) - assert.Empty(t, obj.CheckStatus.Report.Failures) + assert.Empty(t, obj.Status.Report.Failures) } type mockClient struct { resource.Client - lastValue any + values []any } func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions, obj resource.Object) error { - m.lastValue = req.Operations[0].Value + m.values = append(m.values, req.Operations[0].Value) return nil } diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 95da187b1e5..180cc985669 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -394,7 +394,7 @@ AnnotationQuerySpec: { name: string builtIn?: bool | *false filter?: AnnotationPanelFilter - legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling. + legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties } AnnotationQueryKind: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 8cc188b0909..50306a06aa0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -398,7 +398,7 @@ AnnotationQuerySpec: { name: string builtIn?: bool | *false filter?: AnnotationPanelFilter - legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling. + legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties } AnnotationQueryKind: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index f1acc3f273b..79d77c4de74 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -30,7 +30,7 @@ type DashboardAnnotationQuerySpec struct { Name string `json:"name"` BuiltIn *bool `json:"builtIn,omitempty"` Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"` - // Catch-all field for datasource-specific properties. Should not be available in as code tooling. + // Catch-all field for datasource-specific properties LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"` } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 302f539a9ec..4e55caae3c6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -643,7 +643,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref common. }, "legacyOptions": { SchemaProps: spec.SchemaProps{ - Description: "Catch-all field for datasource-specific properties. Should not be available in as code tooling.", + Description: "Catch-all field for datasource-specific properties", Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{ Allows: true, diff --git a/apps/iam/kinds/v0alpha1/rolespec.cue b/apps/iam/kinds/v0alpha1/rolespec.cue index 845ae207c28..b7934f0174e 100644 --- a/apps/iam/kinds/v0alpha1/rolespec.cue +++ b/apps/iam/kinds/v0alpha1/rolespec.cue @@ -10,6 +10,7 @@ RoleSpec: { // Display name of the role title: string + description: string version: int group: string diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go index eb2c1d52e40..ac0b41e1972 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go @@ -18,9 +18,10 @@ func NewCoreRolespecPermission() *CoreRolespecPermission { // +k8s:openapi-gen=true type CoreRoleSpec struct { // Display name of the role - Title string `json:"title"` - Version int64 `json:"version"` - Group string `json:"group"` + Title string `json:"title"` + Description string `json:"description"` + Version int64 `json:"version"` + Group string `json:"group"` // TODO: // delegatable?: bool // created? diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go index 785d2d5724b..1ce8d65646f 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go @@ -18,9 +18,10 @@ func NewGlobalRolespecPermission() *GlobalRolespecPermission { // +k8s:openapi-gen=true type GlobalRoleSpec struct { // Display name of the role - Title string `json:"title"` - Version int64 `json:"version"` - Group string `json:"group"` + Title string `json:"title"` + Description string `json:"description"` + Version int64 `json:"version"` + Group string `json:"group"` // TODO: // delegatable?: bool // created? diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go index f1a165dba4e..5a8185cd25c 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go @@ -18,9 +18,10 @@ func NewRolespecPermission() *RolespecPermission { // +k8s:openapi-gen=true type RoleSpec struct { // Display name of the role - Title string `json:"title"` - Version int64 `json:"version"` - Group string `json:"group"` + Title string `json:"title"` + Description string `json:"description"` + Version int64 `json:"version"` + Group string `json:"group"` // TODO: // delegatable?: bool // created? diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index 406834348e3..7a0703d53e3 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -186,6 +186,13 @@ func schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref common.ReferenceCallback) com Format: "", }, }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "version": { SchemaProps: spec.SchemaProps{ Default: 0, @@ -215,7 +222,7 @@ func schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref common.ReferenceCallback) com }, }, }, - Required: []string{"title", "version", "group", "permissions"}, + Required: []string{"title", "description", "version", "group", "permissions"}, }, }, Dependencies: []string{ @@ -740,6 +747,13 @@ func schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref common.ReferenceCallback) c Format: "", }, }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "version": { SchemaProps: spec.SchemaProps{ Default: 0, @@ -769,7 +783,7 @@ func schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref common.ReferenceCallback) c }, }, }, - Required: []string{"title", "version", "group", "permissions"}, + Required: []string{"title", "description", "version", "group", "permissions"}, }, }, Dependencies: []string{ @@ -1600,6 +1614,13 @@ func schema_pkg_apis_iam_v0alpha1_RoleSpec(ref common.ReferenceCallback) common. Format: "", }, }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "version": { SchemaProps: spec.SchemaProps{ Default: 0, @@ -1629,7 +1650,7 @@ func schema_pkg_apis_iam_v0alpha1_RoleSpec(ref common.ReferenceCallback) common. }, }, }, - Required: []string{"title", "version", "group", "permissions"}, + Required: []string{"title", "description", "version", "group", "permissions"}, }, }, Dependencies: []string{ diff --git a/conf/provisioning/sample/dashboard-v1-annotations.json b/conf/provisioning/sample/dashboard-v1-annotations.json deleted file mode 100644 index 7e90452a2a5..00000000000 --- a/conf/provisioning/sample/dashboard-v1-annotations.json +++ /dev/null @@ -1,257 +0,0 @@ -{ - "kind": "Dashboard", - "apiVersion": "dashboard.grafana.app/v1beta1", - "metadata": { - "name": "test-v1-annotations", - "annotations": { - "hello": "world" - }, - "labels": { - "region": "west" - } - }, - "spec": { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": false, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "target": { - "limit": 100, - "matchAny": false, - "tags": [], - "type": "dashboard" - }, - "type": "dashboard" - }, - { - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, - "enable": true, - "hide": false, - "iconColor": "blue", - "name": "testdata-annos", - "target": { - "lines": 10, - "refId": "Anno", - "scenarioId": "annotations" - } - }, - { - "enable": true, - "hide": false, - "iconColor": "blue", - "name": "no-ds-testdata-annos", - "target": { - "lines": 10, - "refId": "Anno", - "scenarioId": "annotations" - } - }, - { - "datasource": { - "type": "prometheus", - "uid": "gdev-prometheus" - }, - "enable": true, - "hide": false, - "iconColor": "yellow", - "name": "prom-annos", - "target": { - "expr": "{action=\"add_client\"}", - "interval": "", - "lines": 10, - "refId": "Anno", - "scenarioId": "annotations" - } - }, - { - "enable": true, - "hide": false, - "iconColor": "yellow", - "name": "no-ds-prom-annos", - "target": { - "expr": "{action=\"add_client\"}", - "interval": "", - "lines": 10, - "refId": "Anno", - "scenarioId": "annotations" - } - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "PBBCEC2D313BC06C3" - }, - "enable": true, - "hide": false, - "iconColor": "red", - "name": "postgress-annos", - "target": { - "editorMode": "builder", - "format": "table", - "lines": 10, - "rawSql": "", - "refId": "Anno", - "scenarioId": "annotations", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - }, - { - "datasource": { - "type": "elasticsearch", - "uid": "gdev-elasticsearch" - }, - "enable": true, - "hide": false, - "iconColor": "red", - "name": "elastic - annos", - "tagsField": "asd", - "target": { - "lines": 10, - "query": "test query", - "refId": "Anno", - "scenarioId": "annotations" - }, - "textField": "asd", - "timeEndField": "asdas", - "timeField": "asd" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 1, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.1.0-pre", - "targets": [ - { - "refId": "A" - } - ], - "title": "New panel", - "type": "timeseries" - } - ], - "preload": false, - "schemaVersion": 41, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": {}, - "timezone": "browser", - "title": "Test: V1 dashboard with annotations", - "version": 8 - } -} diff --git a/docs/sources/datasources/loki/_index.md b/docs/sources/datasources/loki/_index.md index 523be2c9fbc..4176a2771e7 100644 --- a/docs/sources/datasources/loki/_index.md +++ b/docs/sources/datasources/loki/_index.md @@ -62,7 +62,7 @@ The following guides will help you get started with Loki: This data source supports these versions of Loki: -- v2.8+ +- v2.9+ ## Adding a data source diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index c20e71dab3d..060345d124a 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -72,7 +72,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `pluginsSriChecks` | Enables SRI checks for plugin assets | | | `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | | `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | -| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | Yes | | `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes | | `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | | `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes | @@ -82,7 +81,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `alertingMigrationUI` | Enables the alerting migration UI, to migrate data source-managed rules to Grafana-managed rules | Yes | | `alertingImportYAMLUI` | Enables a UI feature for importing rules from a Prometheus file to Grafana-managed rules | Yes | | `unifiedNavbars` | Enables unified navbars | | -| `tabularNumbers` | Use fixed-width numbers globally in the UI | Yes | +| `tabularNumbers` | Use fixed-width numbers globally in the UI | | ## Public preview feature toggles diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index 5091d40fd94..2405baa4ca0 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -239,10 +239,6 @@ Ensure that you have followed the steps in [Create the Microsoft Entra ID applic ## Configure Azure AD authentication client using the Grafana UI -{{< admonition type="note" >}} -Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle. -{{< /admonition >}} - As a Grafana Admin, you can configure your Azure AD/Entra ID OAuth client from within Grafana using the Grafana UI. To do this, navigate to the **Administration > Authentication > Azure AD** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values. After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations. @@ -255,10 +251,6 @@ If you run Grafana in high availability mode, configuration changes may not get ## Configure Azure AD authentication client using the Terraform provider -{{< admonition type="note" >}} -Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "azuread_sso_settings" { provider_name = "azuread" diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 3beaeca97ff..d1a613d6b66 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -49,10 +49,6 @@ If Users use the same email address in Azure AD that they use with other authent ## Configure generic OAuth authentication client using the Grafana UI -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. -{{< /admonition >}} - As a Grafana Admin, you can configure Generic OAuth client from within Grafana using the Generic OAuth UI. To do this, navigate to **Administration > Authentication > Generic OAuth** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations. @@ -67,10 +63,6 @@ Refer to [configuration options](#configuration-options) for more information. ## Configure generic OAuth authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "generic_sso_settings" { provider_name = "generic_oauth" diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md index 25e83cfd5e6..86edef93132 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md @@ -43,10 +43,6 @@ Ensure you know how to create a GitHub OAuth app. Consult GitHub's documentation ## Configure GitHub authentication client using the Grafana UI -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. -{{< /admonition >}} - As a Grafana Admin, you can configure GitHub OAuth client from within Grafana using the GitHub UI. To do this, navigate to **Administration > Authentication > GitHub** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values. After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. @@ -61,10 +57,6 @@ Refer to [configuration options](#configuration-options) for more information. ## Configure GitHub authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "github_sso_settings" { provider_name = "github" diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index ad42174ae64..8f0aaf0f38f 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -57,10 +57,6 @@ Refer to [configuration options](#configuration-options) for more information. ## Configure GitLab authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "gitlab_sso_settings" { provider_name = "gitlab" diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index 4561f6c63b3..b162eb52305 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -45,10 +45,6 @@ First, you need to create a Google OAuth Client: ## Configure Google authentication client using the Grafana UI -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. -{{< /admonition >}} - As a Grafana Admin, you can configure Google OAuth client from within Grafana using the Google UI. To do this, navigate to **Administration > Authentication > Google** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. @@ -61,10 +57,6 @@ If you run Grafana in high availability mode, configuration changes may not get ## Configure Google authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "google_sso_settings" { provider_name = "google" diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index 1323a6ed423..93a21c70bc1 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -101,10 +101,6 @@ If you want to configure the role for all users in the Okta directory, you can a ## Configure Okta authentication client using the Grafana UI -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. -{{< /admonition >}} - As a Grafana Admin, you can configure Okta OAuth2 client from within Grafana using the Okta UI. To do this, navigate to **Administration > Authentication > Okta** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. @@ -119,10 +115,6 @@ Refer to [configuration options](#configuration-options) for more information. ## Configure Okta authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "okta_sso_settings" { provider_name = "okta" diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/_index.md index 06a38944b83..4a846a622e2 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/_index.md @@ -40,7 +40,7 @@ If you are using Okta or Azure AD as Identity Provider, see the following docume - [Configure SAML with Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/saml/configure-saml-with-okta/) {{< admonition type="note" >}} -The API and Terraform support are available in Public Preview in Grafana v11.1 behind the `ssoSettingsSAML` feature toggle. You must also enable the `ssoSettingsApi` flag. +The API and Terraform support are available in Public Preview in Grafana v11.1 behind the `ssoSettingsSAML` feature toggle. {{< /admonition >}} All methods offer the same configuration options. However, if you want to keep all of Grafana authentication settings in one place, use the Grafana configuration file or the Terraform provider. If you are a Grafana Cloud user, you do not have access to Grafana configuration file. Instead, configure SAML through the other methods. diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e/test-plugins/grafana-extensionstest-app/package.json index 5fe3da7246e..d11250bdc4d 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e/test-plugins/grafana-extensionstest-app/package.json @@ -3,8 +3,8 @@ "version": "12.1.0-pre", "private": true, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "dev": "webpack -w -c ./webpack.config.ts --env development", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "typecheck": "tsc --noEmit", "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx ." }, diff --git a/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts b/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts index 3303ed94f3c..564555396a5 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts +++ b/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts @@ -1,7 +1,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; -import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; import { mergeWithCustomize, unique } from 'webpack-merge'; -import { Configuration } from 'webpack'; +import { type Configuration } from 'webpack'; function skipFiles(f: string): boolean { if (f.includes('/dist/')) { @@ -19,7 +19,7 @@ function skipFiles(f: string): boolean { return true; } -const config = async (env: Record): Promise => { +const config = async (env: Env): Promise => { const baseConfig = await grafanaConfig(env); const customConfig = { plugins: [ diff --git a/e2e/test-plugins/grafana-test-datasource/package.json b/e2e/test-plugins/grafana-test-datasource/package.json index 437d542510a..9f976341268 100644 --- a/e2e/test-plugins/grafana-test-datasource/package.json +++ b/e2e/test-plugins/grafana-test-datasource/package.json @@ -3,8 +3,8 @@ "version": "12.1.0-pre", "private": true, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "dev": "webpack -w -c ./webpack.config.ts --env development", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "typecheck": "tsc --noEmit", "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx ." }, diff --git a/e2e/test-plugins/grafana-test-datasource/webpack.config.ts b/e2e/test-plugins/grafana-test-datasource/webpack.config.ts index 3303ed94f3c..564555396a5 100644 --- a/e2e/test-plugins/grafana-test-datasource/webpack.config.ts +++ b/e2e/test-plugins/grafana-test-datasource/webpack.config.ts @@ -1,7 +1,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; -import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; import { mergeWithCustomize, unique } from 'webpack-merge'; -import { Configuration } from 'webpack'; +import { type Configuration } from 'webpack'; function skipFiles(f: string): boolean { if (f.includes('/dist/')) { @@ -19,7 +19,7 @@ function skipFiles(f: string): boolean { return true; } -const config = async (env: Record): Promise => { +const config = async (env: Env): Promise => { const baseConfig = await grafanaConfig(env); const customConfig = { plugins: [ diff --git a/go.mod b/go.mod index 44aa93b4190..3096b0ef889 100644 --- a/go.mod +++ b/go.mod @@ -58,8 +58,6 @@ require ( github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage github.com/getkin/kin-openapi v0.132.0 // @grafana/grafana-app-platform-squad - github.com/go-git/go-billy/v5 v5.6.2 // @grafana/grafana-app-platform-squad - github.com/go-git/go-git/v5 v5.14.0 // @grafana/grafana-app-platform-squad github.com/go-jose/go-jose/v3 v3.0.4 // @grafana/identity-access-team github.com/go-jose/go-jose/v4 v4.1.0 // indirect; @grafana/identity-access-team github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group @@ -87,7 +85,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250709204613-c5c6f9c1653d // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics @@ -99,14 +97,14 @@ require ( github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend github.com/grafana/grafana-app-sdk v0.39.2 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-app-sdk/logging v0.39.1 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-aws-sdk v1.0.2 // @grafana/aws-datasources + github.com/grafana/grafana-aws-sdk v1.0.4 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.4.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.278.0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs - github.com/grafana/nanogit v0.0.0-20250625082556-a4828b879bbb // @grafana-app-platform-squad + github.com/grafana/nanogit v0.0.0-20250709085038-55508a6a9f40 // @grafana-app-platform-squad 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 github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling @@ -276,7 +274,6 @@ require ( github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect @@ -348,7 +345,6 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect @@ -372,7 +368,6 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gammazero/deque v0.2.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect @@ -423,7 +418,6 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jaegertracing/jaeger-idl v0.5.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect @@ -437,7 +431,6 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect @@ -492,7 +485,6 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect - github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect @@ -519,7 +511,6 @@ require ( github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect @@ -538,7 +529,6 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect @@ -586,7 +576,6 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect gopkg.in/telebot.v3 v3.2.1 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/apiextensions-apiserver v0.33.2 // indirect k8s.io/kms v0.33.2 // indirect modernc.org/libc v1.65.0 // indirect diff --git a/go.sum b/go.sum index 88a184a171a..c9040b526ff 100644 --- a/go.sum +++ b/go.sum @@ -744,7 +744,6 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -803,8 +802,6 @@ github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqR github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= @@ -1054,8 +1051,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= @@ -1200,8 +1195,6 @@ github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7M github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= -github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= @@ -1211,14 +1204,6 @@ github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3 github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= -github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= -github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -1586,8 +1571,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945 h1:3imTbxFpZSVI6IBIB9mn+Xc40lUweWjfMaBSgXR7rLs= -github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945/go.mod h1:gtR7agmxVfJOmNKV/n2ZULgOYTYNL+PDKYB5N48tQ7Q= +github.com/grafana/alerting v0.0.0-20250709204613-c5c6f9c1653d h1:rtlYpwsE3KDWWCg2kytDw3s5qgpDjG87qh1IixAyNz4= +github.com/grafana/alerting v0.0.0-20250709204613-c5c6f9c1653d/go.mod h1:gtR7agmxVfJOmNKV/n2ZULgOYTYNL+PDKYB5N48tQ7Q= github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed h1:k5Ng33zE9fCawqfEVybOasXY7/FQD5Qg2J92ePneeVM= github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM= @@ -1610,8 +1595,8 @@ github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmF github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= -github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9yhDIpkss= -github.com/grafana/grafana-aws-sdk v1.0.2/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= +github.com/grafana/grafana-aws-sdk v1.0.4 h1:D14UAehsOqpjliHmHzveRQ1p43KCsMzdmb7GovWj+SY= +github.com/grafana/grafana-aws-sdk v1.0.4/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= 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-cloud-migration-snapshot v1.6.0 h1:S4kHwr//AqhtL9xHBtz1gqVgZQeCRGTxjgsRBAkpjKY= @@ -1654,8 +1639,8 @@ github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 h1:ZYk42718k github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608/go.mod h1:f3JSoxBTPXX5ec4FxxeC19nTBSxoTz+cBgS3cYLMcr0= github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= -github.com/grafana/nanogit v0.0.0-20250625082556-a4828b879bbb h1:JR672zBiCkfiKuRRsU76TRC2q12q0VfZl2SP8jBIAJQ= -github.com/grafana/nanogit v0.0.0-20250625082556-a4828b879bbb/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco= +github.com/grafana/nanogit v0.0.0-20250709085038-55508a6a9f40 h1:wsIgOI4Ou1o/UtxtJlemLufpVBpMdcXVJxedk0wLoCM= +github.com/grafana/nanogit v0.0.0-20250709085038-55508a6a9f40/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250620093340-be61a673dee6 h1:oJnbhG6ZNy10AjsgNeAtAKeGHogIGOMfAsBH6fYYa5M= @@ -1815,8 +1800,6 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -1877,8 +1860,6 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -2175,8 +2156,6 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= -github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= -github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -2352,11 +2331,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -2481,8 +2457,6 @@ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+x github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= @@ -3542,8 +3516,6 @@ gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/package.json b/package.json index 3c4b63a1357..4ac0d312337 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "@testing-library/user-event": "14.6.1", "@types/babel__core": "^7", "@types/babel__preset-env": "^7", - "@types/chance": "^1.1.3", + "@types/chance": "^1.1.7", "@types/common-tags": "^1.8.0", "@types/confusing-browser-globals": "^1", "@types/d3": "7.4.3", @@ -164,23 +164,23 @@ "@types/yargs": "17.0.33", "@typescript-eslint/eslint-plugin": "8.35.1", "@typescript-eslint/parser": "8.35.1", - "autoprefixer": "10.4.20", + "autoprefixer": "10.4.21", "babel-loader": "9.2.1", "blob-polyfill": "9.0.20240710", "browserslist": "^4.21.4", - "chance": "^1.0.10", - "chrome-remote-interface": "0.33.2", + "chance": "^1.1.13", + "chrome-remote-interface": "0.33.3", "codeowners": "^5.1.1", "confusing-browser-globals": "^1.0.11", "copy-webpack-plugin": "12.0.2", "core-js": "3.40.0", "crashme": "0.0.15", "css-loader": "7.1.2", - "css-minimizer-webpack-plugin": "7.0.0", + "css-minimizer-webpack-plugin": "7.0.2", "cypress": "14.3.2", "cypress-file-upload": "5.0.8", "cypress-recurse": "^1.35.3", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "esbuild-loader": "4.3.0", "esbuild-plugin-browserslist": "^1.0.0", "eslint": "9.19.0", @@ -192,16 +192,16 @@ "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-lodash": "8.0.0", "eslint-plugin-no-barrel-files": "^1.1.1", - "eslint-plugin-react": "7.37.4", + "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.1.0", "eslint-plugin-testing-library": "^7.0.0", "eslint-plugin-unicorn": "^56.0.0", "eslint-scope": "^8.1.0", "eslint-webpack-plugin": "4.2.0", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "fishery": "^2.2.2", "fork-ts-checker-webpack-plugin": "9.0.2", - "glob": "11.0.1", + "glob": "11.0.3", "html-loader": "5.1.0", "html-webpack-plugin": "5.6.3", "http-server": "14.1.1", @@ -217,7 +217,7 @@ "jest-watch-typeahead": "^2.2.2", "jimp": "^1.6.0", "jsdom-testing-mocks": "^1.13.1", - "lerna": "8.2.1", + "lerna": "8.2.3", "mini-css-extract-plugin": "2.9.2", "msw": "2.10.3", "mutationobserver-shim": "0.3.7", @@ -227,7 +227,7 @@ "pa11y-ci": "^3.1.0", "pdf-parse": "^1.1.1", "plop": "^4.0.1", - "postcss": "8.5.1", + "postcss": "8.5.6", "postcss-loader": "8.1.1", "postcss-reporter": "7.1.0", "postcss-scss": "4.0.9", @@ -279,9 +279,9 @@ "@grafana/faro-web-sdk": "^1.13.2", "@grafana/faro-web-tracing": "^1.13.2", "@grafana/flamegraph": "workspace:*", - "@grafana/google-sdk": "0.3.2", + "@grafana/google-sdk": "0.3.4", "@grafana/i18n": "workspace:*", - "@grafana/lezer-logql": "0.2.7", + "@grafana/lezer-logql": "0.2.8", "@grafana/llm": "0.22.1", "@grafana/monaco-logql": "^0.0.8", "@grafana/o11y-ds-frontend": "workspace:*", @@ -356,6 +356,7 @@ "leven": "^4.0.0", "lodash": "4.17.21", "logfmt": "^1.3.2", + "lossless-json": "^4.1.1", "lru-cache": "11.1.0", "lru-memoize": "^1.1.0", "lucene": "^2.1.1", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index aa2da7aef48..9d43bc9b4fa 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -92,7 +92,7 @@ "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/tinycolor2": "1.4.6", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "react": "18.3.1", "react-dom": "18.3.1", "rimraf": "6.0.1", diff --git a/packages/grafana-data/src/context/plugins/DataSourcePluginContextProvider.tsx b/packages/grafana-data/src/context/plugins/DataSourcePluginContextProvider.tsx index 1464f1a6f6c..db5b5a5b610 100644 --- a/packages/grafana-data/src/context/plugins/DataSourcePluginContextProvider.tsx +++ b/packages/grafana-data/src/context/plugins/DataSourcePluginContextProvider.tsx @@ -2,7 +2,7 @@ import { PropsWithChildren, ReactElement, useMemo } from 'react'; import { DataSourceInstanceSettings } from '../../types/datasource'; -import { Context, DataSourcePluginContextType } from './PluginContext'; +import { PluginContext, DataSourcePluginContextType } from './PluginContext'; export type DataSourcePluginContextProviderProps = { instanceSettings: DataSourceInstanceSettings; @@ -16,5 +16,5 @@ export function DataSourcePluginContextProvider( return { instanceSettings, meta: instanceSettings.meta }; }, [instanceSettings]); - return {children}; + return {children}; } diff --git a/packages/grafana-data/src/context/plugins/PluginContext.tsx b/packages/grafana-data/src/context/plugins/PluginContext.tsx index f8748617541..7471109d93d 100644 --- a/packages/grafana-data/src/context/plugins/PluginContext.tsx +++ b/packages/grafana-data/src/context/plugins/PluginContext.tsx @@ -1,14 +1,15 @@ import { createContext } from 'react'; +import { KeyValue } from '../../types/data'; import { DataSourceInstanceSettings } from '../../types/datasource'; import { PluginMeta } from '../../types/plugin'; -export interface PluginContextType { - meta: PluginMeta; +export interface PluginContextType { + meta: PluginMeta; } -export interface DataSourcePluginContextType extends PluginContextType { +export interface DataSourcePluginContextType extends PluginContextType { instanceSettings: DataSourceInstanceSettings; } -export const Context = createContext(undefined); +export const PluginContext = createContext(undefined); diff --git a/packages/grafana-data/src/context/plugins/PluginContextProvider.tsx b/packages/grafana-data/src/context/plugins/PluginContextProvider.tsx index f60381c5aaa..c7698b18a34 100644 --- a/packages/grafana-data/src/context/plugins/PluginContextProvider.tsx +++ b/packages/grafana-data/src/context/plugins/PluginContextProvider.tsx @@ -2,7 +2,7 @@ import { PropsWithChildren, ReactElement } from 'react'; import { PluginMeta } from '../../types/plugin'; -import { Context } from './PluginContext'; +import { PluginContext } from './PluginContext'; export type PluginContextProviderProps = { meta: PluginMeta; @@ -10,5 +10,5 @@ export type PluginContextProviderProps = { export function PluginContextProvider(props: PropsWithChildren): ReactElement { const { children, ...rest } = props; - return {children}; + return {children}; } diff --git a/packages/grafana-data/src/context/plugins/guards.ts b/packages/grafana-data/src/context/plugins/guards.ts index 3a93c862f09..3b89b24dfed 100644 --- a/packages/grafana-data/src/context/plugins/guards.ts +++ b/packages/grafana-data/src/context/plugins/guards.ts @@ -1,5 +1,9 @@ +import { KeyValue } from '../../types/data'; + import { type DataSourcePluginContextType, type PluginContextType } from './PluginContext'; -export function isDataSourcePluginContext(context: PluginContextType): context is DataSourcePluginContextType { +export function isDataSourcePluginContext( + context: PluginContextType +): context is DataSourcePluginContextType { return 'instanceSettings' in context && 'meta' in context; } diff --git a/packages/grafana-data/src/context/plugins/usePluginContext.tsx b/packages/grafana-data/src/context/plugins/usePluginContext.tsx index 58190a7e4e1..b00875909d7 100644 --- a/packages/grafana-data/src/context/plugins/usePluginContext.tsx +++ b/packages/grafana-data/src/context/plugins/usePluginContext.tsx @@ -1,9 +1,11 @@ import { useContext } from 'react'; -import { Context, PluginContextType } from './PluginContext'; +import { KeyValue } from '../../types/data'; -export function usePluginContext(): PluginContextType | null { - const context = useContext(Context); +import { PluginContext, PluginContextType } from './PluginContext'; + +export function usePluginContext(): PluginContextType | null { + const context = useContext(PluginContext); // The extensions hooks (e.g. `usePluginLinks()`) are using this hook to check // if they are inside a plugin or not (core Grafana), so we should be able to return an empty state as well (`null`). @@ -11,5 +13,6 @@ export function usePluginContext(): PluginContextType | null { return null; } - return context; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return context as PluginContextType; } diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 80351af8bc2..bc9e4d245db 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -435,7 +435,7 @@ export { type GroupingToMatrixTransformerOptions } from './transformations/trans export { type PluginContextType, type DataSourcePluginContextType, - Context as PluginContext, + PluginContext, } from './context/plugins/PluginContext'; export { type PluginContextProviderProps, PluginContextProvider } from './context/plugins/PluginContextProvider'; export { diff --git a/packages/grafana-data/src/transformations/standardTransformersRegistry.ts b/packages/grafana-data/src/transformations/standardTransformersRegistry.ts index 4e5a11d318a..54166fe33d1 100644 --- a/packages/grafana-data/src/transformations/standardTransformersRegistry.ts +++ b/packages/grafana-data/src/transformations/standardTransformersRegistry.ts @@ -34,6 +34,11 @@ export interface TransformerRegistryItem extends RegistryItem { * Set of categories associated with the transformer */ categories?: Set; + + /** + * Set of tags associated with the transformer for improved transformation search + */ + tags?: Set; } export enum TransformerCategory { diff --git a/packages/grafana-data/src/transformations/transformers/limit.test.ts b/packages/grafana-data/src/transformations/transformers/limit.test.ts index 9fde97b0283..a042dc5f34e 100644 --- a/packages/grafana-data/src/transformations/transformers/limit.test.ts +++ b/packages/grafana-data/src/transformations/transformers/limit.test.ts @@ -35,18 +35,21 @@ describe('Limit transformer', () => { { name: 'time', type: FieldType.time, + state: { calcs: undefined }, values: [3000, 4000, 5000], config: {}, }, { name: 'message', type: FieldType.string, + state: { calcs: undefined }, values: ['one', 'two', 'two'], config: {}, }, { name: 'values', type: FieldType.number, + state: { calcs: undefined }, values: [1, 2, 2], config: {}, }, @@ -79,18 +82,21 @@ describe('Limit transformer', () => { { name: 'time', type: FieldType.time, + state: { calcs: undefined }, values: [6000, 7000, 8000], config: {}, }, { name: 'message', type: FieldType.string, + state: { calcs: undefined }, values: ['three', 'three', 'three'], config: {}, }, { name: 'values', type: FieldType.number, + state: { calcs: undefined }, values: [3, 3, 3], config: {}, }, diff --git a/packages/grafana-data/src/transformations/transformers/limit.ts b/packages/grafana-data/src/transformations/transformers/limit.ts index 230e66d9dd3..4361e93c385 100644 --- a/packages/grafana-data/src/transformations/transformers/limit.ts +++ b/packages/grafana-data/src/transformations/transformers/limit.ts @@ -37,6 +37,12 @@ export const limitTransformer: DataTransformerInfo = { fields: frame.fields.map((f) => { return { ...f, + // Clear cached field calculations since applying a limit changes the dataset + // and previously computed stats (min, max, mean, etc.) are no longer valid + state: { + ...f.state, + calcs: undefined, + }, values: limit >= 0 ? f.values.slice(0, limit) : f.values.slice(f.values.length + limit, f.values.length), }; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 4a5de4fd928..c88173cf331 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -210,10 +210,6 @@ export interface FeatureToggles { */ provisioning?: boolean; /** - * Use experimental git library for provisioning - */ - nanoGit?: boolean; - /** * Start an additional https handler and write kubectl options */ grafanaAPIServerEnsureKubectlAccess?: boolean; @@ -738,11 +734,6 @@ export interface FeatureToggles { */ crashDetection?: boolean; /** - * Enables querying the Jaeger data source without the proxy - * @default true - */ - jaegerBackendMigration?: boolean; - /** * Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query * @default true */ @@ -999,7 +990,7 @@ export interface FeatureToggles { preferLibraryPanelTitle?: boolean; /** * Use fixed-width numbers globally in the UI - * @default true + * @default false */ tabularNumbers?: boolean; /** diff --git a/packages/grafana-data/src/utils/fuzzySearch.test.ts b/packages/grafana-data/src/utils/fuzzySearch.test.ts index 71d75b27558..b53c26e98aa 100644 --- a/packages/grafana-data/src/utils/fuzzySearch.test.ts +++ b/packages/grafana-data/src/utils/fuzzySearch.test.ts @@ -48,6 +48,14 @@ describe('fuzzySearch', () => { expect(result.map((idx) => haystack[idx])).toEqual(['A水']); }); + it('should do case-insensitive substring match when needle contains non-ascii characters', () => { + const haystack = ['Über']; + const needle = 'ü'; + const result = fuzzySearch(haystack, needle); + + expect(result.map((idx) => haystack[idx])).toEqual(['Über']); + }); + it('should handle multiple non-latin characters', () => { const haystack = ['台灣省', '台中市', '台北市', '台南市', '南投縣', '高雄市', '台中第一高級中學']; const needle = '南'; diff --git a/packages/grafana-data/src/utils/fuzzySearch.ts b/packages/grafana-data/src/utils/fuzzySearch.ts index 72d1540f7b9..09ca02b7bc8 100644 --- a/packages/grafana-data/src/utils/fuzzySearch.ts +++ b/packages/grafana-data/src/utils/fuzzySearch.ts @@ -1,5 +1,7 @@ import uFuzzy from '@leeoniya/ufuzzy'; +import { escapeRegex } from '../text/string'; + // https://catonmat.net/my-favorite-regex :) const REGEXP_NON_ASCII = /[^ -~]/m; // https://www.asciitable.com/ @@ -36,11 +38,13 @@ export function fuzzySearch(haystack: string[], needle: string): number[] { needle.length > maxNeedleLength || uf.split(needle).length > maxFuzzyTerms ) { + const needleRegex = new RegExp(escapeRegex(needle), 'i'); const indices: number[] = []; + for (let i = 0; i < haystack.length; i++) { let item = haystack[i]; - if (item.includes(needle)) { + if (needleRegex.test(item)) { indices.push(i); } } diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index aaa4a34e671..265adc2ffdb 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -42,7 +42,7 @@ "@rollup/plugin-node-resolve": "16.0.1", "@types/node": "22.15.0", "@types/semver": "7.7.0", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-esbuild": "6.2.0", diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 0ff43b6807e..088caf6dfa1 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -1179,11 +1179,6 @@ export const versionedComponents = { '12.1.0': 'data-testid Data links actions tooltip wrapper', }, }, - TablePanel: { - autoCell: { - '12.1.0': 'data-testid Table panel auto cell', - }, - }, CodeEditor: { container: { '10.2.3': 'data-testid Code editor container', diff --git a/packages/grafana-flamegraph/README.md b/packages/grafana-flamegraph/README.md index 9bd09dfc6c6..5022f4b2199 100644 --- a/packages/grafana-flamegraph/README.md +++ b/packages/grafana-flamegraph/README.md @@ -9,7 +9,7 @@ This is a Flamegraph component that is used in Grafana and Pyroscope web app to Currently this library exposes single component `Flamegraph` that renders whole visualization used for profiling which contains a header, a table representation of the data and a flamegraph. ```tsx -import { Flamegraph } from '@grafana/flamegraph'; +import { FlameGraph } from '@grafana/flamegraph'; createTheme({ colors: { mode: 'dark' } })} diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 4e744fa3d8a..93e41401ba8 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -73,7 +73,7 @@ "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", "rollup": "^4.22.4", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 845ae3a750f..cf0cc967eaa 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -6,6 +6,7 @@ "dependencies": { "tslib": "2.8.1" }, + "type": "module", "devDependencies": { "@grafana/tsconfig": "^2.0.0", "@swc/core": "1.10.12", @@ -15,7 +16,7 @@ "eslint": "9.19.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", - "glob": "11.0.1", + "glob": "11.0.3", "imports-loader": "^5.0.0", "replace-in-file-webpack-plugin": "1.0.6", "swc-loader": "0.2.6", diff --git a/packages/grafana-plugin-configs/tsconfig.json b/packages/grafana-plugin-configs/tsconfig.json index 1790c28c284..b7a0dd239e8 100644 --- a/packages/grafana-plugin-configs/tsconfig.json +++ b/packages/grafana-plugin-configs/tsconfig.json @@ -4,19 +4,11 @@ "alwaysStrict": true, "declaration": false, "resolveJsonModule": true, - "moduleResolution": "bundler" - }, - "ts-node": { - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "moduleResolution": "Node", - "esModuleInterop": true - }, - "transpileOnly": true + "moduleResolution": "bundler", + "noEmit": true, + "allowImportingTsExtensions": true }, "extends": "@grafana/tsconfig", - "exclude": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"], "include": ["./types", "."] } diff --git a/packages/grafana-plugin-configs/types/replace-in-webpack-plugin.d.ts b/packages/grafana-plugin-configs/types/replace-in-webpack-plugin.d.ts new file mode 100644 index 00000000000..746b3e0d303 --- /dev/null +++ b/packages/grafana-plugin-configs/types/replace-in-webpack-plugin.d.ts @@ -0,0 +1,23 @@ +declare module 'replace-in-file-webpack-plugin' { + import { Compiler, Plugin } from 'webpack'; + + interface ReplaceRule { + search: string | RegExp; + replace: string | ((match: string) => string); + } + + interface ReplaceOption { + dir?: string; + files?: string[]; + test?: RegExp | RegExp[]; + rules: ReplaceRule[]; + } + + class ReplaceInFilePlugin extends Plugin { + constructor(options?: ReplaceOption[]); + options: ReplaceOption[]; + apply(compiler: Compiler): void; + } + + export = ReplaceInFilePlugin; +} diff --git a/packages/grafana-plugin-configs/utils.ts b/packages/grafana-plugin-configs/utils.ts index 3a74dd90af6..7c517dc2d18 100644 --- a/packages/grafana-plugin-configs/utils.ts +++ b/packages/grafana-plugin-configs/utils.ts @@ -3,12 +3,19 @@ import { glob } from 'glob'; import path from 'path'; import process from 'process'; +// Support for node 22 and 24. Due to the many changes in importing json files +// across recent node versions we load the json files using fs for simplicity. +function loadJson(path: string) { + const rawJson = fs.readFileSync(path, 'utf8'); + return JSON.parse(rawJson); +} + export function getPackageJson() { - return require(path.resolve(process.cwd(), 'package.json')); + return loadJson(path.resolve(process.cwd(), 'package.json')); } export function getPluginJson() { - return require(path.resolve(process.cwd(), 'plugin.json')); + return loadJson(path.resolve(process.cwd(), 'plugin.json')); } export async function getEntries(): Promise> { diff --git a/packages/grafana-plugin-configs/webpack.config.ts b/packages/grafana-plugin-configs/webpack.config.ts index 7714093000a..c1bfc469d7b 100644 --- a/packages/grafana-plugin-configs/webpack.config.ts +++ b/packages/grafana-plugin-configs/webpack.config.ts @@ -2,15 +2,14 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; import ESLintPlugin from 'eslint-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import path from 'path'; -// @ts-expect-error - there are no types for this package import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin'; -import { type Configuration, BannerPlugin } from 'webpack'; +import webpack, { type Configuration } from 'webpack'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import VirtualModulesPlugin from 'webpack-virtual-modules'; -import { DIST_DIR } from './constants'; -import { getPackageJson, getPluginJson, getEntries, hasLicense } from './utils'; +import { DIST_DIR } from './constants.ts'; +import { getPackageJson, getPluginJson, getEntries, hasLicense } from './utils.ts'; function skipFiles(f: string): boolean { if (f.includes('/dist/')) { @@ -32,7 +31,7 @@ function skipFiles(f: string): boolean { return true; } -type Env = { +export type Env = { [key: string]: true | string | Env; }; @@ -54,9 +53,13 @@ const config = async (env: Env): Promise => { cache: { type: 'filesystem', buildDependencies: { - config: [__filename], + config: [import.meta.filename], }, - cacheDirectory: path.resolve(__dirname, '../../node_modules/.cache/webpack', path.basename(process.cwd())), + cacheDirectory: path.resolve( + import.meta.dirname, + '../../node_modules/.cache/webpack', + path.basename(process.cwd()) + ), }, context: process.cwd(), @@ -114,7 +117,7 @@ const config = async (env: Env): Promise => { test: /module\.tsx?$/, use: [ { - loader: require.resolve('imports-loader'), + loader: 'imports-loader', options: { imports: `side-effects grafana-public-path`, }, @@ -125,10 +128,10 @@ const config = async (env: Env): Promise => { exclude: /(node_modules)/, test: /\.[tj]sx?$/, use: { - loader: require.resolve('swc-loader'), + loader: 'swc-loader', options: { jsc: { - baseUrl: path.resolve(__dirname), + baseUrl: path.resolve(import.meta.dirname), target: 'es2015', loose: false, parser: { @@ -209,7 +212,7 @@ const config = async (env: Env): Promise => { plugins: [ virtualPublicPath, // Insert create plugin version information into the bundle so Grafana will load from cdn with script tags. - new BannerPlugin({ + new webpack.BannerPlugin({ banner: '/* [create-plugin] version: 5.22.0 */', raw: true, entryOnly: true, @@ -264,7 +267,7 @@ const config = async (env: Env): Promise => { extensions: ['.ts', '.tsx'], lintDirtyModulesOnly: true, // don't lint on start, only lint changed files cacheLocation: path.resolve( - __dirname, + import.meta.dirname, '../../node_modules/.cache/eslint-webpack-plugin', path.basename(process.cwd()), '.eslintcache' diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 973cd50edaf..d6a5ef7fd3e 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -90,7 +90,7 @@ "@types/node": "22.15.0", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "i18next-parser": "9.3.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 7eab5671779..3d129df19ae 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -78,7 +78,7 @@ "@types/lodash": "4.17.20", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 466b6adcbfc..8dc2e5efe1f 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -38,7 +38,7 @@ "devDependencies": { "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "16.0.1", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "glob": "^11.0.0", "rimraf": "6.0.1", "rollup": "^4.22.4", diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index 4c1fd34f48e..ff937656095 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -708,6 +708,7 @@ export enum TableCellDisplayMode { Image = 'image', JSONView = 'json-view', LcdGauge = 'lcd-gauge', + Pill = 'pill', Sparkline = 'sparkline', } @@ -838,7 +839,38 @@ export enum TableCellHeight { * Table cell options. Each cell has a display mode * and other potential options for that display. */ -export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions); +export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions); + +/** + * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) + * Generally defines alignment, filtering capabilties, display options, etc. + */ +export interface TableFieldOptions { + align: FieldTextAlignment; + cellOptions: TableCellOptions; + /** + * This field is deprecated in favor of using cellOptions + */ + displayMode?: TableCellDisplayMode; + filterable?: boolean; + hidden?: boolean; // ?? default is missing or false ?? + /** + * Hides any header for a column, useful for columns that show some static content or buttons. + */ + hideHeader?: boolean; + inspect: boolean; + minWidth?: number; + width?: number; + /** + * Enables text wrapping for column headers + */ + wrapHeaderText?: boolean; +} + +export const defaultTableFieldOptions: Partial = { + align: 'auto', + inspect: false, +}; /** * Use UTC/GMT timezone @@ -944,37 +976,12 @@ export enum ComparisonOperation { NEQ = 'neq', } -/** - * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) - * Generally defines alignment, filtering capabilties, display options, etc. - */ -export interface TableFieldOptions { - align: FieldTextAlignment; - cellOptions: TableCellOptions; - /** - * This field is deprecated in favor of using cellOptions - */ - displayMode?: TableCellDisplayMode; - filterable?: boolean; - hidden?: boolean; // ?? default is missing or false ?? - /** - * Hides any header for a column, useful for columns that show some static content or buttons. - */ - hideHeader?: boolean; - inspect: boolean; - minWidth?: number; - width?: number; - /** - * Enables text wrapping for column headers - */ - wrapHeaderText?: boolean; +export interface TablePillCellOptions { + color?: string; + colorMode?: ('auto' | 'fixed' | 'mapped'); + type: TableCellDisplayMode.Pill; } -export const defaultTableFieldOptions: Partial = { - align: 'auto', - inspect: false, -}; - /** * A specific timezone from https://en.wikipedia.org/wiki/Tz_database */ diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue index ca61617aedb..0858150ae4c 100644 --- a/packages/grafana-schema/src/common/table.cue +++ b/packages/grafana-schema/src/common/table.cue @@ -4,7 +4,7 @@ package common // in the table such as colored text, JSON, gauge, etc. // The color-background-solid, gradient-gauge, and lcd-gauge // modes are deprecated in favor of new cell subOptions -TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions") +TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" | "pill" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions|Pill") // Display mode to the "Colored Background" display // mode for table cells. Either displays a solid color (basic mode) @@ -89,7 +89,7 @@ TableCellHeight: "sm" | "md" | "lg" | "auto" @cuetsy(kind="enum") // Table cell options. Each cell has a display mode // and other potential options for that display. -TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type") +TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type") // Field options for each field within a table (e.g 10, "The String", 64.20, etc.) // Generally defines alignment, filtering capabilties, display options, etc. @@ -109,3 +109,9 @@ TableFieldOptions: { wrapHeaderText?: bool } @cuetsy(kind="interface") +TablePillCellOptions: { + type: TableCellDisplayMode & "pill" + color?: string + colorMode?: "auto" | "fixed" | "mapped" +} @cuetsy(kind="interface") + diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index dbf274e51bc..73b7933cd79 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -15,6 +15,7 @@ export const pluginVersion = "12.1.0-pre"; export interface Options { controlsStorageKey?: string; dedupStrategy: common.LogsDedupStrategy; + detailsMode?: ('inline' | 'sidebar'); displayedFields?: Array; enableInfiniteScrolling?: boolean; enableLogDetails: boolean; diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 61b29e2ee2e..0075a197ffa 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -18,7 +18,7 @@ export interface AnnotationQuerySpec { name: string; builtIn?: boolean; filter?: AnnotationPanelFilter; - // Catch-all field for datasource-specific properties. Should not be available in as code tooling. + // Catch-all field for datasource-specific properties legacyOptions?: Record; } diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index f0e1b8692d1..854e58ae28a 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -156,7 +156,7 @@ "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", - "@types/chance": "1.1.6", + "@types/chance": "^1.1.7", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.3", "@types/hoist-non-react-statics": "3.3.6", @@ -176,13 +176,13 @@ "@types/slate-react": "0.22.9", "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", - "chance": "1.1.12", + "chance": "^1.1.13", "common-tags": "1.8.2", "core-js": "3.40.0", "css-loader": "7.1.2", "csstype": "3.1.3", - "esbuild": "0.25.0", - "expose-loader": "5.0.0", + "esbuild": "0.25.6", + "expose-loader": "5.0.1", "fs-extra": "^11.2.0", "mock-raf": "1.0.1", "msw": "^2.10.2", diff --git a/packages/grafana-ui/src/components/Cascader/styles.ts b/packages/grafana-ui/src/components/Cascader/styles.ts index cef38b62dcc..68055527b24 100644 --- a/packages/grafana-ui/src/components/Cascader/styles.ts +++ b/packages/grafana-ui/src/components/Cascader/styles.ts @@ -2,6 +2,8 @@ import { css, keyframes } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; +import { getIconPath } from '../Icon/utils'; + const slideUpIn = keyframes({ '0%': { opacity: 0, @@ -58,143 +60,146 @@ const slideDownOut = keyframes({ }, }); -export const getCascaderStyles = (theme: GrafanaTheme2) => ({ - dropdown: css({ - '&.rc-cascader-dropdown': { - position: 'absolute', - // Required, otherwise the portal that the popup is shown in will render under other components - zIndex: 9999, +export const getCascaderStyles = (theme: GrafanaTheme2) => { + const iconPath = getIconPath('angle-right'); + return { + dropdown: css({ + '&.rc-cascader-dropdown': { + position: 'absolute', + // Required, otherwise the portal that the popup is shown in will render under other components + zIndex: 9999, - '&-hidden': { - display: 'none', - }, - }, - '.rc-cascader': { - '&-menus': { - overflow: 'hidden', - background: theme.colors.background.elevated, - border: `none`, - borderRadius: theme.shape.radius.default, - boxShadow: theme.shadows.z3, - whiteSpace: 'nowrap', - - '&.slide-up-enter, &.slide-up-appear': { - animationDuration: '0.3s', - animationFillMode: 'both', - transformOrigin: '0 0', - opacity: 0, - animationTimingFunction: 'cubic-bezier(0.08, 0.82, 0.17, 1)', - animationPlayState: 'paused', - }, - - '&.slide-up-enter.slide-up-enter-active.rc-cascader-menus-placement, &.slide-up-appear.slide-up-appear-active.rc-cascader-menus-placement': - { - '&-bottomLeft': { - animationName: slideUpIn, - animationPlayState: 'running', - }, - - '&-topLeft': { - animationName: slideDownIn, - animationPlayState: 'running', - }, - }, - - '&.slide-up-leave': { - animationDuration: '0.3s', - animationFillMode: 'both', - transformOrigin: '0 0', - opacity: 1, - animationTimingFunction: 'cubic-bezier(0.6, 0.04, 0.98, 0.34)', - animationPlayState: 'paused', - - '&.slide-up-leave-active.rc-cascader-menus-placement': { - '&-bottomLeft': { - animationName: slideUpOut, - animationPlayState: 'running', - }, - - '&-topLeft': { - animationName: slideDownOut, - animationPlayState: 'running', - }, - }, + '&-hidden': { + display: 'none', }, }, - - '&-menu': { - display: 'inline-block', - maxWidth: '50vw', - height: '192px', - listStyle: 'none', - margin: 0, - padding: theme.spacing(0.5), - borderRight: `1px solid ${theme.colors.border.weak}`, - overflow: 'auto', - - '&:last-child': { - borderRight: 0, - }, - - '&-item': { - height: theme.spacing(4), - lineHeight: theme.spacing(4), - padding: theme.spacing(0, 4, 0, 2), - borderRadius: theme.shape.radius.default, - cursor: 'pointer', - whiteSpace: 'nowrap', + '.rc-cascader': { + '&-menus': { overflow: 'hidden', - textOverflow: 'ellipsis', - transition: 'all 0.3s ease', - position: 'relative', + background: theme.colors.background.elevated, + border: `none`, + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z3, + whiteSpace: 'nowrap', - '&:hover': { - background: theme.colors.action.hover, + '&.slide-up-enter, &.slide-up-appear': { + animationDuration: '0.3s', + animationFillMode: 'both', + transformOrigin: '0 0', + opacity: 0, + animationTimingFunction: 'cubic-bezier(0.08, 0.82, 0.17, 1)', + animationPlayState: 'paused', }, - '&-disabled': { - cursor: 'not-allowed', - color: theme.colors.text.disabled, + '&.slide-up-enter.slide-up-enter-active.rc-cascader-menus-placement, &.slide-up-appear.slide-up-appear-active.rc-cascader-menus-placement': + { + '&-bottomLeft': { + animationName: slideUpIn, + animationPlayState: 'running', + }, - '&:hover': { - background: 'transparent', + '&-topLeft': { + animationName: slideDownIn, + animationPlayState: 'running', + }, }, - '&:after': { - position: 'absolute', - right: '12px', - content: "'loading'", - color: theme.colors.text.disabled, - fontStyle: 'italic', + '&.slide-up-leave': { + animationDuration: '0.3s', + animationFillMode: 'both', + transformOrigin: '0 0', + opacity: 1, + animationTimingFunction: 'cubic-bezier(0.6, 0.04, 0.98, 0.34)', + animationPlayState: 'paused', + + '&.slide-up-leave-active.rc-cascader-menus-placement': { + '&-bottomLeft': { + animationName: slideUpOut, + animationPlayState: 'running', + }, + + '&-topLeft': { + animationName: slideDownOut, + animationPlayState: 'running', + }, }, }, + }, - '&-active': { - color: theme.colors.text.maxContrast, - background: theme.colors.background.secondary, + '&-menu': { + display: 'inline-block', + maxWidth: '50vw', + height: '192px', + listStyle: 'none', + margin: 0, + padding: theme.spacing(0.5), + borderRight: `1px solid ${theme.colors.border.weak}`, + overflow: 'auto', + + '&:last-child': { + borderRight: 0, + }, + + '&-item': { + height: theme.spacing(4), + lineHeight: theme.spacing(4), + padding: theme.spacing(0, 4, 0, 2), + borderRadius: theme.shape.radius.default, + cursor: 'pointer', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + transition: 'all 0.3s ease', + position: 'relative', '&:hover': { background: theme.colors.action.hover, }, - }, - '&-expand': { - position: 'relative', + '&-disabled': { + cursor: 'not-allowed', + color: theme.colors.text.disabled, - '&:after': { + '&:hover': { + background: 'transparent', + }, + + '&:after': { + position: 'absolute', + right: '12px', + content: "'loading'", + color: theme.colors.text.disabled, + fontStyle: 'italic', + }, + }, + + '&-active': { + color: theme.colors.text.maxContrast, background: theme.colors.background.secondary, - content: "''", - height: theme.spacing(3), - mask: 'url(../img/icons/unicons/angle-right.svg)', - maskType: 'luminance', - position: 'absolute', - right: 0, - top: theme.spacing(0.5), - width: theme.spacing(3), + + '&:hover': { + background: theme.colors.action.hover, + }, + }, + + '&-expand': { + position: 'relative', + + '&:after': { + background: theme.colors.text.primary, + content: "''", + height: theme.spacing(3), + mask: `url(${iconPath})`, + maskType: 'luminance', + position: 'absolute', + right: 0, + top: theme.spacing(0.5), + width: theme.spacing(3), + }, }, }, }, }, - }, - }), -}); + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index f961cafcc14..59135b9a89f 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -8,7 +8,7 @@ import { useStyles2 } from '../../themes/ThemeContext'; import { IconName, IconType, IconSize } from '../../types/icon'; import { spin } from '../../utils/keyframes'; -import { getIconRoot, getIconSubDir, getSvgSize } from './utils'; +import { getIconPath, getSvgSize } from './utils'; export interface IconProps extends Omit, 'onLoad' | 'onError' | 'ref'> { name: IconName; @@ -53,12 +53,10 @@ export const Icon = React.forwardRef( // handle the deprecated 'fa fa-spinner' const iconName: IconName = name === 'fa fa-spinner' ? 'spinner' : name; - const iconRoot = getIconRoot(); const svgSize = getSvgSize(size); const svgHgt = svgSize; const svgWid = name.startsWith('gf-bar-align') ? 16 : name.startsWith('gf-interp') ? 30 : svgSize; - const subDir = getIconSubDir(iconName, type); - const svgPath = `${iconRoot}${subDir}/${iconName}.svg`; + const svgPath = getIconPath(iconName, type); const composedClassName = cx( styles.icon, diff --git a/packages/grafana-ui/src/components/Icon/utils.ts b/packages/grafana-ui/src/components/Icon/utils.ts index 263e68ac0ff..30f350ee667 100644 --- a/packages/grafana-ui/src/components/Icon/utils.ts +++ b/packages/grafana-ui/src/components/Icon/utils.ts @@ -60,3 +60,9 @@ export function getIconRoot(): string { return iconRoot; } + +export function getIconPath(name: IconName, type: IconType = 'default'): string { + const iconRoot = getIconRoot(); + const subDir = getIconSubDir(name, type); + return `${iconRoot}${subDir}/${name}.svg`; +} diff --git a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx index 1bd8774479b..5b2e28c646f 100644 --- a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx +++ b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx @@ -74,8 +74,6 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt const dismiss = useDismiss(context); - const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length); - const { getFloatingProps, getReferenceProps } = useInteractions([dismiss]); if (links.length === 0 && !Boolean(actions?.length)) { @@ -84,23 +82,22 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt return ( <> + {/* TODO: we can remove `value` from this component when tableNextGen is fully rolled out */} {value} - {hasMultipleLinksOrActions && ( - -
- - - -
-
- )} + +
+ + + +
+
); }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx index 9616baa5a76..087efa47ecc 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; @@ -6,12 +7,16 @@ import { useStyles2 } from '../../../../themes/ThemeContext'; import { ActionButton } from '../../../Actions/ActionButton'; import { ActionCellProps } from '../types'; -export const ActionsCell = ({ actions }: ActionCellProps) => { +export const ActionsCell = ({ field, rowIdx, getActions }: ActionCellProps) => { const styles = useStyles2(getStyles); + const actions = useMemo(() => getActions(field, rowIdx), [getActions, field, rowIdx]); + return (
- {actions && actions.map((action, i) => )} + {actions.map((action, i) => ( + + ))}
); }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx deleted file mode 100644 index b65fa0e0297..00000000000 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { Field, FieldType, LinkModel } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; - -import { TableCellDisplayMode } from '../../types'; - -import AutoCell from './AutoCell'; - -describe('AutoCell', () => { - describe('Displays data Links', () => { - const getFieldWithLinks = (links: LinkModel[]): Field => { - return { - name: 'Category', - type: FieldType.string, - values: ['A', 'B', 'A', 'B', 'A'], - config: { - custom: { - cellOptions: { - type: TableCellDisplayMode.Auto, - wrapText: false, - }, - }, - }, - display: (value: unknown) => ({ - text: String(value), - numeric: 0, - color: undefined, - prefix: undefined, - suffix: undefined, - }), - state: {}, - getLinks: () => links, - }; - }; - - it('shows multiple datalinks in the tooltip', async () => { - const linksForField = [ - { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel, - { href: 'http://asdasd2.com', title: 'Test Title2' } as LinkModel, - ]; - - jest.mock('../utils', () => ({ - getCellLinks: () => linksForField, - })); - - const field = getFieldWithLinks(linksForField); - - render( - - ); - - const cell = screen.getByTestId(selectors.components.TablePanel.autoCell); - await userEvent.click(cell); - - const tooltip = screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper); - expect(tooltip).toBeInTheDocument(); - expect(screen.getByText('Test Title')).toBeInTheDocument(); - expect(screen.getByText('Test Title2')).toBeInTheDocument(); - }); - - it('does not show tooltip for multiple links if one is invalid', async () => { - const linksForField = [ - { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel, - { title: 'Test Title2' } as LinkModel, - ]; - - jest.mock('../utils', () => ({ - getCellLinks: () => linksForField, - })); - - const field = getFieldWithLinks(linksForField); - - render( - - ); - - const cell = screen.getByTestId(selectors.components.TablePanel.autoCell); - await userEvent.click(cell); - - expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument(); - }); - }); -}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx index 3fdcfc85491..71293bbc06e 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx @@ -1,49 +1,24 @@ import { css } from '@emotion/css'; import { Property } from 'csstype'; -import { useState } from 'react'; import { GrafanaTheme2, formattedValueToString } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../../../themes/ThemeContext'; -import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; import { TableCellOptions, TableCellDisplayMode } from '../../types'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils'; +import { useSingleLink } from '../hooks'; import { AutoCellProps } from '../types'; -import { getCellLinks } from '../utils'; -export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions, actions }: AutoCellProps) { +export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions }: AutoCellProps) { const styles = useStyles2(getStyles, justifyContent); const displayValue = field.display!(value); const formattedValue = formattedValueToString(displayValue); - const links = getCellLinks(field, rowIdx) || []; - - const [tooltipCoords, setTooltipCoords] = useState(); - const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions); - const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined; + const link = useSingleLink(field, rowIdx); return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- {shouldShowLink ? ( - renderSingleLink(links[0], formattedValue, getLinkStyle(styles, cellOptions)) - ) : shouldShowTooltip ? ( - setTooltipCoords(undefined)} - /> - ) : ( - formattedValue - )} +
+ {link == null ? formattedValue : renderSingleLink(link, formattedValue, getLinkStyle(styles, cellOptions))}
); } diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx index 710a8c65726..69ebfac671c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx @@ -1,13 +1,11 @@ -import { useState } from 'react'; - import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data'; import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema'; import { BarGauge } from '../../../BarGauge/BarGauge'; -import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; -import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; +import { useSingleLink } from '../hooks'; import { BarGaugeCellProps } from '../types'; -import { extractPixelValue, getCellOptions, getAlignmentFactor, getCellLinks } from '../utils'; +import { extractPixelValue, getCellOptions, getAlignmentFactor } from '../utils'; const defaultScale: ThresholdsConfig = { mode: ThresholdsMode.Absolute, @@ -23,7 +21,7 @@ const defaultScale: ThresholdsConfig = { ], }; -export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actions }: BarGaugeCellProps) => { +export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: BarGaugeCellProps) => { const displayValue = field.display!(value); const cellOptions = getCellOptions(field); const heightOffset = extractPixelValue(theme.spacing(1)); @@ -48,51 +46,26 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actio } const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!); - const links = getCellLinks(field, rowIdx) || []; - const [tooltipCoords, setTooltipCoords] = useState(); - const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions); - const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined; - - const renderComponent = () => { - return ( - - ); - }; - - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- {shouldShowLink ? ( - renderSingleLink(links[0], renderComponent()) - ) : shouldShowTooltip ? ( - setTooltipCoords(undefined)} - /> - ) : ( - renderComponent() - )} -
+ const barGaugeComponent = ( + ); + + const link = useSingleLink(field, rowIdx); + + return link == null ? barGaugeComponent : renderSingleLink(link, barGaugeComponent); }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx index bc62e266b12..e0c1050396a 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx @@ -1,55 +1,28 @@ import { css } from '@emotion/css'; import { Property } from 'csstype'; -import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../../themes/ThemeContext'; -import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; import { TableCellDisplayMode } from '../../types'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils'; +import { useSingleLink } from '../hooks'; import { ImageCellProps } from '../types'; -import { getCellLinks } from '../utils'; const DATALINKS_HEIGHT_OFFSET = 10; -export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx, actions }: ImageCellProps) => { +export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx }: ImageCellProps) => { const calculatedHeight = height - DATALINKS_HEIGHT_OFFSET; const styles = useStyles2(getStyles, calculatedHeight, justifyContent); - const links = getCellLinks(field, rowIdx) || []; - - const [tooltipCoords, setTooltipCoords] = useState(); - const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions); - const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined; const { text } = field.display!(value); const { alt, title } = cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { alt: undefined, title: undefined }; const img = {alt}; + const link = useSingleLink(field, rowIdx); - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- {shouldShowLink ? ( - renderSingleLink(links[0], img) - ) : shouldShowTooltip ? ( - setTooltipCoords(undefined)} - /> - ) : ( - img - )} -
- ); + return
{link == null ? img : renderSingleLink(link, img)}
; }; const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({ diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx index 2506255cb03..dc195cbcd67 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx @@ -1,16 +1,14 @@ import { css } from '@emotion/css'; import { Property } from 'csstype'; -import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../../themes/ThemeContext'; -import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; -import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; +import { useSingleLink } from '../hooks'; import { JSONCellProps } from '../types'; -import { getCellLinks } from '../utils'; -export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSONCellProps) => { +export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps) => { const styles = useStyles2(getStyles, justifyContent); let displayValue = value; @@ -33,34 +31,9 @@ export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSON } } - const links = getCellLinks(field, rowIdx) || []; + const link = useSingleLink(field, rowIdx); - const [tooltipCoords, setTooltipCoords] = useState(); - const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions); - const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined; - - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- {shouldShowLink ? ( - renderSingleLink(links[0], displayValue) - ) : shouldShowTooltip ? ( - setTooltipCoords(undefined)} - /> - ) : ( - displayValue - )} -
- ); + return
{link == null ? displayValue : renderSingleLink(link, displayValue)}
; }; const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({ diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx new file mode 100644 index 00000000000..a216a9221d1 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx @@ -0,0 +1,219 @@ +import { render, screen } from '@testing-library/react'; + +import { DataFrame, Field, FieldType, GrafanaTheme2, MappingType, createTheme } from '@grafana/data'; +import { TableCellDisplayMode, TablePillCellOptions } from '@grafana/schema'; + +import { mockThemeContext } from '../../../../themes/ThemeContext'; + +import { PillCell, inferPills } from './PillCell'; + +describe('PillCell', () => { + let restoreThemeContext: () => void; + + beforeEach(() => { + restoreThemeContext = mockThemeContext(createTheme()); + }); + + afterEach(() => { + restoreThemeContext(); + }); + + const mockCellOptions: TablePillCellOptions = { + type: TableCellDisplayMode.Pill, + colorMode: 'auto', + }; + + const mockField: Field = { + name: 'test', + type: FieldType.string, + values: [], + config: {}, + }; + + const mockFrame: DataFrame = { + name: 'test', + fields: [mockField], + length: 1, + }; + + const defaultProps = { + value: 'test-value', + field: mockField, + justifyContent: 'flex-start' as const, + cellOptions: mockCellOptions, + rowIdx: 0, + frame: mockFrame, + height: 30, + width: 100, + theme: {} as GrafanaTheme2, + cellInspect: false, + showFilters: false, + }; + + describe('pill parsing', () => { + it('should render pills for single values', () => { + render(); + expect(screen.getByText('test-value')).toBeInTheDocument(); + }); + + it('should render pills for CSV values', () => { + render(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + }); + + it('should render pills for JSON array values', () => { + render(); + expect(screen.getByText('item1')).toBeInTheDocument(); + expect(screen.getByText('item2')).toBeInTheDocument(); + expect(screen.getByText('item3')).toBeInTheDocument(); + }); + + it('should show dash for empty values', () => { + render(); + expect(screen.getByText('-')).toBeInTheDocument(); + }); + + it('should show dash for null values', () => { + render(); + expect(screen.getByText('-')).toBeInTheDocument(); + }); + }); + + describe('color mapping', () => { + // These tests primarily ensure the color logic executes without throwing. + // For true color verification, visual regression tests would be needed. + + it('should use mapped colors when colorMode is mapped', () => { + const mappedOptions: TablePillCellOptions = { + type: TableCellDisplayMode.Pill, + colorMode: 'mapped', + }; + + render(); + + const successPill = screen.getByText('success'); + const errorPill = screen.getByText('error'); + const warningPill = screen.getByText('warning'); + const unknownPill = screen.getByText('unknown'); + + expect(successPill).toBeInTheDocument(); + expect(errorPill).toBeInTheDocument(); + expect(warningPill).toBeInTheDocument(); + expect(unknownPill).toBeInTheDocument(); + }); + + it('should use field-level value mappings when available', () => { + const mappedOptions: TablePillCellOptions = { + type: TableCellDisplayMode.Pill, + colorMode: 'mapped', + }; + + // Mock field with value mappings + const fieldWithMappings: Field = { + ...mockField, + config: { + ...mockField.config, + mappings: [ + { + type: MappingType.ValueToText, + options: { + success: { color: '#00FF00' }, + error: { color: '#FF0000' }, + warning: { color: '#FFFF00' }, + }, + }, + ], + }, + display: (value: unknown) => ({ + text: String(value), + color: + String(value) === 'success' + ? '#00FF00' + : String(value) === 'error' + ? '#FF0000' + : String(value) === 'warning' + ? '#FFFF00' + : '#FF780A', + numeric: 0, + }), + }; + + render( + + ); + + const successPill = screen.getByText('success'); + const errorPill = screen.getByText('error'); + const warningPill = screen.getByText('warning'); + const unknownPill = screen.getByText('unknown'); + + expect(successPill).toBeInTheDocument(); + expect(errorPill).toBeInTheDocument(); + expect(warningPill).toBeInTheDocument(); + expect(unknownPill).toBeInTheDocument(); + }); + + it('should use fixed color when colorMode is fixed', () => { + const fixedOptions: TablePillCellOptions = { + type: TableCellDisplayMode.Pill, + colorMode: 'fixed', + color: '#FF00FF', + }; + + render(); + expect(screen.getByText('test-value')).toBeInTheDocument(); + }); + + it('should use auto color when colorMode is auto', () => { + const autoOptions: TablePillCellOptions = { + type: TableCellDisplayMode.Pill, + colorMode: 'auto', + }; + + render(); + expect(screen.getByText('test-value')).toBeInTheDocument(); + }); + }); +}); + +describe('inferPills', () => { + // These tests verify the pill parsing logic handles various input formats correctly. + // They ensure the function can extract pill values from different data structures. + + it('should return empty array for null/undefined values', () => { + expect(inferPills(null)).toEqual([]); + expect(inferPills(undefined)).toEqual([]); + expect(inferPills('')).toEqual([]); + }); + + it('should parse single values', () => { + expect(inferPills('test')).toEqual(['test']); + expect(inferPills('"quoted"')).toEqual(['quoted']); + expect(inferPills("'quoted'")).toEqual(['quoted']); + }); + + it('should parse CSV strings', () => { + expect(inferPills('value1,value2,value3')).toEqual(['value1', 'value2', 'value3']); + expect(inferPills(' value1 , value2 , value3 ')).toEqual(['value1', 'value2', 'value3']); + expect(inferPills('value1, ,value3')).toEqual(['value1', 'value3']); + }); + + it('should parse JSON arrays', () => { + expect(inferPills('["item1","item2","item3"]')).toEqual(['item1', 'item2', 'item3']); + expect(inferPills('["item1", "item2", "item3"]')).toEqual(['item1', 'item2', 'item3']); + expect(inferPills('["item1", null, "item3"]')).toEqual(['item1', 'item3']); + }); + + it('should handle mixed content', () => { + // When JSON parsing fails, it falls back to CSV parsing + expect(inferPills('["item1", "item2"],extra')).toEqual(['["item1"', '"item2"]', 'extra']); + expect(inferPills('not-json,value')).toEqual(['not-json', 'value']); + }); +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx new file mode 100644 index 00000000000..aaa103c3dc7 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx @@ -0,0 +1,185 @@ +import { css } from '@emotion/css'; +import { Property } from 'csstype'; +import { useMemo } from 'react'; + +import { GrafanaTheme2, isDataFrame, classicColors, colorManipulator, Field } from '@grafana/data'; +import { TablePillCellOptions } from '@grafana/schema'; + +import { useStyles2 } from '../../../../themes/ThemeContext'; +import { TableCellRendererProps } from '../types'; + +const DEFAULT_PILL_BG_COLOR = '#FF780A'; + +interface Pill { + value: string; + key: string; + bgColor: string; + color: string; +} + +function createPills(pillValues: string[], cellOptions: TableCellRendererProps['cellOptions'], field: Field): Pill[] { + return pillValues.map((pill, index) => { + const bgColor = getPillColor(pill, cellOptions, field); + const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000'; + return { + value: pill, + key: `${pill}-${index}`, + bgColor, + color: textColor, + }; + }); +} + +export function PillCell({ value, field, justifyContent, cellOptions }: TableCellRendererProps) { + const styles = useStyles2(getStyles, justifyContent); + + const pills: Pill[] = useMemo(() => { + const pillValues = inferPills(value); + return createPills(pillValues, cellOptions, field); + }, [value, cellOptions, field]); + + if (pills.length === 0) { + return
-
; + } + + return ( +
+
+ {pills.map((pill) => ( + + {pill.value} + + ))} +
+
+ ); +} + +export function inferPills(value: unknown): string[] { + if (!value) { + return []; + } + + // Handle DataFrame - not supported for pills + if (isDataFrame(value)) { + return []; + } + + // Handle different value types + const stringValue = String(value); + + // Try to parse as JSON first + try { + const parsed = JSON.parse(stringValue); + if (Array.isArray(parsed)) { + // JSON array of strings + return parsed + .filter((item) => item != null && item !== '') + .map(String) + .map((text) => text.trim()) + .filter((item) => item !== ''); + } + } catch { + // Not valid JSON, continue with other parsing + } + + // Handle CSV string + if (stringValue.includes(',')) { + return stringValue + .split(',') + .map((text) => text.trim()) + .filter((item) => item !== ''); + } + + // Single value - strip quotes + return [stringValue.replace(/["'`]/g, '').trim()]; +} + +function isPillCellOptions(cellOptions: TableCellRendererProps['cellOptions']): cellOptions is TablePillCellOptions { + return cellOptions?.type === 'pill'; +} + +function getPillColor(pill: string, cellOptions: TableCellRendererProps['cellOptions'], field: Field): string { + if (!isPillCellOptions(cellOptions)) { + return getDeterministicColor(pill); + } + + const colorMode = cellOptions.colorMode || 'auto'; + + // Fixed color mode (highest priority) + if (colorMode === 'fixed' && cellOptions.color) { + return cellOptions.color; + } + + // Mapped color mode - use field's value mappings + if (colorMode === 'mapped') { + // Check if field has value mappings + if (field.config.mappings && field.config.mappings.length > 0) { + // Use the field's display processor to get the mapped value + const displayValue = field.display!(pill); + if (displayValue.color) { + return displayValue.color; + } + } + // Fallback to default color for unmapped values + return cellOptions.color || DEFAULT_PILL_BG_COLOR; + } + + // Auto mode - deterministic color assignment based on string hash + if (colorMode === 'auto') { + return getDeterministicColor(pill); + } + + // Default color for unknown values or fallback + return DEFAULT_PILL_BG_COLOR; +} + +function getDeterministicColor(text: string): string { + // Create a simple hash of the string to get consistent colors + let hash = 0; + for (let i = 0; i < text.length; i++) { + const char = text.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + + // Use absolute value and modulo to get a consistent index + const colorValues = Object.values(classicColors); + const index = Math.abs(hash) % colorValues.length; + + return colorValues[index]; +} + +const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({ + cell: css({ + display: 'flex', + justifyContent: justifyContent || 'flex-start', + alignItems: 'center', + height: '100%', + padding: theme.spacing(0.5), + }), + pillsContainer: css({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(0.5), + maxWidth: '100%', + }), + pill: css({ + display: 'inline-block', + padding: theme.spacing(0.25, 0.75), + borderRadius: theme.shape.radius.default, + fontSize: theme.typography.bodySmall.fontSize, + lineHeight: theme.typography.bodySmall.lineHeight, + fontWeight: theme.typography.fontWeightMedium, + whiteSpace: 'nowrap', + textAlign: 'center', + minWidth: 'fit-content', + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx index eb67a369cb2..ba5cc0749f1 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx @@ -24,7 +24,10 @@ export function TableCellActions(props: TableCellActionsProps) { } = props; return ( -
+ // stopping propagation to prevent clicks within the actions menu from triggering the cell click events + // for things like the data links tooltip. + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions +
ev.stopPropagation()}> {cellInspect && ( { expect(container).toBeInTheDocument(); expect(container.childNodes).toHaveLength(1); }); + + it('should return AutoCell when cellOptions is unmapped', () => { + const field = createField(FieldType.string); + + const { container } = renderCell(field, { type: 'number' } as unknown as TableCellOptions); + expect(container).toBeInTheDocument(); + expect(container.childNodes).toHaveLength(1); + }); }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx index 7bb4b39f2cb..74c773f28ff 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx @@ -12,6 +12,7 @@ import { DataLinksCell } from './DataLinksCell'; import { GeoCell } from './GeoCell'; import { ImageCell } from './ImageCell'; import { JSONCell } from './JSONCell'; +import { PillCell } from './PillCell'; import { SparklineCell } from './SparklineCell'; export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode; @@ -24,7 +25,6 @@ const GAUGE_RENDERER: TableCellRenderer = (props) => ( height={props.height} width={props.width} rowIdx={props.rowIdx} - actions={props.actions} /> ); @@ -35,7 +35,6 @@ const AUTO_RENDERER: TableCellRenderer = (props) => ( justifyContent={props.justifyContent} rowIdx={props.rowIdx} cellOptions={props.cellOptions} - actions={props.actions} /> ); @@ -52,13 +51,7 @@ const SPARKLINE_RENDERER: TableCellRenderer = (props) => ( ); const JSON_RENDERER: TableCellRenderer = (props) => ( - + ); const GEO_RENDERER: TableCellRenderer = (props) => ( @@ -73,13 +66,16 @@ const IMAGE_RENDERER: TableCellRenderer = (props) => ( justifyContent={props.justifyContent} value={props.value} rowIdx={props.rowIdx} - actions={props.actions} /> ); const DATA_LINKS_RENDERER: TableCellRenderer = (props) => ; -const ACTIONS_RENDERER: TableCellRenderer = (props) => ; +const ACTIONS_RENDERER: TableCellRenderer = ({ field, rowIdx, getActions = () => [] }) => ( + +); + +const PILL_RENDERER: TableCellRenderer = (props) => ; function isCustomCellOptions(options: TableCellOptions): options is TableCustomCellOptions { return options.type === TableCellDisplayMode.Custom; @@ -104,6 +100,7 @@ const CELL_RENDERERS: Record = { [TableCellDisplayMode.ColorText]: AUTO_RENDERER, [TableCellDisplayMode.ColorBackground]: AUTO_RENDERER, [TableCellDisplayMode.Auto]: AUTO_RENDERER, + [TableCellDisplayMode.Pill]: PILL_RENDERER, }; /** @internal */ @@ -112,7 +109,7 @@ export function getCellRenderer(field: Field, cellOptions: TableCellOptions): Ta if (cellType === TableCellDisplayMode.Auto) { return getAutoRendererResult(field); } - return CELL_RENDERERS[cellType]; + return CELL_RENDERERS[cellType] ?? AUTO_RENDERER; } /** @internal */ diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx index dd1d4364128..9dfe1a2719b 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx @@ -1,7 +1,17 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { applyFieldOverrides, createTheme, DataFrame, EventBus, FieldType, toDataFrame } from '@grafana/data'; +import { + applyFieldOverrides, + createTheme, + DataFrame, + DataLink, + EventBus, + FieldType, + LinkModel, + toDataFrame, +} from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { TableCellBackgroundDisplayMode } from '@grafana/schema'; import { PanelContext, PanelContextProvider } from '../../../components/PanelChrome'; @@ -1682,4 +1692,55 @@ describe('TableNG', () => { expect(mockEventBus.publish).not.toHaveBeenCalled(); }); }); + + describe('Displays data Links', () => { + function toLinkModel(link: DataLink): LinkModel { + return { + href: link.url, + title: link.title, + target: link.targetBlank ? '_blank' : '_self', + origin: link.origin || 'panel', + }; + } + + it('shows multiple datalinks in the tooltip', async () => { + const dataFrame = createBasicDataFrame(); + const links: DataLink[] = [ + { url: 'http://asdasd.com', title: 'Test Title' }, + { url: 'http://asdasd2.com', title: 'Test Title2' }, + ]; + + dataFrame.fields[0].config.links = links; + dataFrame.fields[0].getLinks = () => links.map(toLinkModel); + + render(); + + const cell = screen.getByText('A1'); + await userEvent.click(cell); + + const tooltip = screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper); + expect(tooltip).toBeInTheDocument(); + + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Test Title2')).toBeInTheDocument(); + }); + + it('does not show tooltip for a single link', async () => { + const dataFrame = createBasicDataFrame(); + + const links: DataLink[] = [{ url: 'http://asdasd.com', title: 'Test Title' }]; + + dataFrame.fields[0].config.links = links; + dataFrame.fields[0].getLinks = () => links.map(toLinkModel); + + render(); + + const cell = screen.getByText('A1'); + + // we need to click the parent since the cell itself is a link. + await userEvent.click(cell.parentElement!); + + expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument(); + }); + }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 5a0a2fdc036..129eb256c97 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -1,7 +1,7 @@ import 'react-data-grid/lib/styles.css'; import { css, cx } from '@emotion/css'; import { Property } from 'csstype'; -import { Key, ReactNode, useLayoutEffect, useMemo, useState } from 'react'; +import { Key, ReactNode, useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { Cell, CellRendererProps, @@ -22,8 +22,10 @@ import { ContextMenu } from '../../ContextMenu/ContextMenu'; import { MenuItem } from '../../Menu/MenuItem'; import { Pagination } from '../../Pagination/Pagination'; import { PanelContext, usePanelContext } from '../../PanelChrome'; +import { DataLinksActionsTooltip } from '../DataLinksActionsTooltip'; import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector'; import { CellColors, TableCellDisplayMode } from '../types'; +import { DataLinksActionsTooltipState } from '../utils'; import { HeaderCell } from './Cells/HeaderCell'; import { RowExpander } from './Cells/RowExpander'; @@ -56,6 +58,8 @@ import { getCellOptions, shouldTextWrap, isCellInspectEnabled, + getCellLinks, + withDataLinksActionsTooltip, } from './utils'; type CellRootRenderer = (key: React.Key, props: CellRendererProps) => React.ReactNode; @@ -68,26 +72,31 @@ export function TableNG(props: TableNGProps) { enableSharedCrosshair = false, enableVirtualization, footerOptions, - getActions, + getActions = () => [], height, initialSortBy, noHeader, onCellFilterAdded, onColumnResize, onSortByChange, - replaceVariables, showTypeIcons, structureRev, + transparent, width, } = props; const theme = useTheme2(); const styles = useStyles2(getGridStyles, { enablePagination, - noHeader, + transparent, }); const panelContext = usePanelContext(); + const getCellActions = useCallback( + (field: Field, rowIdx: number) => getActions(data, field, rowIdx), + [getActions, data] + ); + const hasHeader = !noHeader; const hasFooter = Boolean(footerOptions?.show && footerOptions.reducer?.length); const isCountRowsSet = Boolean( @@ -236,33 +245,23 @@ export function TableNG(props: TableNGProps) { }, sortColumns, rowHeight, - headerRowClass: styles.headerRow, - headerRowHeight: headerHeight, bottomSummaryRows: hasFooter ? [{}] : undefined, }) satisfies Partial>, - [ - enableVirtualization, - resizeHandler, - sortColumns, - headerHeight, - styles.headerRow, - rowHeight, - hasFooter, - setSortColumns, - onSortByChange, - ] + [enableVirtualization, resizeHandler, sortColumns, rowHeight, hasFooter, setSortColumns, onSortByChange] ); interface Schema { columns: TableColumn[]; cellRootRenderers: Record; + colsWithTooltip: Record; } - const { columns, cellRootRenderers } = useMemo(() => { + const { columns, cellRootRenderers, colsWithTooltip } = useMemo(() => { const fromFields = (f: Field[], widths: number[]) => { const result: Schema = { columns: [], cellRootRenderers: {}, + colsWithTooltip: {}, }; let lastRowIdx = -1; @@ -280,7 +279,6 @@ export function TableNG(props: TableNGProps) { const showFilters = Boolean(field.config.filterable && onCellFilterAdded != null); const showActions = cellInspect || showFilters; const width = widths[i]; - const frame = data; // helps us avoid string cx and emotion per-cell const cellActionClassName = showActions @@ -294,6 +292,9 @@ export function TableNG(props: TableNGProps) { const cellType = cellOptions.type; const shouldOverflow = shouldTextOverflow(field); const shouldWrap = shouldTextWrap(field); + const withTooltip = withDataLinksActionsTooltip(field, cellType); + + result.colsWithTooltip[displayName] = withTooltip; // this fires first const renderCellRoot = (key: Key, props: CellRendererProps): ReactNode => { @@ -317,7 +318,7 @@ export function TableNG(props: TableNGProps) { colors = {}; } - const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, colors); + const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, withTooltip, colors); return ( ): JSX.Element => { const rowIdx = props.row.__index; const value = props.row[props.column.key]; - - // TODO: defer until click? - const actions = getActions?.(frame, field, props.row.__index, replaceVariables); + const frame = data; return ( <> {renderFieldCell({ - actions, cellOptions, frame, field, @@ -354,6 +352,7 @@ export function TableNG(props: TableNGProps) { width, cellInspect, showFilters, + getActions: getCellActions, })} {showActions && ( + No data +
+ ); + } return ( {...commonDataGridProps} className={cx(styles.grid, styles.gridNested)} + headerRowClass={cx(styles.headerRow, { [styles.displayNone]: !hasNestedHeaders })} + headerRowHeight={hasNestedHeaders ? defaultHeaderHeight : 0} columns={nestedColumns} rows={expandedRecords} renderers={{ renderRow, renderCell: renderCellRoot }} @@ -499,17 +508,16 @@ export function TableNG(props: TableNGProps) { crossFilterOrder, crossFilterRows, data, + defaultHeaderHeight, defaultRowHeight, enableSharedCrosshair, expandedRows, filter, footerCalcs, - getActions, hasNestedFrames, isCountRowsSet, onCellFilterAdded, panelContext, - replaceVariables, rowHeight, rows, setFilter, @@ -519,6 +527,7 @@ export function TableNG(props: TableNGProps) { theme, visibleFields, widths, + getCellActions, ]); // invalidate columns on every structureRev change. this supports width editing in the fieldConfig. @@ -534,6 +543,8 @@ export function TableNG(props: TableNGProps) { return cellRootRenderers[props.column.key](key, props); }; + const [tooltipState, setTooltipState] = useState(); + return ( <> @@ -541,6 +552,26 @@ export function TableNG(props: TableNGProps) { className={styles.grid} columns={structureRevColumns} rows={paginatedRows} + headerRowClass={cx(styles.headerRow, { [styles.displayNone]: noHeader })} + headerRowHeight={headerHeight} + onCellClick={({ column, row }, { clientX, clientY, preventGridDefault }) => { + // Note: could be column.field; JS says yes, but TS says no! + const field = columns[column.idx].field; + + if (colsWithTooltip[getDisplayName(field)]) { + const rowIdx = row.__index; + setTooltipState({ + coords: { + clientX, + clientY, + }, + links: getCellLinks(field, rowIdx), + actions: getCellActions(field, rowIdx), + }); + + preventGridDefault(); + } + }} onCellKeyDown={ hasNestedFrames ? (_, event) => { @@ -577,6 +608,15 @@ export function TableNG(props: TableNGProps) {
)} + {tooltipState && ( + setTooltipState(undefined)} + /> + )} + {isContextMenuOpen && ( ({ grid: css({ - '--rdg-background-color': theme.colors.background.primary, - '--rdg-header-background-color': theme.colors.background.primary, - '--rdg-border-color': theme.isDark ? '#282b30' : '#ebebec', + '--rdg-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-header-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-border-color': theme.colors.border.weak, '--rdg-color': theme.colors.text.primary, // note: this cannot have any transparency since default cells that // overlay/overflow on hover inherit this background and need to occlude cells below - '--rdg-row-hover-background-color': theme.isDark ? '#212428' : '#f4f5f5', + '--rdg-row-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-row-hover-background-color': transparent + ? theme.colors.background.primary + : theme.colors.background.secondary, // TODO: magic 32px number is unfortunate. it would be better to have the content // flow using flexbox rather than hard-coding this size via a calc @@ -685,15 +728,24 @@ const getGridStyles = ( }), gridNested: css({ height: '100%', - width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - 1}px)`, + width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING * 2 - 1}px)`, overflow: 'visible', - marginLeft: COLUMN.EXPANDER_WIDTH - 1, + marginLeft: COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING - 1, + marginBlock: TABLE.CELL_PADDING, }), cellNested: css({ '&[aria-selected=true]': { outline: 'none', }, }), + noDataNested: css({ + height: TABLE.NESTED_NO_DATA_HEIGHT, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.colors.text.secondary, + fontSize: theme.typography.h4.fontSize, + }), cellActions: css({ display: 'none', position: 'absolute', @@ -701,7 +753,7 @@ const getGridStyles = ( margin: 'auto', height: '100%', color: theme.colors.text.primary, - background: theme.isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(255, 255, 255, 0.7)', + background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)', padding: theme.spacing.x0_5, paddingInlineStart: theme.spacing.x1, }), @@ -714,12 +766,14 @@ const getGridStyles = ( headerRow: css({ paddingBlockStart: 0, fontWeight: 'normal', - ...(noHeader ? { display: 'none' } : {}), '& .rdg-cell': { height: '100%', alignItems: 'flex-end', }, }), + displayNone: css({ + display: 'none', + }), paginationContainer: css({ alignItems: 'center', display: 'flex', @@ -770,31 +824,36 @@ const getCellStyles = ( rowHeight: number, shouldWrap: boolean, shouldOverflow: boolean, + hasTooltip: boolean, colors: CellColors -) => ({ - cell: css({ - textOverflow: 'initial', - background: colors.bgColor ?? 'inherit', - alignContent: 'center', - justifyContent: getTextAlign(field), - paddingInline: TABLE.CELL_PADDING, - height: '100%', - minHeight: rowHeight, // min height interacts with the fit-content property on the overflow container - ...(shouldWrap && { whiteSpace: 'pre-line' }), - '&:last-child': { - borderInlineEnd: 'none', - }, - '&:hover': { - background: colors.bgHoverColor, - '.table-cell-actions': { - display: 'flex', +) => { + return { + cell: css({ + textOverflow: 'initial', + background: colors.bgColor ?? 'inherit', + alignContent: 'center', + justifyContent: getTextAlign(field), + paddingInline: TABLE.CELL_PADDING, + height: '100%', + minHeight: rowHeight, // min height interacts with the fit-content property on the overflow container + ...(shouldWrap && { whiteSpace: 'pre-line' }), + ...(hasTooltip && { cursor: 'pointer' }), + '&:last-child': { + borderInlineEnd: 'none', }, - ...(shouldOverflow && { - zIndex: theme.zIndex.tooltip - 2, - whiteSpace: 'pre-line', - height: 'fit-content', - minWidth: 'fit-content', - }), - }, - }), -}); + '&:hover': { + background: colors.bgHoverColor, + '.table-cell-actions': { + display: 'flex', + }, + ...(shouldOverflow && { + zIndex: theme.zIndex.tooltip - 2, + whiteSpace: 'pre-line', + height: 'fit-content', + minWidth: 'fit-content', + paddingBlock: (rowHeight - TABLE.LINE_HEIGHT) / 2 - 1, + }), + }, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Table/TableNG/constants.ts b/packages/grafana-ui/src/components/Table/TableNG/constants.ts index d7d69c160c0..31a537136d2 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/constants.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/constants.ts @@ -14,5 +14,6 @@ export const TABLE = { SCROLL_BAR_WIDTH: 8, SCROLL_BAR_MARGIN: 2, LINE_HEIGHT: 22, + NESTED_NO_DATA_HEIGHT: 60, BORDER_RIGHT: 0.666667, }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts index 1540d1e0383..4ef2c8f185e 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts @@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect } fr import { Column, DataGridProps, SortColumn } from 'react-data-grid'; import { varPreLine } from 'uwrap'; -import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data'; +import { Field, fieldReducers, FieldType, formattedValueToString, LinkModel, reduceField } from '@grafana/data'; import { useTheme2 } from '../../../themes/ThemeContext'; import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types'; @@ -17,6 +17,7 @@ import { getColumnTypes, GetMaxWrapCellOptions, getMaxWrapCell, + getCellLinks, } from './utils'; // Helper function to get displayed value @@ -502,9 +503,13 @@ export function useRowHeight({ return 0; } - // Ensure we have a minimum height (defaultHeight) for the nested table even if data is empty const rowCount = row.data?.length ?? 0; - return Math.max(defaultHeight, defaultHeight * rowCount + headerHeight); + if (rowCount === 0) { + return TABLE.NESTED_NO_DATA_HEIGHT + TABLE.CELL_PADDING * 2; + } + + const nestedHeaderHeight = row.data?.meta?.custom?.noHeader ? 0 : defaultHeight; + return Math.max(defaultHeight, defaultHeight * rowCount + nestedHeaderHeight + TABLE.CELL_PADDING * 2); } // regular rows @@ -518,7 +523,6 @@ export function useRowHeight({ fields, hasNestedFrames, hasWrappedCols, - headerHeight, maxWrapCellOptions, colWidths, ]); @@ -597,3 +601,10 @@ export function useColumnResize( return dataGridResizeHandler; } + +export function useSingleLink(field: Field, rowIdx: number): LinkModel | undefined { + const linksCount = field.config.links?.length ?? 0; + const actionsCount = field.config.actions?.length ?? 0; + const shouldShowLink = linksCount === 1 && actionsCount === 0; + return useMemo(() => (shouldShowLink ? (getCellLinks(field, rowIdx) ?? []) : [])[0], [field, shouldShowLink, rowIdx]); +} diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index 2946923db8f..65d40d3c4d1 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -10,7 +10,6 @@ import { TimeRange, FieldConfigSource, ActionModel, - InterpolateFunction, FieldType, DataFrameWithValue, SelectableValue, @@ -30,12 +29,9 @@ export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void; export type FooterItem = Array> | string | undefined; -export type GetActionsFunction = ( - frame: DataFrame, - field: Field, - rowIndex: number, - replaceVariables?: InterpolateFunction -) => ActionModel[]; +export type GetActionsFunction = (frame: DataFrame, field: Field, rowIndex: number) => ActionModel[]; + +export type GetActionsFunctionLocal = (field: Field, rowIndex: number) => ActionModel[]; export type TableFieldOptionsType = Omit & { cellOptions: TableCellOptions; @@ -135,6 +131,7 @@ export interface BaseTableProps { enablePagination?: boolean; cellHeight?: TableCellHeight; structureRev?: number; + transparent?: boolean; /** @alpha Used by SparklineCell when provided */ timeRange?: TimeRange; enableSharedCrosshair?: boolean; @@ -142,7 +139,6 @@ export interface BaseTableProps { initialRowIndex?: number; fieldConfig?: FieldConfigSource; getActions?: GetActionsFunction; - replaceVariables?: InterpolateFunction; // Used solely for testing as RTL can't correctly render the table otherwise enableVirtualization?: boolean; } @@ -151,7 +147,6 @@ export interface BaseTableProps { export interface TableNGProps extends BaseTableProps {} export interface TableCellRendererProps { - actions?: ActionModel[]; rowIdx: number; frame: DataFrame; timeRange?: TimeRange; @@ -165,6 +160,7 @@ export interface TableCellRendererProps { cellInspect: boolean; showFilters: boolean; justifyContent: Property.JustifyContent; + getActions?: GetActionsFunctionLocal; } export type ContextMenuProps = { @@ -205,7 +201,7 @@ export interface SparklineCellProps { width: number; } -export interface BarGaugeCellProps extends ActionCellProps { +export interface BarGaugeCellProps { field: Field; height: number; rowIdx: number; @@ -214,7 +210,7 @@ export interface BarGaugeCellProps extends ActionCellProps { width: number; } -export interface ImageCellProps extends ActionCellProps { +export interface ImageCellProps { cellOptions: TableCellOptions; field: Field; height: number; @@ -223,7 +219,7 @@ export interface ImageCellProps extends ActionCellProps { rowIdx: number; } -export interface JSONCellProps extends ActionCellProps { +export interface JSONCellProps { justifyContent: Property.JustifyContent; value: TableCellValue; field: Field; @@ -241,24 +237,26 @@ export interface GeoCellProps { height: number; } -export interface ActionCellProps { - actions?: ActionModel[]; -} - export interface CellColors { textColor?: string; bgColor?: string; bgHoverColor?: string; } -export interface AutoCellProps extends ActionCellProps { - value: TableCellValue; +export interface AutoCellProps { field: Field; + value: TableCellValue; justifyContent: Property.JustifyContent; rowIdx: number; cellOptions: TableCellOptions; } +export interface ActionCellProps { + field: Field; + rowIdx: number; + getActions: GetActionsFunctionLocal; +} + // Comparator for sorting table values export type Comparator = (a: TableCellValue, b: TableCellValue) => number; diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index 78f5442f234..90004837874 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -172,6 +172,7 @@ const DEFAULT_CELL_OPTIONS = { type: TableCellDisplayMode.Auto } as const; /** * @internal * Returns the cell options for a field, migrating from legacy displayMode if necessary. + * TODO: remove live migration in favor of doing it in dashboard or panel migrator */ export function getCellOptions(field: Field): TableCellOptions { if (field.config.custom?.displayMode) { @@ -613,3 +614,12 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI } } } + +/** @internal */ +export function withDataLinksActionsTooltip(field: Field, cellType: TableCellDisplayMode) { + return ( + cellType !== TableCellDisplayMode.DataLinks && + cellType !== TableCellDisplayMode.Actions && + (field.config.links?.length ?? 0) + (field.config.actions?.length ?? 0) > 1 + ); +} diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index e2c5b5b8099..1d029ae7099 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -57,7 +57,7 @@ export interface TableCellProps extends CellProps { onCellFilterAdded?: TableFilterActionCallback; innerWidth: number; frame: DataFrame; - actions?: ActionModel[]; + actions?: ActionModel[]; // unused in NG setInspectCell?: TableInspectCellCallback; } diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index f4c9b072b16..77dc0cec078 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -196,6 +196,8 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field return DataLinksCell; case TableCellDisplayMode.Actions: return ActionsCell; + case TableCellDisplayMode.Pill: + return DefaultCell; // Legacy table doesn't support pill cells, fallback to default } if (field.type === FieldType.geo) { @@ -764,10 +766,16 @@ export function guessLongestField(fieldConfig: FieldConfigSource, data: DataFram return longestField; } -export type DataLinksActionsTooltipCoords = { +export interface DataLinksActionsTooltipState { + coords: DataLinksActionsTooltipCoords; + links?: LinkModel[]; + actions?: ActionModel[]; +} + +export interface DataLinksActionsTooltipCoords { clientX: number; clientY: number; -}; +} export const getDataLinksActionsTooltipUtils = (links: LinkModel[], actions?: ActionModel[]) => { const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length); diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index c9798e9a767..68bb5ba74c8 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -74,7 +74,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Resp func (hs *HTTPServer) GetUserPreferences(c *contextmodel.ReqContext) response.Response { userID, err := identity.UserIdentifier(c.GetID()) if err != nil { - return response.Error(http.StatusInternalServerError, "Failed to update user preferences", err) + return response.Error(http.StatusUnauthorized, "Not a valid identity", err) } return prefapi.GetPreferencesFor(c.Req.Context(), hs.DashboardService, hs.preferenceService, hs.Features, c.GetOrgID(), userID, 0) diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 4e849dbba3c..f98738db58d 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -36,8 +36,22 @@ const ( serviceNameForProvisioning = "provisioning" ) -func newInternalIdentity(name string, namespace string, orgID int64) Requester { - return &StaticRequester{ +type IdentityOpts func(*StaticRequester) + +// WithServiceIdentityName sets the `StaticRequester.AccessTokenClaims.Rest.ServiceIdentity` field to the provided name. +// This is so far only used by Secrets Manager to identify and gate the service decrypting a secret. +func WithServiceIdentityName(name string) IdentityOpts { + return func(r *StaticRequester) { + r.AccessTokenClaims.Rest.ServiceIdentity = name + } +} + +func newInternalIdentity(name string, namespace string, orgID int64, opts ...IdentityOpts) Requester { + // Create a copy of the ServiceIdentityClaims to avoid modifying the global one. + // Some of the options might mutate it. + claimsCopy := *ServiceIdentityClaims + + staticRequester := &StaticRequester{ Type: types.TypeAccessPolicy, Name: name, UserUID: name, @@ -50,37 +64,43 @@ func newInternalIdentity(name string, namespace string, orgID int64) Requester { Permissions: map[int64]map[string][]string{ orgID: serviceIdentityPermissions, }, - AccessTokenClaims: ServiceIdentityClaims, + AccessTokenClaims: &claimsCopy, } + + for _, opt := range opts { + opt(staticRequester) + } + + return staticRequester } // WithServiceIdentity sets an identity representing the service itself in provided org and store it in context. // This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with // static permissions so it can be used in legacy code paths. -func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) { - r := newInternalIdentity(serviceName, "*", orgID) +func WithServiceIdentity(ctx context.Context, orgID int64, opts ...IdentityOpts) (context.Context, Requester) { + r := newInternalIdentity(serviceName, "*", orgID, opts...) return WithRequester(ctx, r), r } -func WithProvisioningIdentity(ctx context.Context, namespace string) (context.Context, Requester, error) { +func WithProvisioningIdentity(ctx context.Context, namespace string, opts ...IdentityOpts) (context.Context, Requester, error) { ns, err := types.ParseNamespace(namespace) if err != nil { return nil, nil, err } - r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID) + r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID, opts...) return WithRequester(ctx, r), r, nil } // WithServiceIdentityContext sets an identity representing the service itself in context. -func WithServiceIdentityContext(ctx context.Context, orgID int64) context.Context { - ctx, _ = WithServiceIdentity(ctx, orgID) +func WithServiceIdentityContext(ctx context.Context, orgID int64, opts ...IdentityOpts) context.Context { + ctx, _ = WithServiceIdentity(ctx, orgID, opts...) return ctx } // WithServiceIdentityFN calls provided closure with an context contaning the identity of the service. -func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error)) (T, error) { - return fn(WithServiceIdentityContext(ctx, orgID)) +func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error), opts ...IdentityOpts) (T, error) { + return fn(WithServiceIdentityContext(ctx, orgID, opts...)) } func getWildcardPermissions(actions ...string) map[string][]string { @@ -91,14 +111,6 @@ func getWildcardPermissions(actions ...string) map[string][]string { return permissions } -func getTokenPermissions(groups ...string) []string { - out := make([]string, 0, len(groups)) - for _, group := range groups { - out = append(out, group+":*") - } - return out -} - // serviceIdentityPermissions is a list of wildcard permissions for provided actions. // We should add every action required "internally" here. var serviceIdentityPermissions = getWildcardPermissions( @@ -121,13 +133,16 @@ var serviceIdentityPermissions = getWildcardPermissions( "serviceaccounts:read", // serviceaccounts.ActionRead, ) -var serviceIdentityTokenPermissions = getTokenPermissions( - "folder.grafana.app", - "dashboard.grafana.app", - "secret.grafana.app", - "query.grafana.app", - "iam.grafana.app", -) +var serviceIdentityTokenPermissions = []string{ + "folder.grafana.app:*", + "dashboard.grafana.app:*", + "secret.grafana.app:*", + "query.grafana.app:*", + "iam.grafana.app:*", + + // Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions. + "secret.grafana.app/securevalues:decrypt", +} var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{ Rest: authn.AccessTokenClaims{ diff --git a/pkg/apimachinery/identity/context_test.go b/pkg/apimachinery/identity/context_test.go index 87f31a17763..10c043517d7 100644 --- a/pkg/apimachinery/identity/context_test.go +++ b/pkg/apimachinery/identity/context_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/grafana/authlib/authn" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -24,3 +25,48 @@ func TestRequesterFromContext(t *testing.T) { require.Equal(t, expected.GetUID(), actual.GetUID()) }) } + +func TestWithServiceIdentity(t *testing.T) { + t.Run("with a custom service identity name", func(t *testing.T) { + customName := "custom-service" + orgID := int64(1) + ctx, requester := identity.WithServiceIdentity(context.Background(), orgID, identity.WithServiceIdentityName(customName)) + require.NotNil(t, requester) + require.Equal(t, orgID, requester.GetOrgID()) + require.Equal(t, customName, requester.GetExtra()[string(authn.ServiceIdentityKey)][0]) + require.Contains(t, requester.GetTokenPermissions(), "secret.grafana.app/securevalues:decrypt") + + fromCtx, err := identity.GetRequester(ctx) + require.NoError(t, err) + require.Equal(t, customName, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)][0]) + + // Reuse the context but create another identity on top with a different name and org ID + anotherCustomName := "another-custom-service" + anotherOrgID := int64(2) + ctx2 := identity.WithServiceIdentityContext(ctx, anotherOrgID, identity.WithServiceIdentityName(anotherCustomName)) + + fromCtx, err = identity.GetRequester(ctx2) + require.NoError(t, err) + require.Equal(t, anotherOrgID, fromCtx.GetOrgID()) + require.Equal(t, anotherCustomName, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)][0]) + + // Reuse the context but create another identity without a custom name + ctx3, requester := identity.WithServiceIdentity(ctx2, 1) + require.NotNil(t, requester) + require.Empty(t, requester.GetExtra()[string(authn.ServiceIdentityKey)]) + + fromCtx, err = identity.GetRequester(ctx3) + require.NoError(t, err) + require.Empty(t, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)]) + }) + + t.Run("without a custom service identity name", func(t *testing.T) { + ctx, requester := identity.WithServiceIdentity(context.Background(), 1) + require.NotNil(t, requester) + require.Empty(t, requester.GetExtra()[string(authn.ServiceIdentityKey)]) + + fromCtx, err := identity.GetRequester(ctx) + require.NoError(t, err) + require.Empty(t, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)]) + }) +} diff --git a/pkg/apis/secret/go.mod b/pkg/apis/secret/go.mod index ba65bd5f9c9..81f96e3ecd2 100644 --- a/pkg/apis/secret/go.mod +++ b/pkg/apis/secret/go.mod @@ -9,88 +9,36 @@ require ( google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.33.2 - k8s.io/apiserver v0.33.2 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 ) require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.22.2 // indirect - github.com/onsi/gomega v1.36.2 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.64.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.etcd.io/bbolt v1.4.0 // indirect - go.etcd.io/etcd/api/v3 v3.5.21 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect - go.etcd.io/etcd/client/v3 v3.5.21 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - go.opentelemetry.io/proto/otlp v1.6.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.26.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.34.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/api v0.33.2 // indirect - k8s.io/client-go v0.33.2 // indirect - k8s.io/component-base v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect diff --git a/pkg/apis/secret/go.sum b/pkg/apis/secret/go.sum index e8ea9a27592..3a4d0158519 100644 --- a/pkg/apis/secret/go.sum +++ b/pkg/apis/secret/go.sum @@ -1,41 +1,11 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 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/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= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -46,156 +16,58 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= -github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= -github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= -go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= -go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= -go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= -go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= -go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= -go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA= -go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8= -go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= -go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= -go.etcd.io/etcd/pkg/v3 v3.5.21 h1:jUItxeKyrDuVuWhdh0HtjUANwyuzcb7/FAeUfABmQsk= -go.etcd.io/etcd/pkg/v3 v3.5.21/go.mod h1:wpZx8Egv1g4y+N7JAsqi2zoUiBIUWznLjqJbylDjWgU= -go.etcd.io/etcd/raft/v3 v3.5.21 h1:dOmE0mT55dIUsX77TKBLq+RgyumsQuYeiRQnW/ylugk= -go.etcd.io/etcd/raft/v3 v3.5.21/go.mod h1:fmcuY5R2SNkklU4+fKVBQi2biVp5vafMrWUEj4TJ4Cs= -go.etcd.io/etcd/server/v3 v3.5.21 h1:9w0/k12majtgarGmlMVuhwXRI2ob3/d1Ik3X5TKo0yU= -go.etcd.io/etcd/server/v3 v3.5.21/go.mod h1:G1mOzdwuzKT1VRL7SqRchli/qcFrtLBTAQ4lV20sXXo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= @@ -204,139 +76,58 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/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= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= -k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= -k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= -k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= -k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= -k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= -k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/pkg/apis/secret/v0alpha1/register.go b/pkg/apis/secret/v0alpha1/register.go index f31ab882212..87e99392e91 100644 --- a/pkg/apis/secret/v0alpha1/register.go +++ b/pkg/apis/secret/v0alpha1/register.go @@ -4,10 +4,8 @@ import ( "fmt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/generic" "github.com/grafana/grafana/pkg/apimachinery/utils" ) @@ -33,7 +31,6 @@ var SecureValuesResourceInfo = utils.NewResourceInfo( {Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"}, {Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"}, {Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"}, - {Name: "Status", Type: "string", Format: "string", Description: "The status of the secure value"}, }, // Decodes the object into a concrete type. Return order in the slice must be the same as in `Definition`. Reader: func(obj any) ([]interface{}, error) { @@ -44,7 +41,6 @@ var SecureValuesResourceInfo = utils.NewResourceInfo( r.Spec.Description, r.Spec.Keeper, r.Spec.Ref, - r.Status.Phase, }, nil } @@ -92,13 +88,6 @@ var ( AddToScheme = localSchemeBuilder.AddToScheme ) -// Adds the status phase to the selectable fields, besides the generic metadata name and namespace. -func SelectableSecureValueFields(obj *SecureValue) fields.Set { - return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{ - "status.phase": string(obj.Status.Phase), - }) -} - // Adds the list of known types to the given scheme. func AddKnownTypes(scheme *runtime.Scheme, version string) error { // TODO: do we need a type for the secure value decrypt? @@ -112,20 +101,5 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) error { // &secretV0.SecureValueActivityList{}, ) - err := scheme.AddFieldLabelConversionFunc( - SecureValuesResourceInfo.GroupVersionKind(), - func(label, value string) (string, string, error) { - fieldSet := SelectableSecureValueFields(&SecureValue{}) - for key := range fieldSet { - if label == key { - return label, value, nil - } - } - return "", "", fmt.Errorf("field label not supported for %s: %s", SecureValuesResourceInfo.GroupVersionKind(), label) - }, - ) - if err != nil { - return err - } return nil } diff --git a/pkg/apis/secret/v0alpha1/secure_value.go b/pkg/apis/secret/v0alpha1/secure_value.go index 26a68263568..7208683cd0a 100644 --- a/pkg/apis/secret/v0alpha1/secure_value.go +++ b/pkg/apis/secret/v0alpha1/secure_value.go @@ -11,41 +11,18 @@ type SecureValue struct { // Standard object's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` + metav1.ObjectMeta `json:"metadata"` // This is the actual secure value schema. Spec SecureValueSpec `json:"spec"` // Read-only observed status of the `SecureValue`. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status - Status SecureValueStatus `json:"status,omitempty"` + Status SecureValueStatus `json:"status"` } -// +enum -type SecureValuePhase string - -const ( - // When the `SecureValue` is created, it will start in `Pending` phase to create the underlying secret asynchronously. - SecureValuePhasePending SecureValuePhase = "Pending" - - // If the creation of the secret is successful, it will move to the `Succeeded` phase. - SecureValuePhaseSucceeded SecureValuePhase = "Succeeded" - - // If the creation of the secret fails, it will move to the `Failed` phase. - // Check the additional `status` fields for more information on what caused the failure. - // This state is unrecoverable. - SecureValuePhaseFailed SecureValuePhase = "Failed" -) - type SecureValueStatus struct { - // High-level summary of where the `SecureValue` is in its lifecycle. - // One of: `Pending`, `Succeeded` or `Failed`. - Phase SecureValuePhase `json:"phase"` - - // A human readable message indicating details about why the `SecureValue` is in this phase. - // Only applicable if the `phase=Failed`. - // +optional - Message string `json:"message,omitempty"` + Version int64 `json:"version"` // +optional ExternalID string `json:"externalId,omitempty"` @@ -94,7 +71,7 @@ type SecureValueList struct { // Standard list's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata // +optional - metav1.ListMeta `json:"metadata,omitempty"` + metav1.ListMeta `json:"metadata"` // Slice containing all secure values. This will NOT output decrypted values. Items []SecureValue `json:"items"` diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go b/pkg/apis/secret/v0alpha1/zz_generated.openapi.go index de1934c36cb..836d33f83fa 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/secret/v0alpha1/zz_generated.openapi.go @@ -554,7 +554,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValue(ref common.ReferenceCallback) c }, }, }, - Required: []string{"spec"}, + Required: []string{"spec", "status"}, }, }, Dependencies: []string{ @@ -690,20 +690,11 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueStatus(ref common.ReferenceCallb SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "phase": { + "version": { SchemaProps: spec.SchemaProps{ - Description: "High-level summary of where the `SecureValue` is in its lifecycle. One of: `Pending`, `Succeeded` or `Failed`.\n\nPossible enum values:\n - `\"Failed\"` If the creation of the secret fails, it will move to the `Failed` phase. Check the additional `status` fields for more information on what caused the failure. This state is unrecoverable.\n - `\"Pending\"` When the `SecureValue` is created, it will start in `Pending` phase to create the underlying secret asynchronously.\n - `\"Succeeded\"` If the creation of the secret is successful, it will move to the `Succeeded` phase.", - Default: "", - Type: []string{"string"}, - Format: "", - Enum: []interface{}{"Failed", "Pending", "Succeeded"}, - }, - }, - "message": { - SchemaProps: spec.SchemaProps{ - Description: "A human readable message indicating details about why the `SecureValue` is in this phase. Only applicable if the `phase=Failed`.", - Type: []string{"string"}, - Format: "", + Default: 0, + Type: []string{"integer"}, + Format: "int64", }, }, "externalId": { @@ -713,7 +704,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueStatus(ref common.ReferenceCallb }, }, }, - Required: []string{"phase"}, + Required: []string{"version"}, }, }, } diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list index 679e6938ae0..0487679a6a6 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -6,3 +6,4 @@ API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alp API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,Azure API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,HashiCorp API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID +API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueList,ListMeta diff --git a/pkg/infra/filestorage/file_storage_mock.go b/pkg/infra/filestorage/file_storage_mock.go index daa8eeaaec2..89918dd159d 100644 --- a/pkg/infra/filestorage/file_storage_mock.go +++ b/pkg/infra/filestorage/file_storage_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.10.6. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package filestorage @@ -17,6 +17,10 @@ type MockFileStorage struct { func (_m *MockFileStorage) CreateFolder(ctx context.Context, path string) error { ret := _m.Called(ctx, path) + if len(ret) == 0 { + panic("no return value specified for CreateFolder") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, path) @@ -31,6 +35,10 @@ func (_m *MockFileStorage) CreateFolder(ctx context.Context, path string) error func (_m *MockFileStorage) Delete(ctx context.Context, path string) error { ret := _m.Called(ctx, path) + if len(ret) == 0 { + panic("no return value specified for Delete") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, path) @@ -45,6 +53,10 @@ func (_m *MockFileStorage) Delete(ctx context.Context, path string) error { func (_m *MockFileStorage) DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error { ret := _m.Called(ctx, path, options) + if len(ret) == 0 { + panic("no return value specified for DeleteFolder") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, *DeleteFolderOptions) error); ok { r0 = rf(ctx, path, options) @@ -59,7 +71,16 @@ func (_m *MockFileStorage) DeleteFolder(ctx context.Context, path string, option func (_m *MockFileStorage) Get(ctx context.Context, path string, options *GetFileOptions) (*File, bool, error) { ret := _m.Called(ctx, path, options) + if len(ret) == 0 { + panic("no return value specified for Get") + } + var r0 *File + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, *GetFileOptions) (*File, bool, error)); ok { + return rf(ctx, path, options) + } if rf, ok := ret.Get(0).(func(context.Context, string, *GetFileOptions) *File); ok { r0 = rf(ctx, path, options) } else { @@ -68,14 +89,12 @@ func (_m *MockFileStorage) Get(ctx context.Context, path string, options *GetFil } } - var r1 bool if rf, ok := ret.Get(1).(func(context.Context, string, *GetFileOptions) bool); ok { r1 = rf(ctx, path, options) } else { r1 = ret.Get(1).(bool) } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, string, *GetFileOptions) error); ok { r2 = rf(ctx, path, options) } else { @@ -89,7 +108,15 @@ func (_m *MockFileStorage) Get(ctx context.Context, path string, options *GetFil func (_m *MockFileStorage) List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error) { ret := _m.Called(ctx, folderPath, paging, options) + if len(ret) == 0 { + panic("no return value specified for List") + } + var r0 *ListResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *Paging, *ListOptions) (*ListResponse, error)); ok { + return rf(ctx, folderPath, paging, options) + } if rf, ok := ret.Get(0).(func(context.Context, string, *Paging, *ListOptions) *ListResponse); ok { r0 = rf(ctx, folderPath, paging, options) } else { @@ -98,7 +125,6 @@ func (_m *MockFileStorage) List(ctx context.Context, folderPath string, paging * } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string, *Paging, *ListOptions) error); ok { r1 = rf(ctx, folderPath, paging, options) } else { @@ -112,6 +138,10 @@ func (_m *MockFileStorage) List(ctx context.Context, folderPath string, paging * func (_m *MockFileStorage) Upsert(ctx context.Context, command *UpsertFileCommand) error { ret := _m.Called(ctx, command) + if len(ret) == 0 { + panic("no return value specified for Upsert") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *UpsertFileCommand) error); ok { r0 = rf(ctx, command) @@ -122,10 +152,14 @@ func (_m *MockFileStorage) Upsert(ctx context.Context, command *UpsertFileComman return r0 } -// close provides a mock function with given fields: +// close provides a mock function with no fields func (_m *MockFileStorage) close() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for close") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -135,3 +169,17 @@ func (_m *MockFileStorage) close() error { return r0 } + +// NewMockFileStorage creates a new instance of MockFileStorage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockFileStorage(t interface { + mock.TestingT + Cleanup(func()) +}) *MockFileStorage { + mock := &MockFileStorage{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/login/social/socialimpl/service_test.go b/pkg/login/social/socialimpl/service_test.go index 870c13cf8e3..74760f0a3b5 100644 --- a/pkg/login/social/socialimpl/service_test.go +++ b/pkg/login/social/socialimpl/service_test.go @@ -36,7 +36,7 @@ func TestIntegrationSocialService_ProvideService(t *testing.T) { expectedGenericOAuthSkipOrgRoleSync bool }{ { - name: "should load all social connectors when ssoSettingsApi is enabled", + name: "should load all social connectors", expectedSocialMapLength: 7, expectedGenericOAuthSkipOrgRoleSync: false, }, diff --git a/pkg/login/social/socialtest/social_connector_mock.go b/pkg/login/social/socialtest/social_connector_mock.go index 17ed902bd01..4150fcb5301 100644 --- a/pkg/login/social/socialtest/social_connector_mock.go +++ b/pkg/login/social/socialtest/social_connector_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.27.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package socialtest @@ -31,6 +31,10 @@ func (_m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCode _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for AuthCodeURL") + } + var r0 string if rf, ok := ret.Get(0).(func(string, ...oauth2.AuthCodeOption) string); ok { r0 = rf(state, opts...) @@ -45,6 +49,10 @@ func (_m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCode func (_m *MockSocialConnector) Client(ctx context.Context, t *oauth2.Token) *http.Client { ret := _m.Called(ctx, t) + if len(ret) == 0 { + panic("no return value specified for Client") + } + var r0 *http.Client if rf, ok := ret.Get(0).(func(context.Context, *oauth2.Token) *http.Client); ok { r0 = rf(ctx, t) @@ -68,6 +76,10 @@ func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOp _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for Exchange") + } + var r0 *oauth2.Token var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)); ok { @@ -90,10 +102,14 @@ func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOp return r0, r1 } -// GetOAuthInfo provides a mock function with given fields: +// GetOAuthInfo provides a mock function with no fields func (_m *MockSocialConnector) GetOAuthInfo() *social.OAuthInfo { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for GetOAuthInfo") + } + var r0 *social.OAuthInfo if rf, ok := ret.Get(0).(func() *social.OAuthInfo); ok { r0 = rf() @@ -110,6 +126,10 @@ func (_m *MockSocialConnector) GetOAuthInfo() *social.OAuthInfo { func (_m *MockSocialConnector) IsEmailAllowed(email string) bool { ret := _m.Called(email) + if len(ret) == 0 { + panic("no return value specified for IsEmailAllowed") + } + var r0 bool if rf, ok := ret.Get(0).(func(string) bool); ok { r0 = rf(email) @@ -120,10 +140,14 @@ func (_m *MockSocialConnector) IsEmailAllowed(email string) bool { return r0 } -// IsSignupAllowed provides a mock function with given fields: +// IsSignupAllowed provides a mock function with no fields func (_m *MockSocialConnector) IsSignupAllowed() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for IsSignupAllowed") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() @@ -138,6 +162,10 @@ func (_m *MockSocialConnector) IsSignupAllowed() bool { func (_m *MockSocialConnector) SupportBundleContent(_a0 *bytes.Buffer) error { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for SupportBundleContent") + } + var r0 error if rf, ok := ret.Get(0).(func(*bytes.Buffer) error); ok { r0 = rf(_a0) @@ -152,6 +180,10 @@ func (_m *MockSocialConnector) SupportBundleContent(_a0 *bytes.Buffer) error { func (_m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource { ret := _m.Called(ctx, t) + if len(ret) == 0 { + panic("no return value specified for TokenSource") + } + var r0 oauth2.TokenSource if rf, ok := ret.Get(0).(func(context.Context, *oauth2.Token) oauth2.TokenSource); ok { r0 = rf(ctx, t) @@ -168,6 +200,10 @@ func (_m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) func (_m *MockSocialConnector) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { ret := _m.Called(ctx, client, token) + if len(ret) == 0 { + panic("no return value specified for UserInfo") + } + var r0 *social.BasicUserInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context, *http.Client, *oauth2.Token) (*social.BasicUserInfo, error)); ok { @@ -190,13 +226,12 @@ func (_m *MockSocialConnector) UserInfo(ctx context.Context, client *http.Client return r0, r1 } -type mockConstructorTestingTNewMockSocialConnector interface { +// NewMockSocialConnector creates a new instance of MockSocialConnector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockSocialConnector(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockSocialConnector creates a new instance of MockSocialConnector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockSocialConnector(t mockConstructorTestingTNewMockSocialConnector) *MockSocialConnector { +}) *MockSocialConnector { mock := &MockSocialConnector{} mock.Mock.Test(t) diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go index b15f0dd867b..7802040999c 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -46,10 +46,6 @@ func DefaultService(cfg *config.PluginManagementCfg) *Service { // Base returns the base path for the specified plugin. func (s *Service) Base(n PluginInfo) (string, error) { - if n.class == plugins.ClassCore { - baseDir := getBaseDir(n.fs.Base()) - return path.Join("public/app/plugins", string(n.pluginJSON.Type), baseDir), nil - } if n.class == plugins.ClassCDN { return n.fs.Base(), nil } @@ -64,7 +60,6 @@ func (s *Service) Base(n PluginInfo) (string, error) { if s.cdn.PluginSupported(n.parent.pluginJSON.ID) { return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, relPath) } - return path.Join("public/plugins", n.parent.pluginJSON.ID, relPath), nil } return path.Join("public/plugins", n.pluginJSON.ID), nil @@ -73,32 +68,12 @@ func (s *Service) Base(n PluginInfo) (string, error) { // Module returns the module.js path for the specified plugin. func (s *Service) Module(n PluginInfo) (string, error) { if n.class == plugins.ClassCore { - if filepath.Base(n.fs.Base()) == "dist" { - // The core plugin has been built externally, use the module from the dist folder - } else { - baseDir := getBaseDir(n.fs.Base()) - return path.Join("core:plugin", baseDir), nil + if filepath.Base(n.fs.Base()) != "dist" { + return path.Join("core:plugin", filepath.Base(n.fs.Base())), nil } } - if n.class == plugins.ClassCDN { - return pluginscdn.JoinPath(n.fs.Base(), "module.js") - } - if s.cdn.PluginSupported(n.pluginJSON.ID) { - return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "module.js") - } - if n.parent != nil { - relPath, err := n.parent.fs.Rel(n.fs.Base()) - if err != nil { - return "", err - } - if s.cdn.PluginSupported(n.parent.pluginJSON.ID) { - return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, path.Join(relPath, "module.js")) - } - return path.Join("public/plugins", n.parent.pluginJSON.ID, relPath, "module.js"), nil - } - - return path.Join("public/plugins", n.pluginJSON.ID, "module.js"), nil + return s.RelativeURL(n, "module.js") } // RelativeURL returns the relative URL for an arbitrary plugin asset. @@ -145,15 +120,6 @@ func (s *Service) DefaultLogoPath(pluginType plugins.Type) string { return path.Join("public/img", fmt.Sprintf("icn-%s.svg", string(pluginType))) } -func getBaseDir(pluginDir string) string { - baseDir := filepath.Base(pluginDir) - // Decoupled core plugins will be suffixed with "dist" if they have been built - if baseDir == "dist" { - return filepath.Base(strings.TrimSuffix(pluginDir, baseDir)) - } - return baseDir -} - func (s *Service) GetTranslations(n PluginInfo) (map[string]string, error) { pathToTranslations, err := s.RelativeURL(n, "locales") if err != nil { diff --git a/pkg/plugins/manager/loader/assetpath/assetpath_test.go b/pkg/plugins/manager/loader/assetpath/assetpath_test.go index e9cf4638298..424060471a5 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath_test.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath_test.go @@ -78,7 +78,7 @@ func TestService(t *testing.T) { base, err = svc.Base(NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS, nil)) require.NoError(t, err) - require.Equal(t, "public/app/plugins/table-old", base) + require.Equal(t, "public/plugins/table-old", base) parentFS := pluginFS(oneCDNURL) parentFS.RelFunc = func(_ string) (string, error) { @@ -243,9 +243,9 @@ func TestService_ChildPlugins(t *testing.T) { return childInfo }, expected: expected{ - module: "public/plugins/parent/child/module.js", - baseURL: "public/plugins/parent/child", - relURL: "public/plugins/parent/child/path/to/file.txt", + module: "public/plugins/child/module.js", + baseURL: "public/plugins/child", + relURL: "public/plugins/child/path/to/file.txt", }, }, { @@ -256,8 +256,8 @@ func TestService_ChildPlugins(t *testing.T) { }, expected: expected{ module: "core:plugin/parent", - baseURL: "public/app/plugins/parent", - relURL: "public/app/plugins/parent/path/to/file.txt", + baseURL: "public/plugins/parent", + relURL: "public/plugins/parent/path/to/file.txt", }, }, { @@ -268,8 +268,8 @@ func TestService_ChildPlugins(t *testing.T) { }, expected: expected{ module: "public/plugins/parent/module.js", - baseURL: "public/app/plugins/parent", - relURL: "public/app/plugins/parent/path/to/file.txt", + baseURL: "public/plugins/parent", + relURL: "public/plugins/parent/path/to/file.txt", }, }, { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index c51b200074c..dcd9a536baa 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -85,8 +85,8 @@ func TestLoader_Load(t *testing.T) { Description: "Data source for Amazon AWS monitoring service", Keywords: []string{"aws", "amazon"}, Logos: plugins.Logos{ - Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Small: "public/plugins/cloudwatch/img/amazon-web-services.png", + Large: "public/plugins/cloudwatch/img/amazon-web-services.png", }, Links: []plugins.InfoLink{ {Name: "Raise issue", URL: "https://github.com/grafana/grafana/issues/new"}, @@ -123,7 +123,7 @@ func TestLoader_Load(t *testing.T) { QueryOptions: map[string]bool{"minInterval": true}, }, Module: "core:plugin/cloudwatch", - BaseURL: "public/app/plugins/datasource/cloudwatch", + BaseURL: "public/plugins/cloudwatch", FS: mustNewStaticFSForTests(t, filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")), Signature: plugins.SignatureStatusInternal, Class: plugins.ClassCore, diff --git a/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go b/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go index 18796640c96..bb7231e86f6 100644 --- a/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go +++ b/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go @@ -1,12 +1,11 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package legacy import ( context "context" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" mock "github.com/stretchr/testify/mock" ) diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index bf743893816..e0f748fa54b 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -57,7 +57,7 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) }, Spec: folders.FolderSpec{ Title: v.Title, - Description: descr(v.Description), + Description: &v.Description, }, } @@ -94,10 +94,3 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) f.UID = gapiutil.CalculateClusterWideUID(f) return f, nil } - -func descr(str string) *string { - if str == "" { - return nil - } - return &str -} diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index d3b21177afc..8407cf348cb 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -236,7 +236,7 @@ func (b *FolderAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, _ if !ok { return fmt.Errorf("obj is not folders.Folder") } - f.Spec.Title = strings.Trim(f.Spec.Title, "") + f.Spec.Title = strings.Trim(f.Spec.Title, " ") return nil } return nil diff --git a/pkg/registry/apis/folders/register_test.go b/pkg/registry/apis/folders/register_test.go index d693bd53106..f4ab009ec10 100644 --- a/pkg/registry/apis/folders/register_test.go +++ b/pkg/registry/apis/folders/register_test.go @@ -479,7 +479,7 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := b.Validate(context.Background(), admission.NewAttributesRecord( + admAttr := admission.NewAttributesRecord( tt.input, nil, folders.SchemeGroupVersion.WithKind("folder"), @@ -491,13 +491,18 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { nil, true, &user.SignedInUser{}, - ), nil) + ) + err := b.Validate(context.Background(), admAttr, nil) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) + + err = b.Mutate(context.Background(), admAttr, nil) + require.NoError(t, err) + require.Equal(t, tt.input, tt.expected) }) } } @@ -585,7 +590,7 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := b.Validate(context.Background(), admission.NewAttributesRecord( + admAttr := admission.NewAttributesRecord( tt.input, existingObj, folders.SchemeGroupVersion.WithKind("folder"), @@ -597,13 +602,18 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { nil, true, &user.SignedInUser{}, - ), nil) + ) + err := b.Validate(context.Background(), admAttr, nil) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) + + err = b.Mutate(context.Background(), admAttr, nil) + require.NoError(t, err) + require.Equal(t, tt.input, tt.expected) }) } } diff --git a/pkg/registry/apis/provisioning/files.go b/pkg/registry/apis/provisioning/files.go index 619bad795fe..a0548e1055b 100644 --- a/pkg/registry/apis/provisioning/files.go +++ b/pkg/registry/apis/provisioning/files.go @@ -128,12 +128,6 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime. return } - // TODO: Implement folder delete - if r.Method == http.MethodDelete && isDir { - responder.Error(apierrors.NewBadRequest("folder navigation not yet supported")) - return - } - var obj *provisioning.ResourceWrapper code := http.StatusOK switch r.Method { diff --git a/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_clone_fn.go b/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_clone_fn.go deleted file mode 100644 index bc690475577..00000000000 --- a/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_clone_fn.go +++ /dev/null @@ -1,87 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package export - -import ( - context "context" - - repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - mock "github.com/stretchr/testify/mock" -) - -// MockWrapWithCloneFn is an autogenerated mock type for the WrapWithCloneFn type -type MockWrapWithCloneFn struct { - mock.Mock -} - -type MockWrapWithCloneFn_Expecter struct { - mock *mock.Mock -} - -func (_m *MockWrapWithCloneFn) EXPECT() *MockWrapWithCloneFn_Expecter { - return &MockWrapWithCloneFn_Expecter{mock: &_m.Mock} -} - -// Execute provides a mock function with given fields: ctx, repo, cloneOptions, pushOptions, fn -func (_m *MockWrapWithCloneFn) Execute(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error) error { - ret := _m.Called(ctx, repo, cloneOptions, pushOptions, fn) - - if len(ret) == 0 { - panic("no return value specified for Execute") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error); ok { - r0 = rf(ctx, repo, cloneOptions, pushOptions, fn) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockWrapWithCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' -type MockWrapWithCloneFn_Execute_Call struct { - *mock.Call -} - -// Execute is a helper method to define mock.On call -// - ctx context.Context -// - repo repository.Repository -// - cloneOptions repository.CloneOptions -// - pushOptions repository.PushOptions -// - fn func(repository.Repository , bool) error -func (_e *MockWrapWithCloneFn_Expecter) Execute(ctx interface{}, repo interface{}, cloneOptions interface{}, pushOptions interface{}, fn interface{}) *MockWrapWithCloneFn_Execute_Call { - return &MockWrapWithCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, cloneOptions, pushOptions, fn)} -} - -func (_c *MockWrapWithCloneFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error)) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.CloneOptions), args[3].(repository.PushOptions), args[4].(func(repository.Repository, bool) error)) - }) - return _c -} - -func (_c *MockWrapWithCloneFn_Execute_Call) Return(_a0 error) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockWrapWithCloneFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Return(run) - return _c -} - -// NewMockWrapWithCloneFn creates a new instance of MockWrapWithCloneFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockWrapWithCloneFn(t interface { - mock.TestingT - Cleanup(func()) -}) *MockWrapWithCloneFn { - mock := &MockWrapWithCloneFn{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go b/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go new file mode 100644 index 00000000000..eef200f8693 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go @@ -0,0 +1,86 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package export + +import ( + context "context" + + repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + mock "github.com/stretchr/testify/mock" +) + +// MockWrapWithStageFn is an autogenerated mock type for the WrapWithStageFn type +type MockWrapWithStageFn struct { + mock.Mock +} + +type MockWrapWithStageFn_Expecter struct { + mock *mock.Mock +} + +func (_m *MockWrapWithStageFn) EXPECT() *MockWrapWithStageFn_Expecter { + return &MockWrapWithStageFn_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: ctx, repo, stageOptions, fn +func (_m *MockWrapWithStageFn) Execute(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + ret := _m.Called(ctx, repo, stageOptions, fn) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.StageOptions, func(repository.Repository, bool) error) error); ok { + r0 = rf(ctx, repo, stageOptions, fn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockWrapWithStageFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type MockWrapWithStageFn_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ctx context.Context +// - repo repository.Repository +// - stageOptions repository.StageOptions +// - fn func(repository.Repository , bool) error +func (_e *MockWrapWithStageFn_Expecter) Execute(ctx interface{}, repo interface{}, stageOptions interface{}, fn interface{}) *MockWrapWithStageFn_Execute_Call { + return &MockWrapWithStageFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, stageOptions, fn)} +} + +func (_c *MockWrapWithStageFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error)) *MockWrapWithStageFn_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.StageOptions), args[3].(func(repository.Repository, bool) error)) + }) + return _c +} + +func (_c *MockWrapWithStageFn_Execute_Call) Return(_a0 error) *MockWrapWithStageFn_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWrapWithStageFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.StageOptions, func(repository.Repository, bool) error) error) *MockWrapWithStageFn_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewMockWrapWithStageFn creates a new instance of MockWrapWithStageFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockWrapWithStageFn(t interface { + mock.TestingT + Cleanup(func()) +}) *MockWrapWithStageFn { + mock := &MockWrapWithStageFn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/jobs/export/resources.go b/pkg/registry/apis/provisioning/jobs/export/resources.go index c469f4def8c..e02b7d7bfad 100644 --- a/pkg/registry/apis/provisioning/jobs/export/resources.go +++ b/pkg/registry/apis/provisioning/jobs/export/resources.go @@ -67,7 +67,7 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions, } } - if err := exportResource(ctx, options, client, shim, repositoryResources, progress); err != nil { + if err := exportResource(ctx, kind.Resource, options, client, shim, repositoryResources, progress); err != nil { return fmt.Errorf("export %s: %w", kind.Resource, err) } } @@ -76,6 +76,7 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions, } func exportResource(ctx context.Context, + resource string, options provisioning.ExportJobOptions, client dynamic.ResourceInterface, shim conversionShim, @@ -88,7 +89,7 @@ func exportResource(ctx context.Context, gvk := item.GroupVersionKind() result := jobs.JobResourceResult{ Name: item.GetName(), - Resource: gvk.Kind, + Resource: resource, Group: gvk.Group, Action: repository.FileActionCreated, } diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go index ca4df587476..5ded8555257 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker.go @@ -9,34 +9,33 @@ import ( provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" ) //go:generate mockery --name ExportFn --structname MockExportFn --inpackage --filename mock_export_fn.go --with-expecter type ExportFn func(ctx context.Context, repoName string, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error -//go:generate mockery --name WrapWithCloneFn --structname MockWrapWithCloneFn --inpackage --filename mock_wrap_with_clone_fn.go --with-expecter -type WrapWithCloneFn func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repo repository.Repository, cloned bool) error) error +//go:generate mockery --name WrapWithStageFn --structname MockWrapWithStageFn --inpackage --filename mock_wrap_with_stage_fn.go --with-expecter +type WrapWithStageFn func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repo repository.Repository, staged bool) error) error type ExportWorker struct { clientFactory resources.ClientFactory repositoryResources resources.RepositoryResourcesFactory exportFn ExportFn - wrapWithCloneFn WrapWithCloneFn + wrapWithStageFn WrapWithStageFn } func NewExportWorker( clientFactory resources.ClientFactory, repositoryResources resources.RepositoryResourcesFactory, exportFn ExportFn, - wrapWithCloneFn WrapWithCloneFn, + wrapWithStageFn WrapWithStageFn, ) *ExportWorker { return &ExportWorker{ clientFactory: clientFactory, repositoryResources: repositoryResources, exportFn: exportFn, - wrapWithCloneFn: wrapWithCloneFn, + wrapWithStageFn: wrapWithStageFn, } } @@ -57,32 +56,9 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, return err } - writer := gogit.Progress(func(line string) { - progress.SetMessage(ctx, line) - }, "finished") - - cloneOptions := repository.CloneOptions{ + cloneOptions := repository.StageOptions{ Timeout: 10 * time.Minute, PushOnWrites: false, - Progress: writer, - BeforeFn: func() error { - progress.SetMessage(ctx, "clone target") - // :( the branch is now baked into the repo - if options.Branch != "" { - return fmt.Errorf("branch is not supported for clonable repositories") - } - - return nil - }, - } - - pushOptions := repository.PushOptions{ - Timeout: 10 * time.Minute, - Progress: writer, - BeforeFn: func() error { - progress.SetMessage(ctx, "push changes") - return nil - }, } fn := func(repo repository.Repository, _ bool) error { @@ -104,5 +80,5 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, return r.exportFn(ctx, cfg.Name, *options, clients, repositoryResources, progress) } - return r.wrapWithCloneFn(ctx, repo, cloneOptions, pushOptions, fn) + return r.wrapWithStageFn(ctx, repo, cloneOptions, fn) } diff --git a/pkg/registry/apis/provisioning/jobs/export/worker_test.go b/pkg/registry/apis/provisioning/jobs/export/worker_test.go index abbf2b34143..aa5f2ea1c5e 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -143,12 +142,12 @@ func TestExportWorker_ProcessFailedToCreateClients(t *testing.T) { mockClients := resources.NewMockClientFactory(t) mockClients.On("Clients", context.Background(), "test-namespace").Return(nil, errors.New("failed to create clients")) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, nil, nil, mockCloneFn.Execute) + r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute) mockProgress := jobs.NewMockJobProgressRecorder(t) err := r.Process(context.Background(), mockRepo, job, mockProgress) @@ -179,12 +178,12 @@ func TestExportWorker_ProcessNotReaderWriter(t *testing.T) { mockClients.On("Clients", context.Background(), "test-namespace").Return(resourceClients, nil) mockProgress := jobs.NewMockJobProgressRecorder(t) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, nil, nil, mockCloneFn.Execute) + r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export job submitted targeting repository that is not a ReaderWriter") } @@ -216,16 +215,16 @@ func TestExportWorker_ProcessRepositoryResourcesError(t *testing.T) { mockRepoResources.On("Client", context.Background(), mockRepo).Return(nil, fmt.Errorf("failed to create repository resources client")) mockProgress := jobs.NewMockJobProgressRecorder(t) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, nil, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, nil, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "create repository resource client: failed to create repository resources client") } -func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) { +func TestExportWorker_ProcessStageOptions(t *testing.T) { job := v0alpha1.Job{ Spec: v0alpha1.JobSpec{ Action: v0alpha1.JobActionPush, @@ -245,9 +244,7 @@ func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) - // Verify progress messages are set - mockProgress.On("SetMessage", mock.Anything, "clone target").Return() - mockProgress.On("SetMessage", mock.Anything, "push changes").Return() + // No progress messages expected in current implementation mockClients := resources.NewMockClientFactory(t) mockResourceClients := resources.NewMockResourceClients(t) @@ -260,21 +257,15 @@ func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) { mockExportFn := NewMockExportFn(t) mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockCloneFn := NewMockWrapWithCloneFn(t) + mockStageFn := NewMockWrapWithStageFn(t) // Verify clone and push options - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.CloneOptions) bool { - return opts.Timeout == 10*time.Minute && !opts.PushOnWrites && opts.BeforeFn != nil - }), mock.MatchedBy(func(opts repository.PushOptions) bool { - return opts.Timeout == 10*time.Minute && opts.Progress != nil && opts.BeforeFn != nil - }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { - // Execute both BeforeFn functions to verify progress messages - assert.NoError(t, cloneOpts.BeforeFn()) - assert.NoError(t, pushOpts.BeforeFn()) - + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { + return opts.Timeout == 10*time.Minute && !opts.PushOnWrites + }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -310,17 +301,17 @@ func TestExportWorker_ProcessExportFnError(t *testing.T) { mockExportFn := NewMockExportFn(t) mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("export failed")) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export failed") } -func TestExportWorker_ProcessWrapWithCloneFnError(t *testing.T) { +func TestExportWorker_ProcessWrapWithStageFnError(t *testing.T) { job := v0alpha1.Job{ Spec: v0alpha1.JobSpec{ Action: v0alpha1.JobActionPush, @@ -340,15 +331,15 @@ func TestExportWorker_ProcessWrapWithCloneFnError(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("clone failed")) + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(errors.New("stage failed")) - r := NewExportWorker(nil, nil, nil, mockCloneFn.Execute) + r := NewExportWorker(nil, nil, nil, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) - require.EqualError(t, err, "clone failed") + require.EqualError(t, err, "stage failed") } -func TestExportWorker_ProcessBranchNotAllowedForClonableRepositories(t *testing.T) { +func TestExportWorker_ProcessBranchNotAllowedForStageableRepositories(t *testing.T) { job := v0alpha1.Job{ Spec: v0alpha1.JobSpec{ Action: v0alpha1.JobActionPush, @@ -362,24 +353,16 @@ func TestExportWorker_ProcessBranchNotAllowedForClonableRepositories(t *testing. mockRepo.On("Config").Return(&v0alpha1.Repository{ Spec: v0alpha1.RepositorySpec{ Type: v0alpha1.GitHubRepositoryType, - Workflows: []v0alpha1.Workflow{v0alpha1.BranchWorkflow}, + Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow}, // Only write workflow, not branch }, }) mockProgress := jobs.NewMockJobProgressRecorder(t) - mockProgress.On("SetMessage", mock.Anything, "clone target").Return() - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { - if cloneOpts.BeforeFn != nil { - return cloneOpts.BeforeFn() - } + // No progress messages expected in current implementation - return fn(repo, true) - }) - - r := NewExportWorker(nil, nil, nil, mockCloneFn.Execute) + r := NewExportWorker(nil, nil, nil, nil) err := r.Process(context.Background(), mockRepo, job, mockProgress) - require.EqualError(t, err, "branch is not supported for clonable repositories") + require.EqualError(t, err, "this repository does not support the branch workflow") } func TestExportWorker_ProcessGitRepository(t *testing.T) { @@ -407,9 +390,7 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) - // Verify progress messages are set - mockProgress.On("SetMessage", mock.Anything, "clone target").Return() - mockProgress.On("SetMessage", mock.Anything, "push changes").Return() + // No progress messages expected in current implementation mockClients := resources.NewMockClientFactory(t) mockResourceClients := resources.NewMockResourceClients(t) @@ -422,21 +403,15 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) { mockExportFn := NewMockExportFn(t) mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockCloneFn := NewMockWrapWithCloneFn(t) + mockStageFn := NewMockWrapWithStageFn(t) // Verify clone and push options - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.CloneOptions) bool { - return opts.Timeout == 10*time.Minute && !opts.PushOnWrites && opts.BeforeFn != nil - }), mock.MatchedBy(func(opts repository.PushOptions) bool { - return opts.Timeout == 10*time.Minute && opts.Progress != nil && opts.BeforeFn != nil - }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { - // Execute both BeforeFn functions to verify progress messages - assert.NoError(t, cloneOpts.BeforeFn()) - assert.NoError(t, pushOpts.BeforeFn()) - + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { + return opts.Timeout == 10*time.Minute && !opts.PushOnWrites + }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -477,12 +452,12 @@ func TestExportWorker_ProcessGitRepositoryExportFnError(t *testing.T) { mockExportFn := NewMockExportFn(t) mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("export failed")) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export failed") } diff --git a/pkg/registry/apis/provisioning/jobs/history_mock.go b/pkg/registry/apis/provisioning/jobs/history_mock.go index fd762e9f485..dbb61568566 100644 --- a/pkg/registry/apis/provisioning/jobs/history_mock.go +++ b/pkg/registry/apis/provisioning/jobs/history_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go b/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go index f7f4c53681f..7ead8a206e7 100644 --- a/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go +++ b/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go index ad2754bc444..191e0c4922f 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go @@ -10,57 +10,38 @@ import ( provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" ) type LegacyMigrator struct { legacyMigrator LegacyResourcesMigrator storageSwapper StorageSwapper syncWorker jobs.Worker - wrapWithCloneFn WrapWithCloneFn + wrapWithStageFn WrapWithStageFn } func NewLegacyMigrator( legacyMigrator LegacyResourcesMigrator, storageSwapper StorageSwapper, syncWorker jobs.Worker, - wrapWithCloneFn WrapWithCloneFn, + wrapWithStageFn WrapWithStageFn, ) *LegacyMigrator { return &LegacyMigrator{ legacyMigrator: legacyMigrator, storageSwapper: storageSwapper, syncWorker: syncWorker, - wrapWithCloneFn: wrapWithCloneFn, + wrapWithStageFn: wrapWithStageFn, } } func (m *LegacyMigrator) Migrate(ctx context.Context, rw repository.ReaderWriter, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { namespace := rw.Config().Namespace - - writer := gogit.Progress(func(line string) { - progress.SetMessage(ctx, line) - }, "finished") - cloneOptions := repository.CloneOptions{ + stageOptions := repository.StageOptions{ PushOnWrites: options.History, // TODO: make this configurable - Timeout: 10 * time.Minute, - Progress: writer, - BeforeFn: func() error { - progress.SetMessage(ctx, "clone repository") - return nil - }, - } - pushOptions := repository.PushOptions{ - // TODO: make this configurable - Timeout: 10 * time.Minute, - Progress: writer, - BeforeFn: func() error { - progress.SetMessage(ctx, "push changes") - return nil - }, + Timeout: 10 * time.Minute, } - if err := m.wrapWithCloneFn(ctx, rw, cloneOptions, pushOptions, func(repo repository.Repository, cloned bool) error { + if err := m.wrapWithStageFn(ctx, rw, stageOptions, func(repo repository.Repository, staged bool) error { rw, ok := repo.(repository.ReaderWriter) if !ok { return errors.New("migration job submitted targeting repository that is not a ReaderWriter") diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go index 5dc64480333..877bd01fd33 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go @@ -3,7 +3,6 @@ package migrate import ( "context" "errors" - "fmt" "testing" "time" @@ -16,12 +15,12 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" ) -func TestWrapWithCloneFn(t *testing.T) { +func TestWrapWithStageFn(t *testing.T) { t.Run("should return error when repository is not a ReaderWriter", func(t *testing.T) { // Setup ctx := context.Background() // Create the wrapper function that matches WrapWithCloneFn signature - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { // pass a reader to function call repo := repository.NewMockReader(t) return fn(repo, true) @@ -56,7 +55,7 @@ func TestWrapWithCloneFn_Error(t *testing.T) { expectedErr := errors.New("clone failed") // Create the wrapper function that returns an error - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return expectedErr } @@ -98,7 +97,7 @@ func TestLegacyMigrator_MigrateFails(t *testing.T) { mockWorker := jobs.NewMockWorker(t) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -147,7 +146,7 @@ func TestLegacyMigrator_ResetUnifiedStorageFails(t *testing.T) { mockWorker := jobs.NewMockWorker(t) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -202,7 +201,7 @@ func TestLegacyMigrator_SyncFails(t *testing.T) { }), mock.Anything).Return(expectedErr) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -257,7 +256,7 @@ func TestLegacyMigrator_SyncFails(t *testing.T) { }), mock.Anything).Return(syncErr) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -310,7 +309,7 @@ func TestLegacyMigrator_Success(t *testing.T) { }), mock.Anything).Return(nil) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -352,19 +351,7 @@ func TestLegacyMigrator_BeforeFnExecution(t *testing.T) { mockStorageSwapper := NewMockStorageSwapper(t) mockWorker := jobs.NewMockWorker(t) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { - if clone.BeforeFn != nil { - if err := clone.BeforeFn(); err != nil { - return err - } - } - - if push.BeforeFn != nil { - if err := push.BeforeFn(); err != nil { - return err - } - } - + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return errors.New("abort test here") } @@ -376,8 +363,7 @@ func TestLegacyMigrator_BeforeFnExecution(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, "clone repository").Return() - progress.On("SetMessage", mock.Anything, "push changes").Return() + // No progress messages expected in current staging implementation // Execute repo := repository.NewMockRepository(t) @@ -399,19 +385,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) { mockWorker := jobs.NewMockWorker(t) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { - if clone.Progress != nil { - if _, err := clone.Progress.Write([]byte("clone repository\n")); err != nil { - return fmt.Errorf("failed to write to clone progress in tests: %w", err) - } - } - - if push.Progress != nil { - if _, err := push.Progress.Write([]byte("push changes\n")); err != nil { - return fmt.Errorf("failed to write to push progress in tests: %w", err) - } - } - + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return errors.New("abort test here") } @@ -423,8 +397,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, "clone repository").Return() - progress.On("SetMessage", mock.Anything, "push changes").Return() + // No progress messages expected in current staging implementation repo := repository.NewMockRepository(t) repo.On("Config").Return(&provisioning.Repository{ @@ -437,10 +410,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) { require.EqualError(t, err, "migrate from SQL: abort test here") require.Eventually(t, func() bool { - if len(progress.Calls) != 2 { - return false - } - + // No progress message calls expected in current staging implementation return progress.AssertExpectations(t) }, time.Second, 10*time.Millisecond) }) diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go index ff6a0cd0cf7..9fa21e12ab0 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go @@ -9,7 +9,7 @@ import ( mock "github.com/stretchr/testify/mock" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) // MockBulkStoreClient is an autogenerated mock type for the BulkStoreClient type diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_clone_fn.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_clone_fn.go deleted file mode 100644 index 8ab2973de33..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_clone_fn.go +++ /dev/null @@ -1,87 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package migrate - -import ( - context "context" - - repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - mock "github.com/stretchr/testify/mock" -) - -// MockWrapWithCloneFn is an autogenerated mock type for the WrapWithCloneFn type -type MockWrapWithCloneFn struct { - mock.Mock -} - -type MockWrapWithCloneFn_Expecter struct { - mock *mock.Mock -} - -func (_m *MockWrapWithCloneFn) EXPECT() *MockWrapWithCloneFn_Expecter { - return &MockWrapWithCloneFn_Expecter{mock: &_m.Mock} -} - -// Execute provides a mock function with given fields: ctx, repo, cloneOptions, pushOptions, fn -func (_m *MockWrapWithCloneFn) Execute(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error) error { - ret := _m.Called(ctx, repo, cloneOptions, pushOptions, fn) - - if len(ret) == 0 { - panic("no return value specified for Execute") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error); ok { - r0 = rf(ctx, repo, cloneOptions, pushOptions, fn) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockWrapWithCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' -type MockWrapWithCloneFn_Execute_Call struct { - *mock.Call -} - -// Execute is a helper method to define mock.On call -// - ctx context.Context -// - repo repository.Repository -// - cloneOptions repository.CloneOptions -// - pushOptions repository.PushOptions -// - fn func(repository.Repository , bool) error -func (_e *MockWrapWithCloneFn_Expecter) Execute(ctx interface{}, repo interface{}, cloneOptions interface{}, pushOptions interface{}, fn interface{}) *MockWrapWithCloneFn_Execute_Call { - return &MockWrapWithCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, cloneOptions, pushOptions, fn)} -} - -func (_c *MockWrapWithCloneFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error)) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.CloneOptions), args[3].(repository.PushOptions), args[4].(func(repository.Repository, bool) error)) - }) - return _c -} - -func (_c *MockWrapWithCloneFn_Execute_Call) Return(_a0 error) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockWrapWithCloneFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Return(run) - return _c -} - -// NewMockWrapWithCloneFn creates a new instance of MockWrapWithCloneFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockWrapWithCloneFn(t interface { - mock.TestingT - Cleanup(func()) -}) *MockWrapWithCloneFn { - mock := &MockWrapWithCloneFn{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go index 2879d14fe7e..e2caeb7b3f4 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" ) -//go:generate mockery --name WrapWithCloneFn --structname MockWrapWithCloneFn --inpackage --filename mock_wrap_with_clone_fn.go --with-expecter -type WrapWithCloneFn func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repo repository.Repository, cloned bool) error) error +//go:generate mockery --name WrapWithStageFn --structname MockWrapWithStageFn --inpackage --filename mock_wrap_with_stage_fn.go --with-expecter +type WrapWithStageFn func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repo repository.Repository, staged bool) error) error type UnifiedStorageMigrator struct { namespaceCleaner NamespaceCleaner diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go b/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go index 1524487bf75..31242a48189 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go @@ -12,6 +12,7 @@ import ( provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/local" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) @@ -88,7 +89,7 @@ func TestMigrationWorker_WithHistory(t *testing.T) { progressRecorder.On("SetTotal", mock.Anything, 10).Return() progressRecorder.On("Strict").Return() - repo := repository.NewLocal(&provisioning.Repository{}, nil) + repo := local.NewLocal(&provisioning.Repository{}, nil) err := worker.Process(context.Background(), repo, job, progressRecorder) require.EqualError(t, err, "history is only supported for github repositories") }) diff --git a/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go b/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go index fd634d60844..cfe7e46f88b 100644 --- a/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/queue_mock.go b/pkg/registry/apis/provisioning/jobs/queue_mock.go index f20a8be6221..41552749cad 100644 --- a/pkg/registry/apis/provisioning/jobs/queue_mock.go +++ b/pkg/registry/apis/provisioning/jobs/queue_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go b/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go index d0152e725bc..b7dc38de650 100644 --- a/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go +++ b/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/store_mock.go b/pkg/registry/apis/provisioning/jobs/store_mock.go index 300bf5fb271..8a6bb364f80 100644 --- a/pkg/registry/apis/provisioning/jobs/store_mock.go +++ b/pkg/registry/apis/provisioning/jobs/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/sync/changes.go b/pkg/registry/apis/provisioning/jobs/sync/changes.go index 2e9b789c992..4c1f5db9136 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/changes.go +++ b/pkg/registry/apis/provisioning/jobs/sync/changes.go @@ -3,7 +3,6 @@ package sync import ( "context" "fmt" - "sort" "strings" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" @@ -143,17 +142,7 @@ func Changes(source []repository.FileTreeEntry, target *provisioning.ResourceLis } // Deepest first (stable sort order) - sort.Slice(changes, func(i, j int) bool { - if safepath.Depth(changes[i].Path) > safepath.Depth(changes[j].Path) { - return true - } - - if safepath.Depth(changes[i].Path) < safepath.Depth(changes[j].Path) { - return false - } - - return changes[i].Path < changes[j].Path - }) + safepath.SortByDepth(changes, func(c ResourceFileChange) string { return c.Path }, false) return changes, nil } diff --git a/pkg/registry/apis/provisioning/jobs/sync/compare_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/compare_fn_mock.go index 7cb74e9497a..9a27ddb394c 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/compare_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/compare_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go index 29e44c5c02c..d2da032c6c0 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go index cef2b0deb07..b38f813e68d 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/sync/repository_patch_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/repository_patch_fn_mock.go index 6998d67f019..fb44f7eda29 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/repository_patch_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/repository_patch_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/sync/syncer_mock.go b/pkg/registry/apis/provisioning/jobs/sync/syncer_mock.go index 79ac697436d..3eb3e4de7bb 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/syncer_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/syncer_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/worker_mock.go b/pkg/registry/apis/provisioning/jobs/worker_mock.go index b264b258d1c..59e3c0df1b4 100644 --- a/pkg/registry/apis/provisioning/jobs/worker_mock.go +++ b/pkg/registry/apis/provisioning/jobs/worker_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index fe5087ca1d3..3b6a2775ba7 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -45,9 +45,9 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/migrate" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/sync" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/nanogit" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/local" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" @@ -76,7 +76,7 @@ type APIBuilder struct { features featuremgmt.FeatureToggles getter rest.Getter - localFileResolver *repository.LocalFolderResolver + localFileResolver *local.LocalFolderResolver parsers resources.ParserFactory repositoryResources resources.RepositoryResourcesFactory clients resources.ClientFactory @@ -105,7 +105,7 @@ type APIBuilder struct { // It avoids anything that is core to Grafana, such that it can be used in a multi-tenant service down the line. // This means there are no hidden dependencies, and no use of e.g. *settings.Cfg. func NewAPIBuilder( - local *repository.LocalFolderResolver, + local *local.LocalFolderResolver, features featuremgmt.FeatureToggles, unified resource.ResourceClient, clonedir string, // where repo clones are managed @@ -168,14 +168,7 @@ func RegisterAPIService( return nil, nil } - logger := logging.DefaultLogger.With("logger", "provisioning startup") - if features.IsEnabledGlobally(featuremgmt.FlagNanoGit) { - logger.Info("Using nanogit for repositories") - } else { - logger.Debug("Using go-git and Github API for repositories") - } - - folderResolver := &repository.LocalFolderResolver{ + folderResolver := &local.LocalFolderResolver{ PermittedPrefixes: cfg.PermittedProvisioningPaths, HomePath: safepath.Clean(cfg.HomePath), } @@ -606,11 +599,12 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.repositoryLister = repoInformer.Lister() + stageIfPossible := repository.WrapWithStageAndPushIfPossible exportWorker := export.NewExportWorker( b.clients, b.repositoryResources, export.ExportAll, - repository.WrapWithCloneAndPushIfPossible, + stageIfPossible, ) b.statusPatcher = controller.NewRepositoryStatusPatcher(b.GetClient()) @@ -636,7 +630,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH legacyResources, storageSwapper, syncWorker, - repository.WrapWithCloneAndPushIfPossible, + stageIfPossible, ) cleaner := migrate.NewNamespaceCleaner(b.clients) @@ -1170,52 +1164,63 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor switch r.Spec.Type { case provisioning.LocalRepositoryType: - return repository.NewLocal(r, b.localFileResolver), nil + return local.NewLocal(r, b.localFileResolver), nil case provisioning.GitRepositoryType: - return nanogit.NewGitRepository(ctx, b.secrets, r, nanogit.RepositoryConfig{ + // Decrypt token if needed + token := r.Spec.Git.Token + if token == "" { + decrypted, err := b.secrets.Decrypt(ctx, r.Spec.Git.EncryptedToken) + if err != nil { + return nil, fmt.Errorf("decrypt git token: %w", err) + } + token = string(decrypted) + } + + return git.NewGitRepository(ctx, r, git.RepositoryConfig{ URL: r.Spec.Git.URL, Branch: r.Spec.Git.Branch, Path: r.Spec.Git.Path, - Token: r.Spec.Git.Token, + Token: token, EncryptedToken: r.Spec.Git.EncryptedToken, }) case provisioning.GitHubRepositoryType: - cloneFn := func(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) { - return gogit.Clone(ctx, b.clonedir, r, opts, b.secrets) - } - - apiRepo, err := repository.NewGitHub(ctx, r, b.ghFactory, b.secrets, cloneFn) - if err != nil { - return nil, fmt.Errorf("create github API repository: %w", err) - } - logger := logging.FromContext(ctx).With("url", r.Spec.GitHub.URL, "branch", r.Spec.GitHub.Branch, "path", r.Spec.GitHub.Path) - if !b.features.IsEnabledGlobally(featuremgmt.FlagNanoGit) { - logger.Debug("Instantiating Github repository with go-git and Github API") - return apiRepo, nil - } - - logger.Info("Instantiating Github repository with nanogit") + logger.Info("Instantiating Github repository") ghCfg := r.Spec.GitHub if ghCfg == nil { return nil, fmt.Errorf("github configuration is required for nano git") } - gitCfg := nanogit.RepositoryConfig{ + // Decrypt GitHub token if needed + ghToken := ghCfg.Token + if ghToken == "" && len(ghCfg.EncryptedToken) > 0 { + decrypted, err := b.secrets.Decrypt(ctx, ghCfg.EncryptedToken) + if err != nil { + return nil, fmt.Errorf("decrypt github token: %w", err) + } + ghToken = string(decrypted) + } + + gitCfg := git.RepositoryConfig{ URL: ghCfg.URL, Branch: ghCfg.Branch, Path: ghCfg.Path, - Token: ghCfg.Token, + Token: ghToken, EncryptedToken: ghCfg.EncryptedToken, } - nanogitRepo, err := nanogit.NewGitRepository(ctx, b.secrets, r, gitCfg) + gitRepo, err := git.NewGitRepository(ctx, r, gitCfg) if err != nil { - return nil, fmt.Errorf("error creating nanogit repository: %w", err) + return nil, fmt.Errorf("error creating git repository: %w", err) } - return nanogit.NewGithubRepository(apiRepo, nanogitRepo), nil + ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, ghToken) + if err != nil { + return nil, fmt.Errorf("error creating github repository: %w", err) + } + + return ghRepo, nil default: return nil, fmt.Errorf("unknown repository type (%s)", r.Spec.Type) } diff --git a/pkg/registry/apis/provisioning/repository/clonable_repository_mock.go b/pkg/registry/apis/provisioning/repository/clonable_repository_mock.go deleted file mode 100644 index 9e1d671fe36..00000000000 --- a/pkg/registry/apis/provisioning/repository/clonable_repository_mock.go +++ /dev/null @@ -1,95 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package repository - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockClonableRepository is an autogenerated mock type for the ClonableRepository type -type MockClonableRepository struct { - mock.Mock -} - -type MockClonableRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *MockClonableRepository) EXPECT() *MockClonableRepository_Expecter { - return &MockClonableRepository_Expecter{mock: &_m.Mock} -} - -// Clone provides a mock function with given fields: ctx, opts -func (_m *MockClonableRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Clone") - } - - var r0 ClonedRepository - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ClonedRepository) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClonableRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone' -type MockClonableRepository_Clone_Call struct { - *mock.Call -} - -// Clone is a helper method to define mock.On call -// - ctx context.Context -// - opts CloneOptions -func (_e *MockClonableRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockClonableRepository_Clone_Call { - return &MockClonableRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)} -} - -func (_c *MockClonableRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockClonableRepository_Clone_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(CloneOptions)) - }) - return _c -} - -func (_c *MockClonableRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockClonableRepository_Clone_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClonableRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockClonableRepository_Clone_Call { - _c.Call.Return(run) - return _c -} - -// NewMockClonableRepository creates a new instance of MockClonableRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockClonableRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *MockClonableRepository { - mock := &MockClonableRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/clone.go b/pkg/registry/apis/provisioning/repository/clone.go deleted file mode 100644 index d7224a23631..00000000000 --- a/pkg/registry/apis/provisioning/repository/clone.go +++ /dev/null @@ -1,44 +0,0 @@ -package repository - -import ( - context "context" - "fmt" - - "github.com/grafana/grafana-app-sdk/logging" -) - -// WrapWithCloneAndPushIfPossible clones a repository if possible, executes operations on the clone, -// and automatically pushes changes when the function completes. For repositories that support cloning, -// all operations are transparently executed on the clone, and the clone is automatically cleaned up -// afterward. If cloning is not supported, the original repository instance is used directly. -func WrapWithCloneAndPushIfPossible( - ctx context.Context, - repo Repository, - cloneOptions CloneOptions, - pushOptions PushOptions, - fn func(repo Repository, cloned bool) error, -) error { - clonable, ok := repo.(ClonableRepository) - if !ok { - return fn(repo, false) - } - - clone, err := clonable.Clone(ctx, cloneOptions) - if err != nil { - return fmt.Errorf("clone repository: %w", err) - } - - // We don't, we simply log it - // FIXME: should we handle this differently? - defer func() { - if err := clone.Remove(ctx); err != nil { - logging.FromContext(ctx).Error("failed to remove cloned repository after export", "err", err) - } - }() - - if err := fn(clone, true); err != nil { - return err - } - - return clone.Push(ctx, pushOptions) -} diff --git a/pkg/registry/apis/provisioning/repository/clone_fn_mock.go b/pkg/registry/apis/provisioning/repository/clone_fn_mock.go deleted file mode 100644 index f248ee243a6..00000000000 --- a/pkg/registry/apis/provisioning/repository/clone_fn_mock.go +++ /dev/null @@ -1,95 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package repository - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockCloneFn is an autogenerated mock type for the CloneFn type -type MockCloneFn struct { - mock.Mock -} - -type MockCloneFn_Expecter struct { - mock *mock.Mock -} - -func (_m *MockCloneFn) EXPECT() *MockCloneFn_Expecter { - return &MockCloneFn_Expecter{mock: &_m.Mock} -} - -// Execute provides a mock function with given fields: ctx, opts -func (_m *MockCloneFn) Execute(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Execute") - } - - var r0 ClonedRepository - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ClonedRepository) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' -type MockCloneFn_Execute_Call struct { - *mock.Call -} - -// Execute is a helper method to define mock.On call -// - ctx context.Context -// - opts CloneOptions -func (_e *MockCloneFn_Expecter) Execute(ctx interface{}, opts interface{}) *MockCloneFn_Execute_Call { - return &MockCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, opts)} -} - -func (_c *MockCloneFn_Execute_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockCloneFn_Execute_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(CloneOptions)) - }) - return _c -} - -func (_c *MockCloneFn_Execute_Call) Return(_a0 ClonedRepository, _a1 error) *MockCloneFn_Execute_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockCloneFn_Execute_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockCloneFn_Execute_Call { - _c.Call.Return(run) - return _c -} - -// NewMockCloneFn creates a new instance of MockCloneFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockCloneFn(t interface { - mock.TestingT - Cleanup(func()) -}) *MockCloneFn { - mock := &MockCloneFn{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/clone_test.go b/pkg/registry/apis/provisioning/repository/clone_test.go deleted file mode 100644 index ac685386aac..00000000000 --- a/pkg/registry/apis/provisioning/repository/clone_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package repository - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -type mockClonableRepo struct { - *MockClonableRepository - *MockClonedRepository -} - -func Test_WrapWithCloneAndPushIfPossible_NonClonableRepository(t *testing.T) { - nonClonable := NewMockRepository(t) - var called bool - fn := func(repo Repository, cloned bool) error { - called = true - return errors.New("operation failed") - } - - err := WrapWithCloneAndPushIfPossible(context.Background(), nonClonable, CloneOptions{}, PushOptions{}, fn) - require.EqualError(t, err, "operation failed") - require.True(t, called) -} - -func TestWrapWithCloneAndPushIfPossible(t *testing.T) { - tests := []struct { - name string - setupMocks func(t *testing.T) *mockClonableRepo - operation func(repo Repository, cloned bool) error - expectedError string - }{ - { - name: "successful clone, operation, and push", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - mockCloned := NewMockClonedRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil) - mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(nil) - mockCloned.EXPECT().Remove(mock.Anything).Return(nil) - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - MockClonedRepository: mockCloned, - } - }, - operation: func(repo Repository, cloned bool) error { - require.True(t, cloned) - return nil - }, - }, - { - name: "clone failure", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(nil, errors.New("clone failed")) - - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - } - }, - operation: func(repo Repository, cloned bool) error { - return nil - }, - expectedError: "clone repository: clone failed", - }, - { - name: "operation failure", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - mockCloned := NewMockClonedRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil) - mockCloned.EXPECT().Remove(mock.Anything).Return(nil) - - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - MockClonedRepository: mockCloned, - } - }, - operation: func(repo Repository, cloned bool) error { - return errors.New("operation failed") - }, - expectedError: "operation failed", - }, - { - name: "push failure", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - mockCloned := NewMockClonedRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil) - mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(errors.New("push failed")) - mockCloned.EXPECT().Remove(mock.Anything).Return(nil) - - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - MockClonedRepository: mockCloned, - } - }, - operation: func(repo Repository, cloned bool) error { - return nil - }, - expectedError: "push failed", - }, - { - name: "remove failure should only log", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - mockCloned := NewMockClonedRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil) - mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(nil) - mockCloned.EXPECT().Remove(mock.Anything).Return(errors.New("remove failed")) - - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - MockClonedRepository: mockCloned, - } - }, - operation: func(repo Repository, cloned bool) error { - return nil - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - repo := tt.setupMocks(t) - err := WrapWithCloneAndPushIfPossible(context.Background(), repo, CloneOptions{}, PushOptions{}, tt.operation) - - if tt.expectedError != "" { - require.EqualError(t, err, tt.expectedError) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/pkg/registry/apis/provisioning/repository/git/branch.go b/pkg/registry/apis/provisioning/repository/git/branch.go new file mode 100644 index 00000000000..28d4ec7b84c --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/git/branch.go @@ -0,0 +1,33 @@ +package git + +import ( + "regexp" + "strings" +) + +// basicGitBranchNameRegex is a regular expression to validate a git branch name +// it does not cover all cases as positive lookaheads are not supported in Go's regexp +var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`) + +// IsValidGitBranchName checks if a branch name is valid. +// It uses the following regexp `^[a-zA-Z0-9\-\_\/\.]+$` to validate the branch name with some additional checks that must satisfy the following rules: +// 1. The branch name must have at least one character and must not be empty. +// 2. The branch name cannot start with `/` or end with `/`, `.`, or whitespace. +// 3. The branch name cannot contain consecutive slashes (`//`). +// 4. The branch name cannot contain consecutive dots (`..`). +// 5. The branch name cannot contain `@{`. +// 6. The branch name cannot include the following characters: `~`, `^`, `:`, `?`, `*`, `[`, `\`, or `]`. +func IsValidGitBranchName(branch string) bool { + if !basicGitBranchNameRegex.MatchString(branch) { + return false + } + + // Additional checks for invalid patterns + if strings.HasPrefix(branch, "/") || strings.HasSuffix(branch, "/") || + strings.HasSuffix(branch, ".") || strings.Contains(branch, "..") || + strings.Contains(branch, "//") || strings.HasSuffix(branch, ".lock") { + return false + } + + return true +} diff --git a/pkg/registry/apis/provisioning/repository/git/branch_test.go b/pkg/registry/apis/provisioning/repository/git/branch_test.go new file mode 100644 index 00000000000..fd6d3259a81 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/git/branch_test.go @@ -0,0 +1,47 @@ +package git + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidGitBranchName(t *testing.T) { + tests := []struct { + name string + branch string + expected bool + }{ + {"Valid branch name", "feature/add-tests", true}, + {"Valid branch name with numbers", "feature/123-add-tests", true}, + {"Valid branch name with dots", "feature.add.tests", true}, + {"Valid branch name with hyphens", "feature-add-tests", true}, + {"Valid branch name with underscores", "feature_add_tests", true}, + {"Valid branch name with mixed characters", "feature/add_tests-123", true}, + {"Starts with /", "/feature", false}, + {"Ends with /", "feature/", false}, + {"Ends with .", "feature.", false}, + {"Ends with space", "feature ", false}, + {"Contains consecutive slashes", "feature//branch", false}, + {"Contains consecutive dots", "feature..branch", false}, + {"Contains @{", "feature@{branch", false}, + {"Contains invalid character ~", "feature~branch", false}, + {"Contains invalid character ^", "feature^branch", false}, + {"Contains invalid character :", "feature:branch", false}, + {"Contains invalid character ?", "feature?branch", false}, + {"Contains invalid character *", "feature*branch", false}, + {"Contains invalid character [", "feature[branch", false}, + {"Contains invalid character ]", "feature]branch", false}, + {"Contains invalid character \\", "feature\\branch", false}, + {"Empty branch name", "", false}, + {"Only whitespace", " ", false}, + {"Single valid character", "a", true}, + {"Ends with .lock", "feature.lock", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, IsValidGitBranchName(tt.branch)) + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/git.go b/pkg/registry/apis/provisioning/repository/git/git.go similarity index 60% rename from pkg/registry/apis/provisioning/repository/git.go rename to pkg/registry/apis/provisioning/repository/git/git.go index 6a4244e76da..02c0f70dc9b 100644 --- a/pkg/registry/apis/provisioning/repository/git.go +++ b/pkg/registry/apis/provisioning/repository/git/git.go @@ -1,15 +1,17 @@ -package repository +package git + +import "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" // GitRepository is an interface that combines all repository capabilities // needed for Git repositories. // //go:generate mockery --name GitRepository --structname MockGitRepository --inpackage --filename git_repository_mock.go --with-expecter type GitRepository interface { - Repository - Versioned - Writer - Reader - ClonableRepository + repository.Repository + repository.Versioned + repository.Writer + repository.Reader + repository.StageableRepository URL() string Branch() string } diff --git a/pkg/registry/apis/provisioning/repository/git_repository_mock.go b/pkg/registry/apis/provisioning/repository/git/git_repository_mock.go similarity index 88% rename from pkg/registry/apis/provisioning/repository/git_repository_mock.go rename to pkg/registry/apis/provisioning/repository/git/git_repository_mock.go index 07ef1c73cba..4492595e98f 100644 --- a/pkg/registry/apis/provisioning/repository/git_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/git/git_repository_mock.go @@ -1,6 +1,6 @@ // Code generated by mockery v2.52.4. DO NOT EDIT. -package repository +package git import ( context "context" @@ -8,6 +8,8 @@ import ( mock "github.com/stretchr/testify/mock" field "k8s.io/apimachinery/pkg/util/validation/field" + repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" ) @@ -69,83 +71,24 @@ func (_c *MockGitRepository_Branch_Call) RunAndReturn(run func() string) *MockGi return _c } -// Clone provides a mock function with given fields: ctx, opts -func (_m *MockGitRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Clone") - } - - var r0 ClonedRepository - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ClonedRepository) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockGitRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone' -type MockGitRepository_Clone_Call struct { - *mock.Call -} - -// Clone is a helper method to define mock.On call -// - ctx context.Context -// - opts CloneOptions -func (_e *MockGitRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockGitRepository_Clone_Call { - return &MockGitRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)} -} - -func (_c *MockGitRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockGitRepository_Clone_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(CloneOptions)) - }) - return _c -} - -func (_c *MockGitRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockGitRepository_Clone_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockGitRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockGitRepository_Clone_Call { - _c.Call.Return(run) - return _c -} - // CompareFiles provides a mock function with given fields: ctx, base, ref -func (_m *MockGitRepository) CompareFiles(ctx context.Context, base string, ref string) ([]VersionedFileChange, error) { +func (_m *MockGitRepository) CompareFiles(ctx context.Context, base string, ref string) ([]repository.VersionedFileChange, error) { ret := _m.Called(ctx, base, ref) if len(ret) == 0 { panic("no return value specified for CompareFiles") } - var r0 []VersionedFileChange + var r0 []repository.VersionedFileChange var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]VersionedFileChange, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]repository.VersionedFileChange, error)); ok { return rf(ctx, base, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) []VersionedFileChange); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) []repository.VersionedFileChange); ok { r0 = rf(ctx, base, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]VersionedFileChange) + r0 = ret.Get(0).([]repository.VersionedFileChange) } } @@ -178,12 +121,12 @@ func (_c *MockGitRepository_CompareFiles_Call) Run(run func(ctx context.Context, return _c } -func (_c *MockGitRepository_CompareFiles_Call) Return(_a0 []VersionedFileChange, _a1 error) *MockGitRepository_CompareFiles_Call { +func (_c *MockGitRepository_CompareFiles_Call) Return(_a0 []repository.VersionedFileChange, _a1 error) *MockGitRepository_CompareFiles_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGitRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]VersionedFileChange, error)) *MockGitRepository_CompareFiles_Call { +func (_c *MockGitRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]repository.VersionedFileChange, error)) *MockGitRepository_CompareFiles_Call { _c.Call.Return(run) return _c } @@ -451,23 +394,23 @@ func (_c *MockGitRepository_LatestRef_Call) RunAndReturn(run func(context.Contex } // Read provides a mock function with given fields: ctx, path, ref -func (_m *MockGitRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { +func (_m *MockGitRepository) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) { ret := _m.Called(ctx, path, ref) if len(ret) == 0 { panic("no return value specified for Read") } - var r0 *FileInfo + var r0 *repository.FileInfo var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*repository.FileInfo, error)); ok { return rf(ctx, path, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *repository.FileInfo); ok { r0 = rf(ctx, path, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*FileInfo) + r0 = ret.Get(0).(*repository.FileInfo) } } @@ -500,34 +443,34 @@ func (_c *MockGitRepository_Read_Call) Run(run func(ctx context.Context, path st return _c } -func (_c *MockGitRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockGitRepository_Read_Call { +func (_c *MockGitRepository_Read_Call) Return(_a0 *repository.FileInfo, _a1 error) *MockGitRepository_Read_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGitRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockGitRepository_Read_Call { +func (_c *MockGitRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*repository.FileInfo, error)) *MockGitRepository_Read_Call { _c.Call.Return(run) return _c } // ReadTree provides a mock function with given fields: ctx, ref -func (_m *MockGitRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { +func (_m *MockGitRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { ret := _m.Called(ctx, ref) if len(ret) == 0 { panic("no return value specified for ReadTree") } - var r0 []FileTreeEntry + var r0 []repository.FileTreeEntry var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) ([]repository.FileTreeEntry, error)); ok { return rf(ctx, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) []repository.FileTreeEntry); ok { r0 = rf(ctx, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]FileTreeEntry) + r0 = ret.Get(0).([]repository.FileTreeEntry) } } @@ -559,12 +502,71 @@ func (_c *MockGitRepository_ReadTree_Call) Run(run func(ctx context.Context, ref return _c } -func (_c *MockGitRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockGitRepository_ReadTree_Call { +func (_c *MockGitRepository_ReadTree_Call) Return(_a0 []repository.FileTreeEntry, _a1 error) *MockGitRepository_ReadTree_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGitRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockGitRepository_ReadTree_Call { +func (_c *MockGitRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]repository.FileTreeEntry, error)) *MockGitRepository_ReadTree_Call { + _c.Call.Return(run) + return _c +} + +// Stage provides a mock function with given fields: ctx, opts +func (_m *MockGitRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Stage") + } + + var r0 repository.StagedRepository + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) (repository.StagedRepository, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) repository.StagedRepository); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(repository.StagedRepository) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, repository.StageOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGitRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage' +type MockGitRepository_Stage_Call struct { + *mock.Call +} + +// Stage is a helper method to define mock.On call +// - ctx context.Context +// - opts repository.StageOptions +func (_e *MockGitRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockGitRepository_Stage_Call { + return &MockGitRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)} +} + +func (_c *MockGitRepository_Stage_Call) Run(run func(ctx context.Context, opts repository.StageOptions)) *MockGitRepository_Stage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(repository.StageOptions)) + }) + return _c +} + +func (_c *MockGitRepository_Stage_Call) Return(_a0 repository.StagedRepository, _a1 error) *MockGitRepository_Stage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGitRepository_Stage_Call) RunAndReturn(run func(context.Context, repository.StageOptions) (repository.StagedRepository, error)) *MockGitRepository_Stage_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/repository/nanogit/git.go b/pkg/registry/apis/provisioning/repository/git/repository.go similarity index 93% rename from pkg/registry/apis/provisioning/repository/nanogit/git.go rename to pkg/registry/apis/provisioning/repository/git/repository.go index ce1773e787a..7d0d4d518f1 100644 --- a/pkg/registry/apis/provisioning/repository/nanogit/git.go +++ b/pkg/registry/apis/provisioning/repository/git/repository.go @@ -1,12 +1,14 @@ -package nanogit +package git import ( + "bytes" "context" "errors" "fmt" "log/slog" "net/http" "net/url" + "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -17,7 +19,6 @@ import ( provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/nanogit" "github.com/grafana/nanogit/log" "github.com/grafana/nanogit/options" @@ -38,28 +39,19 @@ type gitRepository struct { config *provisioning.Repository gitConfig RepositoryConfig client nanogit.Client - secrets secrets.Service } func NewGitRepository( ctx context.Context, - secrets secrets.Service, config *provisioning.Repository, gitConfig RepositoryConfig, -) (repository.GitRepository, error) { - if gitConfig.Token == "" { - decrypted, err := secrets.Decrypt(ctx, gitConfig.EncryptedToken) - if err != nil { - return nil, fmt.Errorf("decrypt token: %w", err) - } - gitConfig.Token = string(decrypted) +) (GitRepository, error) { + var opts []options.Option + if len(gitConfig.Token) > 0 { + opts = append(opts, options.WithBasicAuth("git", gitConfig.Token)) } - // Create nanogit client with authentication - client, err := nanogit.NewHTTPClient( - gitConfig.URL, - options.WithBasicAuth("git", gitConfig.Token), - ) + client, err := nanogit.NewHTTPClient(gitConfig.URL, opts...) if err != nil { return nil, fmt.Errorf("create nanogit client: %w", err) } @@ -68,7 +60,6 @@ func NewGitRepository( config: config, gitConfig: gitConfig, client: client, - secrets: secrets, }, nil } @@ -98,12 +89,15 @@ func (r *gitRepository) Validate() (list field.ErrorList) { } if cfg.Branch == "" { list = append(list, field.Required(field.NewPath("spec", t, "branch"), "a git branch is required")) - } else if !repository.IsValidGitBranchName(cfg.Branch) { + } else if !IsValidGitBranchName(cfg.Branch) { list = append(list, field.Invalid(field.NewPath("spec", t, "branch"), cfg.Branch, "invalid branch name")) } - if cfg.Token == "" && len(cfg.EncryptedToken) == 0 { - list = append(list, field.Required(field.NewPath("spec", t, "token"), "a git access token is required")) + // If the repository has workflows, we require a token or encrypted token + if len(r.config.Spec.Workflows) > 0 { + if cfg.Token == "" && len(cfg.EncryptedToken) == 0 { + list = append(list, field.Required(field.NewPath("spec", t, "token"), "a git access token is required")) + } } if err := safepath.IsSafe(cfg.Path); err != nil { @@ -412,11 +406,15 @@ func (r *gitRepository) Write(ctx context.Context, path string, ref string, data } ctx, _ = r.logger(ctx, ref) - _, err := r.Read(ctx, path, ref) + info, err := r.Read(ctx, path, ref) if err != nil && !(errors.Is(err, repository.ErrFileNotFound)) { return fmt.Errorf("check if file exists before writing: %w", err) } if err == nil { + // If the value already exists and is the same, we don't need to do anything + if bytes.Equal(info.Data, data) { + return nil + } return r.Update(ctx, path, ref, data, message) } @@ -450,7 +448,8 @@ func (r *gitRepository) delete(ctx context.Context, path string, writer nanogit. finalPath := safepath.Join(r.gitConfig.Path, path) // Check if it's a directory - use DeleteTree for directories, DeleteBlob for files if safepath.IsDir(path) { - if _, err := writer.DeleteTree(ctx, finalPath); err != nil { + trimmed := strings.TrimSuffix(finalPath, "/") + if _, err := writer.DeleteTree(ctx, trimmed); err != nil { if errors.Is(err, nanogit.ErrObjectNotFound) { return repository.ErrFileNotFound } @@ -582,7 +581,7 @@ func (r *gitRepository) CompareFiles(ctx context.Context, base, ref string) ([]r return changes, nil } -func (r *gitRepository) Clone(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) { +func (r *gitRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { return NewStagedGitRepository(ctx, r, opts) } @@ -590,23 +589,26 @@ func (r *gitRepository) Clone(ctx context.Context, opts repository.CloneOptions) func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash.Hash, error) { // Use default branch if ref is empty if ref == "" { - ref = fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch) + ref = r.gitConfig.Branch } // Try to parse ref as a hash first refHash, err := hash.FromHex(ref) - if err == nil && refHash != nil { + if err == nil && refHash != hash.Zero { // Valid hash, return it return refHash, nil } + // Prefix ref with refs/heads/ + ref = fmt.Sprintf("refs/heads/%s", ref) + // Not a valid hash, try to resolve as a branch reference branchRef, err := r.client.GetRef(ctx, ref) if err != nil { if errors.Is(err, nanogit.ErrObjectNotFound) { - return nil, fmt.Errorf("ref not found: %s: %w", ref, repository.ErrRefNotFound) + return hash.Zero, fmt.Errorf("ref not found: %s: %w", ref, repository.ErrRefNotFound) } - return nil, fmt.Errorf("get ref %s: %w", ref, err) + return hash.Zero, fmt.Errorf("get ref %s: %w", ref, err) } return branchRef.Hash, nil @@ -615,7 +617,7 @@ func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash. // ensureBranchExists checks if a branch exists and creates it if it doesn't, // returning the branch reference to avoid duplicate GetRef calls func (r *gitRepository) ensureBranchExists(ctx context.Context, branchName string) (nanogit.Ref, error) { - if !repository.IsValidGitBranchName(branchName) { + if !IsValidGitBranchName(branchName) { return nanogit.Ref{}, &apierrors.StatusError{ ErrStatus: metav1.Status{ Code: http.StatusBadRequest, diff --git a/pkg/registry/apis/provisioning/repository/nanogit/git_test.go b/pkg/registry/apis/provisioning/repository/git/repository_test.go similarity index 92% rename from pkg/registry/apis/provisioning/repository/nanogit/git_test.go rename to pkg/registry/apis/provisioning/repository/git/repository_test.go index 20df321fc74..999d1a74816 100644 --- a/pkg/registry/apis/provisioning/repository/nanogit/git_test.go +++ b/pkg/registry/apis/provisioning/repository/git/repository_test.go @@ -1,4 +1,4 @@ -package nanogit +package git import ( "context" @@ -7,17 +7,17 @@ import ( "testing" "time" - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" - "github.com/grafana/nanogit" - "github.com/grafana/nanogit/mocks" - "github.com/grafana/nanogit/protocol" - "github.com/grafana/nanogit/protocol/hash" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/nanogit" + "github.com/grafana/nanogit/mocks" + "github.com/grafana/nanogit/protocol" + "github.com/grafana/nanogit/protocol/hash" ) func TestGitRepository_Validate(t *testing.T) { @@ -138,10 +138,11 @@ func TestGitRepository_Validate(t *testing.T) { }, }, { - name: "missing token", + name: "missing token for R/W repository", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ - Type: "test_type", + Type: "test_type", + Workflows: []provisioning.Workflow{provisioning.WriteWorkflow}, }, }, gitConfig: RepositoryConfig{ @@ -153,6 +154,21 @@ func TestGitRepository_Validate(t *testing.T) { field.Required(field.NewPath("spec", "test_type", "token"), "a git access token is required"), }, }, + { + name: "missing token for read-only repository", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Type: "test_type", + Workflows: nil, // read-only + }, + }, + gitConfig: RepositoryConfig{ + URL: "https://git.example.com/repo.git", + Branch: "main", + Token: "", // Empty token + }, + want: nil, + }, { name: "unsafe path", config: &provisioning.Repository{ @@ -258,20 +274,8 @@ func TestIsValidGitURL(t *testing.T) { } } -// Mock secrets service for testing -type mockSecretsService struct{} - -func (m *mockSecretsService) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - return []byte("decrypted-token"), nil -} - -func (m *mockSecretsService) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - return []byte("encrypted-token"), nil -} - func TestNewGit(t *testing.T) { ctx := context.Background() - mockSecrets := &mockSecretsService{} config := &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -288,7 +292,7 @@ func TestNewGit(t *testing.T) { // This should succeed in creating the client but won't be able to connect // We just test that the basic structure is created correctly - gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig) + gitRepo, err := NewGitRepository(ctx, config, gitConfig) require.NoError(t, err) require.NotNil(t, gitRepo) require.Equal(t, "https://git.example.com/owner/repo.git", gitRepo.URL()) @@ -695,7 +699,7 @@ func TestGitRepository_Read(t *testing.T) { Hash: hash.Hash{}, }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash([]byte("tree-hash")), + Tree: hash.MustFromHex("abcdef1234567890abcdef1234567890abcdef12"), }, nil) mockClient.GetTreeByPathReturns(&nanogit.Tree{ Hash: hash.Hash{}, @@ -1073,27 +1077,27 @@ func TestGitRepository_Update(t *testing.T) { func TestGitRepository_Delete(t *testing.T) { tests := []struct { - name string - setupMock func(*mocks.FakeClient) - gitConfig RepositoryConfig - path string - ref string - comment string - wantError bool - errorType error + name string + setupMock func(*mocks.FakeClient, *mocks.FakeStagedWriter) + assertions func(*testing.T, *mocks.FakeClient, *mocks.FakeStagedWriter) + gitConfig RepositoryConfig + path string + ref string + comment string + wantError bool + errorType error }{ { name: "success - delete file", - setupMock: func(mockClient *mocks.FakeClient) { + setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", Hash: hash.Hash{}, }, nil) - mockWriter := &mocks.FakeStagedWriter{} + mockWriter.DeleteBlobReturns(hash.Hash{}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) - mockClient.NewStagedWriterReturns(mockWriter, nil) }, gitConfig: RepositoryConfig{ Branch: "main", @@ -1106,16 +1110,19 @@ func TestGitRepository_Delete(t *testing.T) { }, { name: "success - delete directory", - setupMock: func(mockClient *mocks.FakeClient) { + setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", Hash: hash.Hash{}, }, nil) - mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteTreeReturns(hash.Hash{}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) - mockClient.NewStagedWriterReturns(mockWriter, nil) + }, + assertions: func(t *testing.T, fakeClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) { + require.Equal(t, 1, mockWriter.DeleteTreeCallCount(), "DeleteTree should be called once") + _, p := mockWriter.DeleteTreeArgsForCall(0) + require.Equal(t, "configs/testdir", p, "DeleteTree should be called with correct path") }, gitConfig: RepositoryConfig{ Branch: "main", @@ -1128,14 +1135,12 @@ func TestGitRepository_Delete(t *testing.T) { }, { name: "failure - file not found", - setupMock: func(mockClient *mocks.FakeClient) { + setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", Hash: hash.Hash{}, }, nil) - mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound) - mockClient.NewStagedWriterReturns(mockWriter, nil) }, gitConfig: RepositoryConfig{ Branch: "main", @@ -1152,7 +1157,10 @@ func TestGitRepository_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mocks.FakeClient{} - tt.setupMock(mockClient) + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + tt.setupMock(mockClient, mockWriter) gitRepo := &gitRepository{ client: mockClient, @@ -1174,6 +1182,10 @@ func TestGitRepository_Delete(t *testing.T) { } else { require.NoError(t, err) } + + if tt.assertions != nil { + tt.assertions(t, mockClient, mockWriter) + } }) } } @@ -1191,7 +1203,7 @@ func TestGitRepository_LatestRef(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, // Non-empty hash + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, // Non-empty hash }, nil) }, gitConfig: RepositoryConfig{ @@ -1412,11 +1424,11 @@ func TestGitRepository_CompareFiles(t *testing.T) { // Return refs for base and ref mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) // Return comparison results @@ -1449,11 +1461,11 @@ func TestGitRepository_CompareFiles(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{}, nil) @@ -1547,7 +1559,7 @@ func TestGitRepository_ensureBranchExists(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) }, gitConfig: RepositoryConfig{ @@ -1565,7 +1577,7 @@ func TestGitRepository_ensureBranchExists(t *testing.T) { // Second call - get source branch mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) // CreateRef succeeds mockClient.CreateRefReturns(nil) @@ -1729,57 +1741,21 @@ func TestGitRepository_createSignature(t *testing.T) { func TestNewGitRepository(t *testing.T) { tests := []struct { - name string - setupMock func(*mockSecretsService) - gitConfig RepositoryConfig - wantError bool - expectURL string - expectToken string + name string + gitConfig RepositoryConfig + wantError bool + expectURL string }{ { name: "success - with token", - setupMock: func(mockSecrets *mockSecretsService) { - // No setup needed for token case - }, gitConfig: RepositoryConfig{ URL: "https://git.example.com/owner/repo.git", Branch: "main", Token: "plain-token", Path: "configs", }, - wantError: false, - expectURL: "https://git.example.com/owner/repo.git", - expectToken: "plain-token", - }, - { - name: "success - with encrypted token", - setupMock: func(mockSecrets *mockSecretsService) { - // Mock will return decrypted token - }, - gitConfig: RepositoryConfig{ - URL: "https://git.example.com/owner/repo.git", - Branch: "main", - Token: "", // Empty token, will use encrypted - EncryptedToken: []byte("encrypted-token"), - Path: "configs", - }, - wantError: false, - expectURL: "https://git.example.com/owner/repo.git", - expectToken: "decrypted-token", // From mock - }, - { - name: "failure - decryption error", - setupMock: func(mockSecrets *mockSecretsService) { - // This test will use the separate mockSecretsServiceWithError - }, - gitConfig: RepositoryConfig{ - URL: "https://git.example.com/owner/repo.git", - Branch: "main", - Token: "", - EncryptedToken: []byte("bad-encrypted-token"), - Path: "configs", - }, - wantError: true, + wantError: false, + expectURL: "https://git.example.com/owner/repo.git", }, } @@ -1787,20 +1763,13 @@ func TestNewGitRepository(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - var mockSecrets secrets.Service - if tt.name == "failure - decryption error" { - mockSecrets = &mockSecretsServiceWithError{shouldError: true} - } else { - mockSecrets = &mockSecretsService{} - } - config := &provisioning.Repository{ Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, }, } - gitRepo, err := NewGitRepository(ctx, mockSecrets, config, tt.gitConfig) + gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig) if tt.wantError { require.Error(t, err) @@ -1862,7 +1831,7 @@ func TestGitRepository_resolveRefToHash(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) }, gitConfig: RepositoryConfig{ @@ -1887,7 +1856,7 @@ func TestGitRepository_resolveRefToHash(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) }, gitConfig: RepositoryConfig{ @@ -1939,7 +1908,7 @@ func TestGitRepository_resolveRefToHash(t *testing.T) { if tt.wantError { require.Error(t, err) - require.Nil(t, refHash) + require.Equal(t, hash.Zero, refHash) } else { require.NoError(t, err) require.NotNil(t, refHash) @@ -2168,7 +2137,7 @@ func TestGitRepository_logger(t *testing.T) { }) } -func TestGitRepository_Clone(t *testing.T) { +func TestGitRepository_Stage(t *testing.T) { gitRepo := &gitRepository{ config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -2184,9 +2153,8 @@ func TestGitRepository_Clone(t *testing.T) { t.Run("calls NewStagedGitRepository", func(t *testing.T) { ctx := context.Background() - opts := repository.CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, + opts := repository.StageOptions{ + PushOnWrites: true, } // Since NewStagedGitRepository is not mocked and may panic, we expect this to fail @@ -2198,11 +2166,11 @@ func TestGitRepository_Clone(t *testing.T) { } }() - cloned, err := gitRepo.Clone(ctx, opts) + staged, err := gitRepo.Stage(ctx, opts) // This will likely error/panic since we don't have a real implementation // but we're testing that the method exists and forwards to NewStagedGitRepository - _ = cloned + _ = staged _ = err }) } @@ -2212,7 +2180,7 @@ func TestGitRepository_EdgeCases(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockWriter := &mocks.FakeStagedWriter{} @@ -2251,7 +2219,7 @@ func TestGitRepository_EdgeCases(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetCommitReturns(&nanogit.Commit{Tree: hash.Hash{}}, nil) mockClient.GetBlobByPathReturns(&nanogit.Blob{}, errors.New("some read error")) @@ -2271,50 +2239,6 @@ func TestGitRepository_EdgeCases(t *testing.T) { }) } -// Enhanced secrets service mock with error handling -type mockSecretsServiceWithError struct { - shouldError bool -} - -func (m *mockSecretsServiceWithError) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - if m.shouldError { - return nil, errors.New("decryption failed") - } - return []byte("decrypted-token"), nil -} - -func (m *mockSecretsServiceWithError) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - if m.shouldError { - return nil, errors.New("encryption failed") - } - return []byte("encrypted-token"), nil -} - -func TestNewGitRepository_DecryptionError(t *testing.T) { - ctx := context.Background() - mockSecrets := &mockSecretsServiceWithError{shouldError: true} - - config := &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - }, - } - - gitConfig := RepositoryConfig{ - URL: "https://git.example.com/owner/repo.git", - Branch: "main", - Token: "", - EncryptedToken: []byte("encrypted-token"), - Path: "configs", - } - - gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig) - - require.Error(t, err) - require.Nil(t, gitRepo) - require.Contains(t, err.Error(), "decrypt token") -} - func TestGitRepository_ValidateBranchNames(t *testing.T) { tests := []struct { name string @@ -2401,7 +2325,7 @@ func TestGitRepository_ResolveRefToHash_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/invalid-hex", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) }, ref: "invalid-hex-zzz", @@ -2412,7 +2336,7 @@ func TestGitRepository_ResolveRefToHash_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) }, ref: "refs/heads/feature", @@ -2423,7 +2347,7 @@ func TestGitRepository_ResolveRefToHash_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/tags/v1.0.0", - Hash: hash.Hash{7, 8, 9}, + Hash: hash.Hash{7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, }, nil) }, ref: "refs/tags/v1.0.0", @@ -2540,11 +2464,11 @@ func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.CompareCommitsReturns(nil, errors.New("compare error")) }, @@ -2558,11 +2482,11 @@ func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -2580,11 +2504,11 @@ func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -2606,11 +2530,11 @@ func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -2683,7 +2607,7 @@ func TestGitRepository_ReadTree_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetFlatTreeReturns(nil, errors.New("flat tree error")) }, @@ -2694,23 +2618,23 @@ func TestGitRepository_ReadTree_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetFlatTreeReturns(&nanogit.FlatTree{ Entries: []nanogit.FlatTreeEntry{ { Path: "other/file.yaml", // Outside configs/ - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Type: protocol.ObjectTypeBlob, }, { Path: "configs/included.yaml", // Inside configs/ - Hash: hash.Hash{7, 8, 9}, + Hash: hash.Hash{7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, Type: protocol.ObjectTypeBlob, }, { Path: "configs/dir", // Directory without trailing slash - Hash: hash.Hash{10, 11, 12}, + Hash: hash.Hash{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}, Type: protocol.ObjectTypeTree, }, }, @@ -2782,7 +2706,6 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) { // This test would require mocking nanogit.NewHTTPClient which is difficult // We test the path where the client creation would fail by using invalid URL ctx := context.Background() - mockSecrets := &mockSecretsService{} config := &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -2797,7 +2720,7 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) { Path: "configs", } - gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig) + gitRepo, err := NewGitRepository(ctx, config, gitConfig) // We expect this to fail during client creation require.Error(t, err) @@ -2828,7 +2751,7 @@ func TestGitRepository_ensureBranchExists_ErrorConditions(t *testing.T) { // Second call - get source branch mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) // CreateRef fails mockClient.CreateRefReturns(errors.New("create ref failed")) @@ -2880,10 +2803,10 @@ func TestGitRepository_Read_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121315"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash([]byte("tree-hash")), + Tree: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetTreeByPathReturns(nil, errors.New("tree error")) }, @@ -2895,7 +2818,7 @@ func TestGitRepository_Read_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(nil, errors.New("commit error")) }, @@ -2907,10 +2830,10 @@ func TestGitRepository_Read_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708092a0b0c0d0e0f1011121314"), }, nil) mockClient.GetBlobByPathReturns(nil, errors.New("blob error")) }, @@ -2965,7 +2888,7 @@ func TestGitRepository_Create_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error")) }, @@ -2977,7 +2900,7 @@ func TestGitRepository_Create_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create blob error")) @@ -3030,7 +2953,7 @@ func TestGitRepository_Update_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error")) }, @@ -3042,7 +2965,7 @@ func TestGitRepository_Update_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update blob error")) @@ -3096,7 +3019,7 @@ func TestGitRepository_Delete_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error")) }, @@ -3109,7 +3032,7 @@ func TestGitRepository_Delete_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteBlobReturns(hash.Hash{}, errors.New("delete blob error")) @@ -3124,7 +3047,7 @@ func TestGitRepository_Delete_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteTreeReturns(hash.Hash{}, errors.New("delete tree error")) @@ -3139,7 +3062,7 @@ func TestGitRepository_Delete_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteTreeReturns(hash.Hash{}, nanogit.ErrObjectNotFound) @@ -3186,7 +3109,7 @@ func TestGitRepository_CompareFiles_EmptyBase(t *testing.T) { // Only setup for ref resolution mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -3218,8 +3141,8 @@ func TestGitRepository_CompareFiles_EmptyBase(t *testing.T) { // Verify CompareCommits was called with empty base hash and feature hash require.Equal(t, 1, mockClient.CompareCommitsCallCount()) _, baseHash, refHash := mockClient.CompareCommitsArgsForCall(0) - require.Nil(t, baseHash) // Empty hash for empty base - require.Equal(t, hash.Hash{4, 5, 6}, refHash) + require.Equal(t, hash.Zero, baseHash) // Empty hash for empty base + require.Equal(t, hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), refHash) } func TestGitRepository_EmptyRefHandling(t *testing.T) { @@ -3237,7 +3160,7 @@ func TestGitRepository_EmptyRefHandling(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} @@ -3299,7 +3222,7 @@ func TestGitRepository_CompareFiles_ResolveErrors(t *testing.T) { // First call succeeds for base mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) // Second call fails for ref mockClient.GetRefReturnsOnCall(1, nanogit.Ref{}, errors.New("ref error")) @@ -3341,14 +3264,14 @@ func TestGitRepository_Read_EmptyRef(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash{4, 5, 6}, + Tree: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.GetBlobByPathReturns(&nanogit.Blob{ Content: []byte("file content"), - Hash: hash.Hash{7, 8, 9}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) gitRepo := &gitRepository{ @@ -3376,13 +3299,13 @@ func TestGitRepository_ReadTree_EmptyRef(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetFlatTreeReturns(&nanogit.FlatTree{ Entries: []nanogit.FlatTreeEntry{ { Path: "configs/test.yaml", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), Type: protocol.ObjectTypeBlob, }, }, @@ -3504,17 +3427,17 @@ func TestGitRepository_Write_DefaultRef(t *testing.T) { // First call for Read check - file not found mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetBlobByPathReturns(&nanogit.Blob{}, nanogit.ErrObjectNotFound) // Second call for Create mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.CreateBlobReturns(hash.Hash{}, nil) @@ -3545,14 +3468,14 @@ func TestGitRepository_Read_RefInFileInfo(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetBlobByPathReturns(&nanogit.Blob{ Content: []byte("file content"), - Hash: hash.Hash{7, 8, 9}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) gitRepo := &gitRepository{ @@ -3582,10 +3505,10 @@ func TestGitRepository_Read_GetTreeByPath_NotFound(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash([]byte("tree-hash")), + Tree: hash.Hash{30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49}, }, nil) mockClient.GetTreeByPathReturns(nil, nanogit.ErrObjectNotFound) @@ -3614,7 +3537,7 @@ func TestGitRepository_ReadTree_GetFlatTree_NotFound(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetFlatTreeReturns(nil, nanogit.ErrObjectNotFound) @@ -3655,11 +3578,11 @@ func TestGitRepository_CompareFiles_FilesOutsideConfiguredPath_AllStatuses(t *te mockClient := &mocks.FakeClient{} mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121315"), }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { diff --git a/pkg/registry/apis/provisioning/repository/nanogit/staged.go b/pkg/registry/apis/provisioning/repository/git/staged.go similarity index 86% rename from pkg/registry/apis/provisioning/repository/nanogit/staged.go rename to pkg/registry/apis/provisioning/repository/git/staged.go index d383e5b47e3..e11044230b2 100644 --- a/pkg/registry/apis/provisioning/repository/nanogit/staged.go +++ b/pkg/registry/apis/provisioning/repository/git/staged.go @@ -1,4 +1,4 @@ -package nanogit +package git import ( "context" @@ -15,17 +15,11 @@ import ( // once that happens we could do more magic here. type stagedGitRepository struct { *gitRepository - opts repository.CloneOptions + opts repository.StageOptions writer nanogit.StagedWriter } -func NewStagedGitRepository(ctx context.Context, repo *gitRepository, opts repository.CloneOptions) (repository.ClonedRepository, error) { - if opts.BeforeFn != nil { - if err := opts.BeforeFn(); err != nil { - return nil, err - } - } - +func NewStagedGitRepository(ctx context.Context, repo *gitRepository, opts repository.StageOptions) (repository.StagedRepository, error) { if opts.Timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, opts.Timeout) @@ -89,28 +83,35 @@ func (r *stagedGitRepository) Create(ctx context.Context, path, ref string, data } if r.opts.PushOnWrites { - return r.Push(ctx, repository.PushOptions{}) + return r.Push(ctx) } return nil } +func (r *stagedGitRepository) blobExists(ctx context.Context, path string) (bool, error) { + if r.gitConfig.Path != "" { + path = safepath.Join(r.gitConfig.Path, path) + } + return r.writer.BlobExists(ctx, path) +} + func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data []byte, message string) error { if ref != "" && ref != r.gitConfig.Branch { return errors.New("ref is not supported for staged repository") } - ok, err := r.writer.BlobExists(ctx, path) + exists, err := r.blobExists(ctx, path) if err != nil { return fmt.Errorf("check if file exists: %w", err) } - if !ok { - if err := r.create(ctx, path, data, r.writer); err != nil { + if exists { + if err := r.update(ctx, path, data, r.writer); err != nil { return err } } else { - if err := r.update(ctx, path, data, r.writer); err != nil { + if err := r.create(ctx, path, data, r.writer); err != nil { return err } } @@ -120,7 +121,7 @@ func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data } if r.opts.PushOnWrites { - return r.Push(ctx, repository.PushOptions{}) + return r.Push(ctx) } return nil @@ -144,7 +145,7 @@ func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data } if r.opts.PushOnWrites { - return r.Push(ctx, repository.PushOptions{}) + return r.Push(ctx) } return nil @@ -164,22 +165,16 @@ func (r *stagedGitRepository) Delete(ctx context.Context, path, ref, message str } if r.opts.PushOnWrites { - return r.Push(ctx, repository.PushOptions{}) + return r.Push(ctx) } return nil } -func (r *stagedGitRepository) Push(ctx context.Context, opts repository.PushOptions) error { - if opts.BeforeFn != nil { - if err := opts.BeforeFn(); err != nil { - return err - } - } - - if opts.Timeout > 0 { +func (r *stagedGitRepository) Push(ctx context.Context) error { + if r.opts.Timeout > 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, opts.Timeout) + ctx, cancel = context.WithTimeout(ctx, r.opts.Timeout) defer cancel() } diff --git a/pkg/registry/apis/provisioning/repository/nanogit/staged_test.go b/pkg/registry/apis/provisioning/repository/git/staged_test.go similarity index 88% rename from pkg/registry/apis/provisioning/repository/nanogit/staged_test.go rename to pkg/registry/apis/provisioning/repository/git/staged_test.go index 77f63016a66..9dc502cf8c2 100644 --- a/pkg/registry/apis/provisioning/repository/nanogit/staged_test.go +++ b/pkg/registry/apis/provisioning/repository/git/staged_test.go @@ -1,4 +1,4 @@ -package nanogit +package git import ( "context" @@ -18,7 +18,7 @@ func TestNewStagedGitRepository(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeClient) - opts repository.CloneOptions + opts repository.StageOptions wantError error }{ { @@ -31,9 +31,8 @@ func TestNewStagedGitRepository(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} mockClient.NewStagedWriterReturns(mockWriter, nil) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, + opts: repository.StageOptions{ + PushOnWrites: false, }, wantError: nil, }, @@ -47,12 +46,8 @@ func TestNewStagedGitRepository(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} mockClient.NewStagedWriterReturns(mockWriter, nil) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, - BeforeFn: func() error { - return nil - }, + opts: repository.StageOptions{ + PushOnWrites: false, }, wantError: nil, }, @@ -66,33 +61,19 @@ func TestNewStagedGitRepository(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} mockClient.NewStagedWriterReturns(mockWriter, nil) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, - Timeout: time.Second * 5, + opts: repository.StageOptions{ + PushOnWrites: false, + Timeout: time.Second * 5, }, wantError: nil, }, - { - name: "fails with BeforeFn error", - setupMock: func(mockClient *mocks.FakeClient) { - // No setup needed as BeforeFn fails first - }, - opts: repository.CloneOptions{ - BeforeFn: func() error { - return errors.New("before function failed") - }, - }, - wantError: errors.New("before function failed"), - }, { name: "fails with GetRef error", setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{}, errors.New("ref not found")) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, + opts: repository.StageOptions{ + PushOnWrites: false, }, wantError: errors.New("ref not found"), }, @@ -105,9 +86,8 @@ func TestNewStagedGitRepository(t *testing.T) { }, nil) mockClient.NewStagedWriterReturns(nil, errors.New("failed to create writer")) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, + opts: repository.StageOptions{ + PushOnWrites: false, }, wantError: errors.New("build staged writer: failed to create writer"), }, @@ -141,13 +121,8 @@ func TestNewStagedGitRepository(t *testing.T) { // Compare opts fields individually since function pointers can't be compared directly actualOpts := stagedRepo.(*stagedGitRepository).opts - require.Equal(t, tt.opts.CreateIfNotExists, actualOpts.CreateIfNotExists) require.Equal(t, tt.opts.PushOnWrites, actualOpts.PushOnWrites) - require.Equal(t, tt.opts.MaxSize, actualOpts.MaxSize) require.Equal(t, tt.opts.Timeout, actualOpts.Timeout) - require.Equal(t, tt.opts.Progress, actualOpts.Progress) - // BeforeFn is a function pointer, so we just check if both are nil or both are not nil - require.Equal(t, tt.opts.BeforeFn == nil, actualOpts.BeforeFn == nil) } }) } @@ -287,7 +262,7 @@ func TestStagedGitRepository_Create(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.CloneOptions + opts repository.StageOptions path string ref string data []byte @@ -301,7 +276,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -318,7 +293,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "test.yaml", @@ -333,7 +308,7 @@ func TestStagedGitRepository_Create(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -347,7 +322,7 @@ func TestStagedGitRepository_Create(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create blob failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -362,7 +337,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -378,7 +353,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(errors.New("push failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "test.yaml", @@ -419,7 +394,7 @@ func TestStagedGitRepository_Write(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.CloneOptions + opts repository.StageOptions path string ref string data []byte @@ -435,7 +410,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -454,7 +429,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "test.yaml", @@ -470,7 +445,7 @@ func TestStagedGitRepository_Write(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -484,7 +459,7 @@ func TestStagedGitRepository_Write(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.BlobExistsReturns(false, errors.New("blob exists check failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -499,7 +474,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.BlobExistsReturns(false, nil) mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -514,7 +489,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.BlobExistsReturns(true, nil) mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -530,7 +505,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -570,7 +545,7 @@ func TestStagedGitRepository_Update(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.CloneOptions + opts repository.StageOptions path string ref string data []byte @@ -584,7 +559,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -601,7 +576,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "test.yaml", @@ -616,7 +591,7 @@ func TestStagedGitRepository_Update(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -630,7 +605,7 @@ func TestStagedGitRepository_Update(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "directory/", @@ -644,7 +619,7 @@ func TestStagedGitRepository_Update(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update blob failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -659,7 +634,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -699,7 +674,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.CloneOptions + opts repository.StageOptions path string ref string message string @@ -712,7 +687,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.DeleteBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -728,7 +703,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "testdir/", @@ -742,7 +717,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -755,7 +730,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.DeleteBlobReturns(hash.Hash{}, errors.New("delete blob failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -769,7 +744,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.DeleteBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -808,7 +783,6 @@ func TestStagedGitRepository_Push(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.PushOptions wantError error expectCalls int }{ @@ -817,7 +791,6 @@ func TestStagedGitRepository_Push(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(nil) }, - opts: repository.PushOptions{}, wantError: nil, expectCalls: 1, }, @@ -826,11 +799,6 @@ func TestStagedGitRepository_Push(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(nil) }, - opts: repository.PushOptions{ - BeforeFn: func() error { - return nil - }, - }, wantError: nil, expectCalls: 1, }, @@ -839,31 +807,14 @@ func TestStagedGitRepository_Push(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(nil) }, - opts: repository.PushOptions{ - Timeout: time.Second * 5, - }, wantError: nil, expectCalls: 1, }, - { - name: "fails with before fn error", - setupMock: func(_ *mocks.FakeStagedWriter) { - // No setup needed as BeforeFn fails first - }, - opts: repository.PushOptions{ - BeforeFn: func() error { - return errors.New("before function failed") - }, - }, - wantError: errors.New("before function failed"), - expectCalls: 0, - }, { name: "fails with push error", setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(errors.New("push failed")) }, - opts: repository.PushOptions{}, wantError: errors.New("push failed"), expectCalls: 1, }, @@ -874,9 +825,9 @@ func TestStagedGitRepository_Push(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} tt.setupMock(mockWriter) - stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{}) + stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{}) - err := stagedRepo.Push(context.Background(), tt.opts) + err := stagedRepo.Push(context.Background()) if tt.wantError != nil { require.EqualError(t, err, tt.wantError.Error()) @@ -892,7 +843,7 @@ func TestStagedGitRepository_Push(t *testing.T) { func TestStagedGitRepository_Remove(t *testing.T) { t.Run("succeeds with remove", func(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} - stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{}) + stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{}) err := stagedRepo.Remove(context.Background()) require.NoError(t, err) @@ -904,10 +855,10 @@ func TestStagedGitRepository_Remove(t *testing.T) { func createTestStagedRepository(mockClient *mocks.FakeClient) *stagedGitRepository { mockWriter := &mocks.FakeStagedWriter{} - return createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{}, mockClient) + return createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{}, mockClient) } -func createTestStagedRepositoryWithWriter(mockWriter *mocks.FakeStagedWriter, opts repository.CloneOptions, mockClient ...*mocks.FakeClient) *stagedGitRepository { +func createTestStagedRepositoryWithWriter(mockWriter *mocks.FakeStagedWriter, opts repository.StageOptions, mockClient ...*mocks.FakeClient) *stagedGitRepository { var client nanogit.Client if len(mockClient) > 0 { client = mockClient[0] diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go deleted file mode 100644 index 42c78e55c8e..00000000000 --- a/pkg/registry/apis/provisioning/repository/github.go +++ /dev/null @@ -1,683 +0,0 @@ -package repository - -import ( - "context" - "errors" - "fmt" - "log/slog" - "net/http" - "net/url" - "regexp" - "strings" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/validation/field" - - "github.com/grafana/grafana-app-sdk/logging" - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" -) - -// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included. -type githubRepository struct { - config *provisioning.Repository - gh pgh.Client // assumes github.com base URL - secrets secrets.Service - - owner string - repo string - - cloneFn CloneFn -} - -// GithubRepository is an interface that combines all repository capabilities -// needed for GitHub repositories. - -//go:generate mockery --name GithubRepository --structname MockGithubRepository --inpackage --filename github_repository_mock.go --with-expecter -type GithubRepository interface { - Repository - Versioned - Writer - Reader - RepositoryWithURLs - ClonableRepository - Owner() string - Repo() string - Client() pgh.Client -} - -func NewGitHub( - ctx context.Context, - config *provisioning.Repository, - factory *pgh.Factory, - secrets secrets.Service, - cloneFn CloneFn, -) (GithubRepository, error) { - owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL) - if err != nil { - return nil, fmt.Errorf("parse owner and repo: %w", err) - } - - token := config.Spec.GitHub.Token - if token == "" { - decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken) - if err != nil { - return nil, fmt.Errorf("decrypt token: %w", err) - } - token = string(decrypted) - } - - return &githubRepository{ - config: config, - gh: factory.New(ctx, token), // TODO, baseURL from config - secrets: secrets, - owner: owner, - repo: repo, - cloneFn: cloneFn, - }, nil -} - -func (r *githubRepository) Config() *provisioning.Repository { - return r.config -} - -func (r *githubRepository) Owner() string { - return r.owner -} - -func (r *githubRepository) Repo() string { - return r.repo -} - -func (r *githubRepository) Client() pgh.Client { - return r.gh -} - -// Validate implements provisioning.Repository. -func (r *githubRepository) Validate() (list field.ErrorList) { - gh := r.config.Spec.GitHub - if gh == nil { - list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required")) - return list - } - if gh.URL == "" { - list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required")) - } else { - _, _, err := ParseOwnerRepoGithub(gh.URL) - if err != nil { - list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error())) - } else if !strings.HasPrefix(gh.URL, "https://github.com/") { - list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/")) - } - } - if gh.Branch == "" { - list = append(list, field.Required(field.NewPath("spec", "github", "branch"), "a github branch is required")) - } else if !IsValidGitBranchName(gh.Branch) { - list = append(list, field.Invalid(field.NewPath("spec", "github", "branch"), gh.Branch, "invalid branch name")) - } - // TODO: Use two fields for token - if gh.Token == "" && len(gh.EncryptedToken) == 0 { - list = append(list, field.Required(field.NewPath("spec", "github", "token"), "a github access token is required")) - } - - if err := safepath.IsSafe(gh.Path); err != nil { - list = append(list, field.Invalid(field.NewPath("spec", "github", "prefix"), gh.Path, err.Error())) - } - - if safepath.IsAbs(gh.Path) { - list = append(list, field.Invalid(field.NewPath("spec", "github", "prefix"), gh.Path, "path must be relative")) - } - - return list -} - -func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) { - parsed, e := url.Parse(strings.TrimSuffix(giturl, ".git")) - if e != nil { - err = e - return - } - parts := strings.Split(parsed.Path, "/") - if len(parts) < 3 { - err = fmt.Errorf("unable to parse repo+owner from url") - return - } - return parts[1], parts[2], nil -} - -// Test implements provisioning.Repository. -func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { - if err := r.gh.IsAuthenticated(ctx); err != nil { - return &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueInvalid, - Field: field.NewPath("spec", "github", "token").String(), - Detail: err.Error(), - }}}, nil - } - - url := r.config.Spec.GitHub.URL - owner, repo, err := ParseOwnerRepoGithub(url) - if err != nil { - return fromFieldError(field.Invalid( - field.NewPath("spec", "github", "url"), url, err.Error())), nil - } - - // FIXME: check token permissions - ok, err := r.gh.RepoExists(ctx, owner, repo) - if err != nil { - return fromFieldError(field.Invalid( - field.NewPath("spec", "github", "url"), url, err.Error())), nil - } - - if !ok { - return fromFieldError(field.NotFound( - field.NewPath("spec", "github", "url"), url)), nil - } - - branch := r.config.Spec.GitHub.Branch - ok, err = r.gh.BranchExists(ctx, r.owner, r.repo, branch) - if err != nil { - return fromFieldError(field.Invalid( - field.NewPath("spec", "github", "branch"), branch, err.Error())), nil - } - - if !ok { - return fromFieldError(field.NotFound( - field.NewPath("spec", "github", "branch"), branch)), nil - } - - return &provisioning.TestResults{ - Code: http.StatusOK, - Success: true, - }, nil -} - -// ReadResource implements provisioning.Repository. -func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*FileInfo, error) { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - - finalPath := safepath.Join(r.config.Spec.GitHub.Path, filePath) - content, dirContent, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return nil, ErrFileNotFound - } - - return nil, fmt.Errorf("get contents: %w", err) - } - if dirContent != nil { - return &FileInfo{ - Path: filePath, - Ref: ref, - }, nil - } - - data, err := content.GetFileContent() - if err != nil { - return nil, fmt.Errorf("get content: %w", err) - } - return &FileInfo{ - Path: filePath, - Ref: ref, - Data: []byte(data), - Hash: content.GetSHA(), - }, nil -} - -func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - - ctx, _ = r.logger(ctx, ref) - tree, truncated, err := r.gh.GetTree(ctx, r.owner, r.repo, r.config.Spec.GitHub.Path, ref, true) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return nil, &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: fmt.Sprintf("tree not found; ref=%s", ref), - Code: http.StatusNotFound, - }, - } - } - return nil, fmt.Errorf("get tree: %w", err) - } - - if truncated { - return nil, fmt.Errorf("tree truncated") - } - - entries := make([]FileTreeEntry, 0, len(tree)) - for _, entry := range tree { - isBlob := !entry.IsDirectory() - // FIXME: this we could potentially do somewhere else on in a different way - filePath := entry.GetPath() - if !isBlob && !safepath.IsDir(filePath) { - filePath = filePath + "/" - } - - converted := FileTreeEntry{ - Path: filePath, - Size: entry.GetSize(), - Hash: entry.GetSHA(), - Blob: !entry.IsDirectory(), - } - entries = append(entries, converted) - } - return entries, nil -} - -func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - ctx, _ = r.logger(ctx, ref) - - if err := r.ensureBranchExists(ctx, ref); err != nil { - return err - } - - finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) - - // Create .keep file if it is a directory - if safepath.IsDir(finalPath) { - if data != nil { - return apierrors.NewBadRequest("data cannot be provided for a directory") - } - - finalPath = safepath.Join(finalPath, ".keep") - data = []byte{} - } - - err := r.gh.CreateFile(ctx, r.owner, r.repo, finalPath, ref, comment, data) - if errors.Is(err, pgh.ErrResourceAlreadyExists) { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "file already exists", - Code: http.StatusConflict, - }, - } - } - - return err -} - -func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - ctx, _ = r.logger(ctx, ref) - - if err := r.ensureBranchExists(ctx, ref); err != nil { - return err - } - - finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) - file, _, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "file not found", - Code: http.StatusNotFound, - }, - } - } - - return fmt.Errorf("get content before file update: %w", err) - } - if file.IsDirectory() { - return apierrors.NewBadRequest("cannot update a directory") - } - - if err := r.gh.UpdateFile(ctx, r.owner, r.repo, finalPath, ref, comment, file.GetSHA(), data); err != nil { - return fmt.Errorf("update file: %w", err) - } - return nil -} - -func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - - ctx, _ = r.logger(ctx, ref) - _, err := r.Read(ctx, path, ref) - if err != nil && !(errors.Is(err, ErrFileNotFound)) { - return fmt.Errorf("check if file exists before writing: %w", err) - } - if err == nil { - return r.Update(ctx, path, ref, data, message) - } - - return r.Create(ctx, path, ref, data, message) -} - -func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - ctx, _ = r.logger(ctx, ref) - - if err := r.ensureBranchExists(ctx, ref); err != nil { - return err - } - - // TODO: should add some protection against deleting the root directory? - - // Inside deleteRecursively, all paths are relative to the root of the repository - // so we need to prepend the prefix there but only here. - finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) - - return r.deleteRecursively(ctx, finalPath, ref, comment) -} - -func (r *githubRepository) deleteRecursively(ctx context.Context, path, ref, comment string) error { - file, contents, err := r.gh.GetContents(ctx, r.owner, r.repo, path, ref) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return ErrFileNotFound - } - - return fmt.Errorf("find file to delete: %w", err) - } - - if file != nil && !file.IsDirectory() { - return r.gh.DeleteFile(ctx, r.owner, r.repo, path, ref, comment, file.GetSHA()) - } - - for _, c := range contents { - p := c.GetPath() - if c.IsDirectory() { - if err := r.deleteRecursively(ctx, p, ref, comment); err != nil { - return fmt.Errorf("delete directory recursively: %w", err) - } - continue - } - - if err := r.gh.DeleteFile(ctx, r.owner, r.repo, p, ref, comment, c.GetSHA()); err != nil { - return fmt.Errorf("delete file: %w", err) - } - } - - return nil -} - -func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - ctx, _ = r.logger(ctx, ref) - - finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) - commits, err := r.gh.Commits(ctx, r.owner, r.repo, finalPath, ref) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return nil, ErrFileNotFound - } - - return nil, fmt.Errorf("get commits: %w", err) - } - - ret := make([]provisioning.HistoryItem, 0, len(commits)) - for _, commit := range commits { - authors := make([]provisioning.Author, 0) - if commit.Author != nil { - authors = append(authors, provisioning.Author{ - Name: commit.Author.Name, - Username: commit.Author.Username, - AvatarURL: commit.Author.AvatarURL, - }) - } - - if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name { - authors = append(authors, provisioning.Author{ - Name: commit.Committer.Name, - Username: commit.Committer.Username, - AvatarURL: commit.Committer.AvatarURL, - }) - } - - ret = append(ret, provisioning.HistoryItem{ - Ref: commit.Ref, - Message: commit.Message, - Authors: authors, - CreatedAt: commit.CreatedAt.UnixMilli(), - }) - } - - return ret, nil -} - -// basicGitBranchNameRegex is a regular expression to validate a git branch name -// it does not cover all cases as positive lookaheads are not supported in Go's regexp -var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`) - -// IsValidGitBranchName checks if a branch name is valid. -// It uses the following regexp `^[a-zA-Z0-9\-\_\/\.]+$` to validate the branch name with some additional checks that must satisfy the following rules: -// 1. The branch name must have at least one character and must not be empty. -// 2. The branch name cannot start with `/` or end with `/`, `.`, or whitespace. -// 3. The branch name cannot contain consecutive slashes (`//`). -// 4. The branch name cannot contain consecutive dots (`..`). -// 5. The branch name cannot contain `@{`. -// 6. The branch name cannot include the following characters: `~`, `^`, `:`, `?`, `*`, `[`, `\`, or `]`. -func IsValidGitBranchName(branch string) bool { - if !basicGitBranchNameRegex.MatchString(branch) { - return false - } - - // Additional checks for invalid patterns - if strings.HasPrefix(branch, "/") || strings.HasSuffix(branch, "/") || - strings.HasSuffix(branch, ".") || strings.Contains(branch, "..") || - strings.Contains(branch, "//") || strings.HasSuffix(branch, ".lock") { - return false - } - - return true -} - -func (r *githubRepository) ensureBranchExists(ctx context.Context, branchName string) error { - if !IsValidGitBranchName(branchName) { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusBadRequest, - Message: "invalid branch name", - }, - } - } - - ok, err := r.gh.BranchExists(ctx, r.owner, r.repo, branchName) - if err != nil { - return fmt.Errorf("check branch exists: %w", err) - } - - if ok { - logging.FromContext(ctx).Info("branch already exists", "branch", branchName) - - return nil - } - - srcBranch := r.config.Spec.GitHub.Branch - if err := r.gh.CreateBranch(ctx, r.owner, r.repo, srcBranch, branchName); err != nil { - if errors.Is(err, pgh.ErrResourceAlreadyExists) { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusConflict, - Message: "branch already exists", - }, - } - } - - return fmt.Errorf("create branch: %w", err) - } - - return nil -} - -func (r *githubRepository) LatestRef(ctx context.Context) (string, error) { - ctx, _ = r.logger(ctx, "") - branch, err := r.gh.GetBranch(ctx, r.owner, r.repo, r.Config().Spec.GitHub.Branch) - if err != nil { - return "", fmt.Errorf("get branch: %w", err) - } - - return branch.Sha, nil -} - -func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]VersionedFileChange, error) { - if ref == "" { - var err error - ref, err = r.LatestRef(ctx) - if err != nil { - return nil, fmt.Errorf("get latest ref: %w", err) - } - } - ctx, logger := r.logger(ctx, ref) - - files, err := r.gh.CompareCommits(ctx, r.owner, r.repo, base, ref) - if err != nil { - return nil, fmt.Errorf("compare commits: %w", err) - } - - changes := make([]VersionedFileChange, 0) - for _, f := range files { - // reference: https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit - switch f.GetStatus() { - case "added", "copied": - currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) - if err != nil { - // do nothing as it's outside of configured path - continue - } - - changes = append(changes, VersionedFileChange{ - Path: currentPath, - Ref: ref, - Action: FileActionCreated, - }) - case "modified", "changed": - currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) - if err != nil { - // do nothing as it's outside of configured path - continue - } - - changes = append(changes, VersionedFileChange{ - Path: currentPath, - Ref: ref, - Action: FileActionUpdated, - }) - case "renamed": - previousPath, previousErr := safepath.RelativeTo(f.GetPreviousFilename(), r.config.Spec.GitHub.Path) - currentPath, currentErr := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) - - // Handle all possible combinations of path validation results: - // 1. Both paths outside configured path, do nothing - // 2. Both paths inside configured path, rename - // 3. Moving out of configured path, delete previous file - // 4. Moving into configured path, create new file - switch { - case previousErr != nil && currentErr != nil: - // do nothing as it's outside of configured path - case previousErr == nil && currentErr == nil: - changes = append(changes, VersionedFileChange{ - Path: currentPath, - PreviousPath: previousPath, - Ref: ref, - PreviousRef: base, - Action: FileActionRenamed, - }) - case previousErr == nil && currentErr != nil: - changes = append(changes, VersionedFileChange{ - Path: previousPath, - Ref: base, - Action: FileActionDeleted, - }) - case previousErr != nil && currentErr == nil: - changes = append(changes, VersionedFileChange{ - Path: currentPath, - Ref: ref, - Action: FileActionCreated, - }) - } - case "removed": - currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) - if err != nil { - // do nothing as it's outside of configured path - continue - } - - changes = append(changes, VersionedFileChange{ - Ref: ref, - PreviousRef: base, - Path: currentPath, - PreviousPath: currentPath, - Action: FileActionDeleted, - }) - case "unchanged": - // do nothing - default: - logger.Error("ignore unhandled file", "file", f.GetFilename(), "status", f.GetStatus()) - } - } - - return changes, nil -} - -// ResourceURLs implements RepositoryWithURLs. -func (r *githubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*provisioning.ResourceURLs, error) { - cfg := r.config.Spec.GitHub - if file.Path == "" || cfg == nil { - return nil, nil - } - - ref := file.Ref - if ref == "" { - ref = cfg.Branch - } - - urls := &provisioning.ResourceURLs{ - RepositoryURL: cfg.URL, - SourceURL: fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path), - } - - if ref != cfg.Branch { - urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref) - - // Create a new pull request - urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL) - } - - return urls, nil -} - -func (r *githubRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - return r.cloneFn(ctx, opts) -} - -func (r *githubRepository) logger(ctx context.Context, ref string) (context.Context, logging.Logger) { - logger := logging.FromContext(ctx) - - type containsGh int - var containsGhKey containsGh - if ctx.Value(containsGhKey) != nil { - return ctx, logging.FromContext(ctx) - } - - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - logger = logger.With(slog.Group("github_repository", "owner", r.owner, "name", r.repo, "ref", ref)) - ctx = logging.Context(ctx, logger) - // We want to ensure we don't add multiple github_repository keys. With doesn't deduplicate the keys... - ctx = context.WithValue(ctx, containsGhKey, true) - return ctx, logger -} diff --git a/pkg/registry/apis/provisioning/repository/github/client.go b/pkg/registry/apis/provisioning/repository/github/client.go index 9c35ca3898a..5b21a5a4f1e 100644 --- a/pkg/registry/apis/provisioning/repository/github/client.go +++ b/pkg/registry/apis/provisioning/repository/github/client.go @@ -7,127 +7,34 @@ import ( "errors" "time" - "github.com/google/go-github/v70/github" apierrors "k8s.io/apimachinery/pkg/api/errors" ) // API errors that we need to convey after parsing real GH errors (or faking them). var ( - ErrResourceAlreadyExists = errors.New("the resource already exists") - ErrResourceNotFound = errors.New("the resource does not exist") - ErrMismatchedHash = errors.New("the update cannot be applied because the expected and actual hashes are unequal") - ErrNoSecret = errors.New("new webhooks must have a secret") + ErrResourceNotFound = errors.New("the resource does not exist") //lint:ignore ST1005 this is not punctuation - ErrPathTraversalDisallowed = errors.New("the path contained ..") //nolint:staticcheck - ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable") - ErrFileTooLarge = errors.New("file exceeds maximum allowed size") - ErrTooManyItems = errors.New("maximum number of items exceeded") + ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable") + ErrTooManyItems = errors.New("maximum number of items exceeded") ) -// MaxFileSize maximum file size limit (10MB) -const MaxFileSize = 10 * 1024 * 1024 // 10MB in bytes - -type ErrRateLimited = github.RateLimitError - //go:generate mockery --name Client --structname MockClient --inpackage --filename mock_client.go --with-expecter type Client interface { - // IsAuthenticated checks if the client is authenticated. - IsAuthenticated(ctx context.Context) error - - // GetContents returns the metadata and content of a file or directory. - // When a file is checked, the first returned value will have a value. For a directory, the second will. The other value is always nil. - // If an error occurs, the returned values may or may not be nil. - // - // If ".." appears in the "path", this method will return an error. - GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) - - // GetTree returns the Git tree in the repository. - // When recursive is given, subtrees are mapped into the returned array. - // When basePath is given, only trees under it are given. The results do not include this path in their names. - // - // The truncated bool will be set to true if the tree is larger than 7 MB or 100 000 entries. - // When truncated is true, you may wish to read each subtree manually instead. - GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) (entries []RepositoryContent, truncated bool, err error) - - // CreateFile creates a new file in the repository under the given path. - // The file is created on the branch given. - // The message given is the commit message. If none is given, an appropriate default is used. - // The content is what the file should contain. An empty slice is valid, though often not very useful. - // - // If ".." appears in the "path", this method will return an error. - CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error - - // UpdateFile updates a file in the repository under the given path. - // The file is updated on the branch given. - // The message given is the commit message. If none is given, an appropriate default is used. - // The content is what the file should contain. An empty slice is valid, though often not very useful. - // If the path does not exist, an error is returned. - // The hash given must be the SHA hash of the file contents. Calling GetContents in advance is an easy way of handling this. - // - // If ".." appears in the "path", this method will return an error. - UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error - - // DeleteFile deletes a file in the repository under the given path. - // The file is deleted from the branch given. - // The message given is the commit message. If none is given, an appropriate default is used. - // If the path does not exist, an error is returned. - // The hash given must be the SHA hash of the file contents. Calling GetContents in advance is an easy way of handling this. - // - // If ".." appears in the "path", this method will return an error. - DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error - - // Commits returns the commits for the given path + // Commits Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) - // CompareCommits returns the changes between two commits. - CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) - - // RepoExists checks if a repository exists. - RepoExists(ctx context.Context, owner, repository string) (bool, error) - - // CreateBranch creates a new branch in the repository. - CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error - // BranchExists checks if a branch exists in the repository. - BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) - // GetBranch returns the branch of the repository. - GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) - + // Webhooks ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) CreateWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) (WebhookConfig, error) GetWebhook(ctx context.Context, owner, repository string, webhookID int64) (WebhookConfig, error) DeleteWebhook(ctx context.Context, owner, repository string, webhookID int64) error EditWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) error + // Pull requests ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error) CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error } -//go:generate mockery --name RepositoryContent --structname MockRepositoryContent --inpackage --filename mock_repository_content.go --with-expecter -type RepositoryContent interface { - // Returns true if this is a directory, false if it is a file. - IsDirectory() bool - // Returns the contents of the file. Decoding happens if necessary. - // Returns an error if the content represents a directory. - GetFileContent() (string, error) - // Returns true if this is a symlink. - // If true, GetPath returns the path where this symlink leads. - IsSymlink() bool - // Returns the full path from the root of the repository. - // This has no leading or trailing slashes. - // The path only uses '/' for directories. You can use the 'path' package to interact with these. - GetPath() string - // Get the SHA hash. This is usually a SHA-256, but may also be SHA-512. - // Directories have SHA hashes, too (TODO: how is this calculated?). - GetSHA() string - // The size of the file. Not necessarily non-zero, even if the file is supposed to be non-zero. - GetSize() int64 -} - -type Branch struct { - Name string - Sha string -} - type CommitAuthor struct { Name string Username string @@ -150,20 +57,6 @@ type CommitFile interface { GetStatus() string } -type FileComment struct { - Content string - Path string - Position int - Ref string -} - -type CreateFileOptions struct { - // The message of the commit. May be empty, in which case a default value is entered. - Message string - // The content of the file to write, unencoded. - Content []byte -} - type WebhookConfig struct { // The ID of the webhook. // Can be 0 on creation. diff --git a/pkg/registry/apis/provisioning/repository/github/factory.go b/pkg/registry/apis/provisioning/repository/github/factory.go index b143cd7ad74..da2d50a6131 100644 --- a/pkg/registry/apis/provisioning/repository/github/factory.go +++ b/pkg/registry/apis/provisioning/repository/github/factory.go @@ -27,6 +27,11 @@ func (r *Factory) New(ctx context.Context, ghToken string) Client { tokenSrc := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: ghToken}, ) - tokenClient := oauth2.NewClient(ctx, tokenSrc) - return NewClient(github.NewClient(tokenClient)) + + if len(ghToken) == 0 { + tokenClient := oauth2.NewClient(ctx, tokenSrc) + return NewClient(github.NewClient(tokenClient)) + } + + return NewClient(github.NewClient(&http.Client{})) } diff --git a/pkg/registry/apis/provisioning/repository/github_repository_mock.go b/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go similarity index 86% rename from pkg/registry/apis/provisioning/repository/github_repository_mock.go rename to pkg/registry/apis/provisioning/repository/github/github_repository_mock.go index 8d4dd2d58ff..c40a23d709d 100644 --- a/pkg/registry/apis/provisioning/repository/github_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go @@ -1,14 +1,14 @@ // Code generated by mockery v2.52.4. DO NOT EDIT. -package repository +package github import ( context "context" - github "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + mock "github.com/stretchr/testify/mock" field "k8s.io/apimachinery/pkg/util/validation/field" - mock "github.com/stretchr/testify/mock" + repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" ) @@ -27,19 +27,19 @@ func (_m *MockGithubRepository) EXPECT() *MockGithubRepository_Expecter { } // Client provides a mock function with no fields -func (_m *MockGithubRepository) Client() github.Client { +func (_m *MockGithubRepository) Client() Client { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Client") } - var r0 github.Client - if rf, ok := ret.Get(0).(func() github.Client); ok { + var r0 Client + if rf, ok := ret.Get(0).(func() Client); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(github.Client) + r0 = ret.Get(0).(Client) } } @@ -63,93 +63,34 @@ func (_c *MockGithubRepository_Client_Call) Run(run func()) *MockGithubRepositor return _c } -func (_c *MockGithubRepository_Client_Call) Return(_a0 github.Client) *MockGithubRepository_Client_Call { +func (_c *MockGithubRepository_Client_Call) Return(_a0 Client) *MockGithubRepository_Client_Call { _c.Call.Return(_a0) return _c } -func (_c *MockGithubRepository_Client_Call) RunAndReturn(run func() github.Client) *MockGithubRepository_Client_Call { - _c.Call.Return(run) - return _c -} - -// Clone provides a mock function with given fields: ctx, opts -func (_m *MockGithubRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Clone") - } - - var r0 ClonedRepository - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ClonedRepository) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockGithubRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone' -type MockGithubRepository_Clone_Call struct { - *mock.Call -} - -// Clone is a helper method to define mock.On call -// - ctx context.Context -// - opts CloneOptions -func (_e *MockGithubRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockGithubRepository_Clone_Call { - return &MockGithubRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)} -} - -func (_c *MockGithubRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockGithubRepository_Clone_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(CloneOptions)) - }) - return _c -} - -func (_c *MockGithubRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockGithubRepository_Clone_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockGithubRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockGithubRepository_Clone_Call { +func (_c *MockGithubRepository_Client_Call) RunAndReturn(run func() Client) *MockGithubRepository_Client_Call { _c.Call.Return(run) return _c } // CompareFiles provides a mock function with given fields: ctx, base, ref -func (_m *MockGithubRepository) CompareFiles(ctx context.Context, base string, ref string) ([]VersionedFileChange, error) { +func (_m *MockGithubRepository) CompareFiles(ctx context.Context, base string, ref string) ([]repository.VersionedFileChange, error) { ret := _m.Called(ctx, base, ref) if len(ret) == 0 { panic("no return value specified for CompareFiles") } - var r0 []VersionedFileChange + var r0 []repository.VersionedFileChange var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]VersionedFileChange, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]repository.VersionedFileChange, error)); ok { return rf(ctx, base, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) []VersionedFileChange); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) []repository.VersionedFileChange); ok { r0 = rf(ctx, base, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]VersionedFileChange) + r0 = ret.Get(0).([]repository.VersionedFileChange) } } @@ -182,12 +123,12 @@ func (_c *MockGithubRepository_CompareFiles_Call) Run(run func(ctx context.Conte return _c } -func (_c *MockGithubRepository_CompareFiles_Call) Return(_a0 []VersionedFileChange, _a1 error) *MockGithubRepository_CompareFiles_Call { +func (_c *MockGithubRepository_CompareFiles_Call) Return(_a0 []repository.VersionedFileChange, _a1 error) *MockGithubRepository_CompareFiles_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGithubRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]VersionedFileChange, error)) *MockGithubRepository_CompareFiles_Call { +func (_c *MockGithubRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]repository.VersionedFileChange, error)) *MockGithubRepository_CompareFiles_Call { _c.Call.Return(run) return _c } @@ -500,23 +441,23 @@ func (_c *MockGithubRepository_Owner_Call) RunAndReturn(run func() string) *Mock } // Read provides a mock function with given fields: ctx, path, ref -func (_m *MockGithubRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { +func (_m *MockGithubRepository) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) { ret := _m.Called(ctx, path, ref) if len(ret) == 0 { panic("no return value specified for Read") } - var r0 *FileInfo + var r0 *repository.FileInfo var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*repository.FileInfo, error)); ok { return rf(ctx, path, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *repository.FileInfo); ok { r0 = rf(ctx, path, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*FileInfo) + r0 = ret.Get(0).(*repository.FileInfo) } } @@ -549,34 +490,34 @@ func (_c *MockGithubRepository_Read_Call) Run(run func(ctx context.Context, path return _c } -func (_c *MockGithubRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockGithubRepository_Read_Call { +func (_c *MockGithubRepository_Read_Call) Return(_a0 *repository.FileInfo, _a1 error) *MockGithubRepository_Read_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGithubRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockGithubRepository_Read_Call { +func (_c *MockGithubRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*repository.FileInfo, error)) *MockGithubRepository_Read_Call { _c.Call.Return(run) return _c } // ReadTree provides a mock function with given fields: ctx, ref -func (_m *MockGithubRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { +func (_m *MockGithubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { ret := _m.Called(ctx, ref) if len(ret) == 0 { panic("no return value specified for ReadTree") } - var r0 []FileTreeEntry + var r0 []repository.FileTreeEntry var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) ([]repository.FileTreeEntry, error)); ok { return rf(ctx, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) []repository.FileTreeEntry); ok { r0 = rf(ctx, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]FileTreeEntry) + r0 = ret.Get(0).([]repository.FileTreeEntry) } } @@ -608,12 +549,12 @@ func (_c *MockGithubRepository_ReadTree_Call) Run(run func(ctx context.Context, return _c } -func (_c *MockGithubRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockGithubRepository_ReadTree_Call { +func (_c *MockGithubRepository_ReadTree_Call) Return(_a0 []repository.FileTreeEntry, _a1 error) *MockGithubRepository_ReadTree_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGithubRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockGithubRepository_ReadTree_Call { +func (_c *MockGithubRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]repository.FileTreeEntry, error)) *MockGithubRepository_ReadTree_Call { _c.Call.Return(run) return _c } @@ -664,7 +605,7 @@ func (_c *MockGithubRepository_Repo_Call) RunAndReturn(run func() string) *MockG } // ResourceURLs provides a mock function with given fields: ctx, file -func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*v0alpha1.ResourceURLs, error) { +func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*v0alpha1.ResourceURLs, error) { ret := _m.Called(ctx, file) if len(ret) == 0 { @@ -673,10 +614,10 @@ func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo var r0 *v0alpha1.ResourceURLs var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *FileInfo) (*v0alpha1.ResourceURLs, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *repository.FileInfo) (*v0alpha1.ResourceURLs, error)); ok { return rf(ctx, file) } - if rf, ok := ret.Get(0).(func(context.Context, *FileInfo) *v0alpha1.ResourceURLs); ok { + if rf, ok := ret.Get(0).(func(context.Context, *repository.FileInfo) *v0alpha1.ResourceURLs); ok { r0 = rf(ctx, file) } else { if ret.Get(0) != nil { @@ -684,7 +625,7 @@ func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo } } - if rf, ok := ret.Get(1).(func(context.Context, *FileInfo) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *repository.FileInfo) error); ok { r1 = rf(ctx, file) } else { r1 = ret.Error(1) @@ -700,14 +641,14 @@ type MockGithubRepository_ResourceURLs_Call struct { // ResourceURLs is a helper method to define mock.On call // - ctx context.Context -// - file *FileInfo +// - file *repository.FileInfo func (_e *MockGithubRepository_Expecter) ResourceURLs(ctx interface{}, file interface{}) *MockGithubRepository_ResourceURLs_Call { return &MockGithubRepository_ResourceURLs_Call{Call: _e.mock.On("ResourceURLs", ctx, file)} } -func (_c *MockGithubRepository_ResourceURLs_Call) Run(run func(ctx context.Context, file *FileInfo)) *MockGithubRepository_ResourceURLs_Call { +func (_c *MockGithubRepository_ResourceURLs_Call) Run(run func(ctx context.Context, file *repository.FileInfo)) *MockGithubRepository_ResourceURLs_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*FileInfo)) + run(args[0].(context.Context), args[1].(*repository.FileInfo)) }) return _c } @@ -717,7 +658,66 @@ func (_c *MockGithubRepository_ResourceURLs_Call) Return(_a0 *v0alpha1.ResourceU return _c } -func (_c *MockGithubRepository_ResourceURLs_Call) RunAndReturn(run func(context.Context, *FileInfo) (*v0alpha1.ResourceURLs, error)) *MockGithubRepository_ResourceURLs_Call { +func (_c *MockGithubRepository_ResourceURLs_Call) RunAndReturn(run func(context.Context, *repository.FileInfo) (*v0alpha1.ResourceURLs, error)) *MockGithubRepository_ResourceURLs_Call { + _c.Call.Return(run) + return _c +} + +// Stage provides a mock function with given fields: ctx, opts +func (_m *MockGithubRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Stage") + } + + var r0 repository.StagedRepository + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) (repository.StagedRepository, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) repository.StagedRepository); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(repository.StagedRepository) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, repository.StageOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGithubRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage' +type MockGithubRepository_Stage_Call struct { + *mock.Call +} + +// Stage is a helper method to define mock.On call +// - ctx context.Context +// - opts repository.StageOptions +func (_e *MockGithubRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockGithubRepository_Stage_Call { + return &MockGithubRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)} +} + +func (_c *MockGithubRepository_Stage_Call) Run(run func(ctx context.Context, opts repository.StageOptions)) *MockGithubRepository_Stage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(repository.StageOptions)) + }) + return _c +} + +func (_c *MockGithubRepository_Stage_Call) Return(_a0 repository.StagedRepository, _a1 error) *MockGithubRepository_Stage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGithubRepository_Stage_Call) RunAndReturn(run func(context.Context, repository.StageOptions) (repository.StagedRepository, error)) *MockGithubRepository_Stage_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/repository/github/impl.go b/pkg/registry/apis/provisioning/repository/github/impl.go index d94f2a544cb..c9f5468bc8d 100644 --- a/pkg/registry/apis/provisioning/repository/github/impl.go +++ b/pkg/registry/apis/provisioning/repository/github/impl.go @@ -8,10 +8,6 @@ import ( "time" "github.com/google/go-github/v70/github" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" ) type githubClient struct { @@ -22,268 +18,12 @@ func NewClient(client *github.Client) Client { return &githubClient{client} } -func (r *githubClient) IsAuthenticated(ctx context.Context) error { - if _, _, err := r.gh.Users.Get(ctx, ""); err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { - switch ghErr.Response.StatusCode { - case http.StatusUnauthorized: - return apierrors.NewUnauthorized("token is invalid or expired") - case http.StatusForbidden: - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Status: metav1.StatusFailure, - Code: http.StatusUnauthorized, - Reason: metav1.StatusReasonUnauthorized, - Message: "token is revoked or has insufficient permissions", - }, - } - case http.StatusServiceUnavailable: - return ErrServiceUnavailable - } - } - - return err - } - - return nil -} - -func (r *githubClient) RepoExists(ctx context.Context, owner, repository string) (bool, error) { - _, resp, err := r.gh.Repositories.Get(ctx, owner, repository) - if err == nil { - return true, nil - } - if resp.StatusCode == http.StatusNotFound { - return false, nil - } - - return false, err -} - const ( - maxDirectoryItems = 1000 // Maximum number of items allowed in a directory - maxTreeItems = 10000 // Maximum number of items allowed in a tree - maxCommits = 1000 // Maximum number of commits to fetch - maxCompareFiles = 1000 // Maximum number of files to compare between commits - maxWebhooks = 100 // Maximum number of webhooks allowed per repository - maxPRFiles = 1000 // Maximum number of files allowed in a pull request - maxPullRequestsFileComments = 1000 // Maximum number of comments allowed in a pull request - maxFileSize = 10 * 1024 * 1024 // 10MB in bytes + maxCommits = 1000 // Maximum number of commits to fetch + maxWebhooks = 100 // Maximum number of webhooks allowed per repository + maxPRFiles = 1000 // Maximum number of files allowed in a pull request ) -func (r *githubClient) GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) { - // First try to get repository contents - opts := &github.RepositoryContentGetOptions{ - Ref: ref, - } - - fc, dc, _, err := r.gh.Repositories.GetContents(ctx, owner, repository, path, opts) - if err != nil { - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return nil, nil, err - } - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return nil, nil, ErrServiceUnavailable - } - if ghErr.Response.StatusCode == http.StatusNotFound { - return nil, nil, ErrResourceNotFound - } - return nil, nil, err - } - - if fc != nil { - // Check file size before returning content - if fc.GetSize() > maxFileSize { - return nil, nil, ErrFileTooLarge - } - return realRepositoryContent{fc}, nil, nil - } - - // For directories, check size limits - if len(dc) > maxDirectoryItems { - return nil, nil, fmt.Errorf("directory contains too many items (more than %d)", maxDirectoryItems) - } - - // Convert directory contents - allContents := make([]RepositoryContent, 0, len(dc)) - for _, original := range dc { - allContents = append(allContents, realRepositoryContent{original}) - } - - return nil, allContents, nil -} - -func (r *githubClient) GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) ([]RepositoryContent, bool, error) { - var tree *github.Tree - var err error - - subPaths := safepath.Split(basePath) - currentRef := ref - - for { - // If subPaths is empty, we can read recursively, as we're reading the tree from the "base" of the repository. Otherwise, always read only the direct children. - recursive := recursive && len(subPaths) == 0 - - tree, _, err = r.gh.Git.GetTree(ctx, owner, repository, currentRef, recursive) - if err != nil { - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return nil, false, err - } - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return nil, false, ErrServiceUnavailable - } - if ghErr.Response.StatusCode == http.StatusNotFound { - if currentRef != ref { - // We're operating with a subpath which doesn't exist yet. - // Pretend as if there is simply no files. - // FIXME: why should we pretend this? - return nil, false, nil - } - // currentRef == ref - // This indicates the repository or commitish reference doesn't exist. This should always return an error. - return nil, false, ErrResourceNotFound - } - return nil, false, err - } - - // Check if we've exceeded the maximum allowed items - if len(tree.Entries) > maxTreeItems { - return nil, false, fmt.Errorf("tree contains too many items (more than %d)", maxTreeItems) - } - - // Prep for next iteration. - if len(subPaths) == 0 { - // We're done: we've discovered the tree we want. - break - } - - // the ref must be equal the SHA of the entry corresponding to subPaths[0] - currentRef = "" - for _, e := range tree.Entries { - if e.GetPath() == subPaths[0] { - currentRef = e.GetSHA() - break - } - } - subPaths = subPaths[1:] - if currentRef == "" { - // We couldn't find the folder in the tree... - return nil, false, nil - } - } - - // If the tree is truncated and we're in recursive mode, return an error - if tree.GetTruncated() && recursive { - return nil, true, fmt.Errorf("tree is too large to fetch recursively (more than %d items)", maxTreeItems) - } - - entries := make([]RepositoryContent, 0, len(tree.Entries)) - for _, te := range tree.Entries { - rrc := &realRepositoryContent{ - real: &github.RepositoryContent{ - Path: te.Path, - Size: te.Size, - SHA: te.SHA, - }, - } - if te.GetType() == "tree" { - rrc.real.Type = github.Ptr("dir") - } else { - rrc.real.Type = te.Type - } - entries = append(entries, rrc) - } - return entries, tree.GetTruncated(), nil -} - -func (r *githubClient) CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error { - if message == "" { - message = fmt.Sprintf("Create %s", path) - } - - _, _, err := r.gh.Repositories.CreateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ - Branch: &branch, - Message: &message, - Content: content, - }) - if err == nil { - return nil - } - - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return err - } - if ghErr.Response.StatusCode == http.StatusUnprocessableEntity { - return ErrResourceAlreadyExists - } - return err -} - -func (r *githubClient) UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error { - if message == "" { - message = fmt.Sprintf("Update %s", path) - } - - _, _, err := r.gh.Repositories.UpdateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ - Branch: &branch, - Message: &message, - Content: content, - SHA: &hash, - }) - if err == nil { - return nil - } - - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return err - } - if ghErr.Response.StatusCode == http.StatusNotFound { - return ErrResourceNotFound - } - if ghErr.Response.StatusCode == http.StatusConflict { - return ErrMismatchedHash - } - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return ErrServiceUnavailable - } - return err -} - -func (r *githubClient) DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error { - if message == "" { - message = fmt.Sprintf("Delete %s", path) - } - - _, _, err := r.gh.Repositories.DeleteFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ - Branch: &branch, - Message: &message, - SHA: &hash, - }) - if err == nil { - return nil - } - - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return err - } - if ghErr.Response.StatusCode == http.StatusNotFound { - return ErrResourceNotFound - } - if ghErr.Response.StatusCode == http.StatusConflict { - return ErrMismatchedHash - } - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return ErrServiceUnavailable - } - return err -} - // Commits returns a list of commits for a given repository and branch. func (r *githubClient) Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) { listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.RepositoryCommit, *github.Response, error) { @@ -343,105 +83,6 @@ func (r *githubClient) Commits(ctx context.Context, owner, repository, path, bra return ret, nil } -func (r *githubClient) CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) { - listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) { - compare, resp, err := r.gh.Repositories.CompareCommits(ctx, owner, repository, base, head, opts) - if err != nil { - return nil, resp, err - } - return compare.Files, resp, nil - } - - files, err := paginatedList( - ctx, - listFn, - defaultListOptions(maxCompareFiles), - ) - if errors.Is(err, ErrTooManyItems) { - return nil, fmt.Errorf("too many files changed between commits (more than %d)", maxCompareFiles) - } - if err != nil { - return nil, err - } - - // Convert to the interface type - ret := make([]CommitFile, 0, len(files)) - for _, f := range files { - ret = append(ret, f) - } - - return ret, nil -} - -func (r *githubClient) GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) { - branch, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0) - if err != nil { - // For some reason, GitHub client handles this case differently by failing with a wrapped error - if resp != nil && resp.StatusCode == http.StatusNotFound { - return Branch{}, ErrResourceNotFound - } - - if resp != nil && resp.StatusCode == http.StatusServiceUnavailable { - return Branch{}, ErrServiceUnavailable - } - - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return Branch{}, err - } - // Leaving these just in case - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return Branch{}, ErrServiceUnavailable - } - if ghErr.Response.StatusCode == http.StatusNotFound { - return Branch{}, ErrResourceNotFound - } - return Branch{}, err - } - - return Branch{ - Name: branch.GetName(), - Sha: branch.GetCommit().GetSHA(), - }, nil -} - -func (r *githubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error { - // Fail if the branch already exists - if _, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0); err == nil { - return ErrResourceAlreadyExists - } - - // Branch out based on the repository branch - baseRef, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, sourceBranch, 0) - if err != nil { - return fmt.Errorf("get base branch: %w", err) - } - - if _, _, err := r.gh.Git.CreateRef(ctx, owner, repository, &github.Reference{ - Ref: github.Ptr(fmt.Sprintf("refs/heads/%s", branchName)), - Object: &github.GitObject{ - SHA: baseRef.Commit.SHA, - }, - }); err != nil { - return fmt.Errorf("create branch ref: %w", err) - } - - return nil -} - -func (r *githubClient) BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) { - _, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0) - if err == nil { - return true, nil - } - - if resp.StatusCode == http.StatusNotFound { - return false, nil - } - - return false, err -} - func (r *githubClient) ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) { listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { return r.gh.Repositories.ListHooks(ctx, owner, repository, opts) @@ -626,44 +267,6 @@ func (r *githubClient) CreatePullRequestComment(ctx context.Context, owner, repo return nil } -type realRepositoryContent struct { - real *github.RepositoryContent -} - -var _ RepositoryContent = realRepositoryContent{} - -func (c realRepositoryContent) IsDirectory() bool { - return c.real.GetType() == "dir" -} - -func (c realRepositoryContent) GetFileContent() (string, error) { - return c.real.GetContent() -} - -func (c realRepositoryContent) IsSymlink() bool { - return c.real.Target != nil -} - -func (c realRepositoryContent) GetPath() string { - return c.real.GetPath() -} - -func (c realRepositoryContent) GetSHA() string { - return c.real.GetSHA() -} - -func (c realRepositoryContent) GetSize() int64 { - if c.real.Size != nil { - return int64(*c.real.Size) - } - if c.real.Content != nil { - if c, err := c.real.GetContent(); err == nil { - return int64(len(c)) - } - } - return 0 -} - // listOptions represents pagination parameters for list operations type listOptions struct { github.ListOptions diff --git a/pkg/registry/apis/provisioning/repository/github/impl_test.go b/pkg/registry/apis/provisioning/repository/github/impl_test.go index b979f288836..fedbc2a9850 100644 --- a/pkg/registry/apis/provisioning/repository/github/impl_test.go +++ b/pkg/registry/apis/provisioning/repository/github/impl_test.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net/http" - "strings" "testing" "time" @@ -15,1415 +14,8 @@ import ( mockhub "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - apierrors "k8s.io/apimachinery/pkg/api/errors" ) -func TestIsAuthenticated(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - wantErr error - }{ - { - name: "successful authentication", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatch( - mockhub.GetUser, - github.User{}, - ), - ), - wantErr: nil, - }, - { - name: "unauthorized - invalid token", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Bad credentials"})) - }), - ), - ), - wantErr: apierrors.NewUnauthorized("token is invalid or expired"), - }, - { - name: "forbidden - insufficient permissions", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusForbidden) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Forbidden"})) - }), - ), - ), - wantErr: apierrors.NewUnauthorized("token is revoked or has insufficient permissions"), - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Service unavailable"})) - }), - ), - ), - wantErr: ErrServiceUnavailable, - }, - { - name: "unknown error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Internal server error"})) - }), - ), - ), - wantErr: errors.New("500 Internal server error []"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.IsAuthenticated(context.Background()) - // Check the error - if tt.wantErr == nil { - assert.NoError(t, err) - } else { - assert.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.wantErr, &statusErr) { - // For StatusError, compare status code - var actualStatusErr *apierrors.StatusError - assert.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - if actualStatusErr != nil { - assert.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - assert.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - } - } else { - // For regular errors, compare error messages - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - } - }) - } -} -func TestGithubClient_RepoExists(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - want bool - wantErr error - }{ - { - name: "repository exists", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(map[string]interface{}{ - "id": 123, - "name": "test-repo", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - want: true, - wantErr: nil, - }, - { - name: "repository does not exist", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Not Found"})) - }), - ), - ), - owner: "test-owner", - repository: "non-existent-repo", - want: false, - wantErr: nil, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Service unavailable"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - want: false, - wantErr: errors.New("503 Service unavailable []"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - exists, err := client.RepoExists(context.Background(), tt.owner, tt.repository) - - // Check the result - assert.Equal(t, tt.want, exists) - - // Check the error - if tt.wantErr == nil { - assert.NoError(t, err) - } else { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - }) - } -} - -func TestGithubClient_GetContents(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - path string - ref string - wantFile bool - wantDir bool - wantErr error - }{ - { - name: "get file contents", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - fileContent := &github.RepositoryContent{ - Type: github.Ptr("file"), - Name: github.Ptr("test.txt"), - Path: github.Ptr("test.txt"), - Content: github.Ptr("dGVzdCBjb250ZW50"), // base64 encoded "test content" - Encoding: github.Ptr("base64"), - Size: github.Ptr(12), - SHA: github.Ptr("abc123"), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(fileContent)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - ref: "main", - wantFile: true, - wantDir: false, - wantErr: nil, - }, - { - name: "get directory contents", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - dirContents := []*github.RepositoryContent{ - { - Type: github.Ptr("file"), - Name: github.Ptr("file1.txt"), - Path: github.Ptr("dir/file1.txt"), - Size: github.Ptr(100), - SHA: github.Ptr("abc123"), - }, - { - Type: github.Ptr("dir"), - Name: github.Ptr("subdir"), - Path: github.Ptr("dir/subdir"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(dirContents)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "dir", - ref: "main", - wantFile: false, - wantDir: true, - wantErr: nil, - }, - { - name: "resource not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Not Found"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "nonexistent.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: ErrResourceNotFound, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Service unavailable"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: ErrServiceUnavailable, - }, - { - name: "file too large", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - fileContent := &github.RepositoryContent{ - Type: github.Ptr("file"), - Name: github.Ptr("large.txt"), - Path: github.Ptr("large.txt"), - Content: github.Ptr(""), - Encoding: github.Ptr("base64"), - Size: github.Ptr(maxFileSize + 1), // Exceeds max file size - SHA: github.Ptr("abc123"), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(fileContent)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "large.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: ErrFileTooLarge, - }, - { - name: "not a github error response", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusConflict) - // Return a non-GitHub error format - _, err := w.Write([]byte("not a github error")) - require.NoError(t, err) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: errors.New("409"), - }, - { - name: "directory with too many items", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Create a directory with more than maxDirectoryItems - dirContents := make([]*github.RepositoryContent, maxDirectoryItems+1) - for i := 0; i < maxDirectoryItems+1; i++ { - dirContents[i] = &github.RepositoryContent{ - Type: github.Ptr("file"), - Name: github.Ptr(fmt.Sprintf("file%d.txt", i)), - Path: github.Ptr(fmt.Sprintf("dir/file%d.txt", i)), - Size: github.Ptr(100), - SHA: github.Ptr(fmt.Sprintf("sha%d", i)), - } - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(dirContents)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "dir", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: fmt.Errorf("directory contains too many items (more than %d)", maxDirectoryItems), - }, - { - name: "error response with other status code", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusForbidden) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusForbidden, - }, - Message: "Forbidden access", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: errors.New("Forbidden access"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - fileContent, dirContents, err := client.GetContents(context.Background(), tt.owner, tt.repository, tt.path, tt.ref) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr.Error()) - assert.Nil(t, fileContent) - assert.Nil(t, dirContents) - return - } - assert.NoError(t, err) - - // Check the result - if tt.wantFile { - assert.NotNil(t, fileContent) - assert.Nil(t, dirContents) - } else if tt.wantDir { - assert.Nil(t, fileContent) - assert.NotNil(t, dirContents) - assert.Greater(t, len(dirContents), 0) - } - }) - } -} - -func TestGithubClient_GetTree(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - basePath string - ref string - recursive bool - wantItems int - wantTrunc bool - wantErr error - }{ - { - name: "get tree successfully", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - tree := &github.Tree{ - SHA: github.Ptr("abc123"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("file1.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr("file1sha"), - }, - { - Path: github.Ptr("file2.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(14), - SHA: github.Ptr("file2sha"), - }, - { - Path: github.Ptr("dir"), - Mode: github.Ptr("040000"), - Type: github.Ptr("tree"), - SHA: github.Ptr("dirsha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 3, - wantTrunc: false, - wantErr: nil, - }, - { - name: "get tree with subpath", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check if this is the first request for the root tree - if !strings.Contains(r.URL.Path, "subdirsha") { - // Verify the request URL contains the correct owner, repo, and ref - expectedPath := "/repos/test-owner/test-repo/git/trees/main" - assert.True(t, strings.Contains(r.URL.Path, expectedPath), - "Expected URL path to contain %s, got %s", expectedPath, r.URL.Path) - - // Verify query parameters for recursive flag - query := r.URL.Query() - assert.Equal(t, "", query.Get("recursive"), "Recursive parameter should not be set") - } else { - // This is the second request for the subtree - assert.True(t, strings.Contains(r.URL.Path, "subdirsha"), - "Expected URL path to contain subdirsha, got %s", r.URL.Path) - } - // First request for the root tree - tree := &github.Tree{ - SHA: github.Ptr("rootsha"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("subdir"), - Mode: github.Ptr("040000"), - Type: github.Ptr("tree"), - SHA: github.Ptr("subdirsha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Second request for the subdir tree - if strings.Contains(r.URL.Path, "subdirsha") { - tree := &github.Tree{ - SHA: github.Ptr("subdirsha"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("file3.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(16), - SHA: github.Ptr("file3sha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - } - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "subdir", - ref: "main", - recursive: false, - wantItems: 1, - wantTrunc: false, - wantErr: nil, - }, - { - name: "subpath not found should pretend is empty", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // First request for the root tree - if !strings.Contains(r.URL.Path, "nonexistentsha") { - tree := &github.Tree{ - SHA: github.Ptr("rootsha"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("nonexistent"), - Mode: github.Ptr("040000"), - Type: github.Ptr("tree"), - SHA: github.Ptr("nonexistentsha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - } else { - // Second request for the nonexistent subtree returns 404 - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Not Found", - })) - } - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "nonexistent", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: nil, - }, - { - name: "get tree fails with service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: ErrServiceUnavailable, - }, - { - name: "tree contains too many items", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Create more entries than the maxTreeItems limit - entries := make([]*github.TreeEntry, maxTreeItems+1) - for i := 0; i < maxTreeItems+1; i++ { - entries[i] = &github.TreeEntry{ - Path: github.Ptr(fmt.Sprintf("file%d.txt", i+1)), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr(fmt.Sprintf("sha%d", i+1)), - } - } - - tree := &github.Tree{ - SHA: github.Ptr("abc123"), - Entries: entries, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: fmt.Errorf("tree contains too many items (more than %d)", maxTreeItems), - }, - - { - name: "tree is truncated with recursive mode", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - tree := &github.Tree{ - SHA: github.Ptr("abc123"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("file1.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr("file1sha"), - }, - }, - Truncated: github.Ptr(true), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: true, - wantItems: 0, - wantTrunc: true, - wantErr: fmt.Errorf("tree is too large to fetch recursively (more than 10000 items)"), - }, - { - name: "repository not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Not Found", - })) - }), - ), - ), - owner: "test-owner", - repository: "non-existent-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: ErrResourceNotFound, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: ErrServiceUnavailable, - }, - { - name: "too many items in tree", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Create a tree with more than maxTreeItems entries - entries := make([]*github.TreeEntry, maxTreeItems+1) - for i := 0; i < maxTreeItems+1; i++ { - entries[i] = &github.TreeEntry{ - Path: github.Ptr(fmt.Sprintf("file%d.txt", i)), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr(fmt.Sprintf("sha%d", i)), - } - } - tree := &github.Tree{ - SHA: github.Ptr("abc123"), - Entries: entries, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: fmt.Errorf("tree contains too many items (more than 10000)"), - }, - { - name: "folder not found in tree", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Return a tree that doesn't contain the requested folder - tree := &github.Tree{ - SHA: github.Ptr("rootsha"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("other-folder"), - Mode: github.Ptr("040000"), - Type: github.Ptr("tree"), - SHA: github.Ptr("othersha"), - }, - { - Path: github.Ptr("file.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr("filesha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "non-existent-folder/subpath", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: nil, - }, - { - name: "non-standard error is passed through", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusForbidden) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusForbidden, - }, - Message: "Forbidden access", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: errors.New("Forbidden access"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - contents, truncated, err := client.GetTree(context.Background(), tt.owner, tt.repository, tt.basePath, tt.ref, tt.recursive) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr.Error()) - assert.Nil(t, contents) - return - } - assert.NoError(t, err) - - // Check truncated flag - assert.Equal(t, tt.wantTrunc, truncated) - - // Check the result - if tt.wantItems > 0 { - assert.NotNil(t, contents) - assert.Equal(t, tt.wantItems, len(contents)) - } else { - assert.Empty(t, contents) - } - }) - } -} - -func TestGithubClient_CreateFile(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - path string - branch string - message string - content []byte - wantErr error - }{ - { - name: "create file successfully", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - response := &github.RepositoryContentResponse{ - Content: &github.RepositoryContent{ - Name: github.Ptr("test.txt"), - Path: github.Ptr("test.txt"), - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusCreated) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Add test.txt", - content: []byte("test content"), - wantErr: nil, - }, - { - name: "file already exists", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnprocessableEntity) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusUnprocessableEntity, - }, - Message: "File already exists", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "existing.txt", - branch: "main", - message: "Add existing.txt", - content: []byte("test content"), - wantErr: ErrResourceAlreadyExists, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Add test.txt", - content: []byte("test content"), - wantErr: errors.New("Service unavailable"), - }, - { - name: "not a github error response", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write([]byte("not a github error")) - require.NoError(t, err) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Add test.txt", - content: []byte("test content"), - wantErr: errors.New("500"), - }, - { - name: "default commit message", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Decode the request to verify the message - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var reqData struct { - Message string `json:"message"` - } - require.NoError(t, json.Unmarshal(body, &reqData)) - assert.Equal(t, "Create test.txt", reqData.Message) - - response := &github.RepositoryContentResponse{ - Content: &github.RepositoryContent{ - Name: github.Ptr("test.txt"), - Path: github.Ptr("test.txt"), - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusCreated) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "", // Empty message should use default - content: []byte("test content"), - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.CreateFile(context.Background(), tt.owner, tt.repository, tt.path, tt.branch, tt.message, tt.content) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestUpdateFile(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - path string - branch string - message string - hash string - content []byte - wantErr error - }{ - { - name: "successful update", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify request body - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var reqData struct { - Message string `json:"message"` - SHA string `json:"sha"` - } - require.NoError(t, json.Unmarshal(body, &reqData)) - assert.Equal(t, "Update test.txt", reqData.Message) - assert.Equal(t, "abc123", reqData.SHA) - - response := &github.RepositoryContentResponse{ - Content: &github.RepositoryContent{ - Name: github.Ptr("test.txt"), - Path: github.Ptr("test.txt"), - SHA: github.Ptr("def456"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "", // Empty message should use default - hash: "abc123", - content: []byte("updated content"), - wantErr: nil, - }, - { - name: "file not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Not Found"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "nonexistent.txt", - branch: "main", - message: "Update nonexistent file", - hash: "abc123", - content: []byte("content"), - wantErr: ErrResourceNotFound, - }, - { - name: "mismatched hash", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusConflict) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "SHA mismatch"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Update with wrong hash", - hash: "wrong-hash", - content: []byte("content"), - wantErr: ErrMismatchedHash, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Service unavailable"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Update during outage", - hash: "abc123", - content: []byte("content"), - wantErr: ErrServiceUnavailable, - }, - { - name: "other error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Internal server error"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Update with server error", - hash: "abc123", - content: []byte("content"), - wantErr: errors.New("Internal server error"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.UpdateFile(context.Background(), tt.owner, tt.repository, tt.path, tt.branch, tt.message, tt.hash, tt.content) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestGithubClient_DeleteFile(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - path string - branch string - message string - hash string - wantErr error - }{ - { - name: "delete file successfully", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - response := &github.RepositoryContentResponse{ - Content: nil, - Commit: github.Commit{ - SHA: github.Ptr("def456"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Delete test.txt", - hash: "abc123", - wantErr: nil, - }, - { - name: "file not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Not Found", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "nonexistent.txt", - branch: "main", - message: "Delete nonexistent.txt", - hash: "abc123", - wantErr: ErrResourceNotFound, - }, - { - name: "mismatched hash", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusConflict) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusConflict, - }, - Message: "Conflict", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Delete test.txt", - hash: "wrong-hash", - wantErr: ErrMismatchedHash, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Delete test.txt", - hash: "abc123", - wantErr: ErrServiceUnavailable, - }, - { - name: "other error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Internal server error"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Delete with server error", - hash: "abc123", - wantErr: errors.New("Internal server error"), - }, - { - name: "default commit message", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Decode the request to verify the message - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var reqData struct { - Message string `json:"message"` - } - require.NoError(t, json.Unmarshal(body, &reqData)) - assert.Equal(t, "Delete test.txt", reqData.Message) - - response := &github.RepositoryContentResponse{ - Content: nil, - Commit: github.Commit{ - SHA: github.Ptr("def456"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "", - hash: "abc123", - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.DeleteFile(context.Background(), tt.owner, tt.repository, tt.path, tt.branch, tt.message, tt.hash) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - } else { - assert.NoError(t, err) - } - }) - } -} - func TestGithubClient_GetCommits(t *testing.T) { tests := []struct { name string @@ -1767,621 +359,6 @@ func TestGithubClient_GetCommits(t *testing.T) { } } -func TestCompareCommits(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - base string - head string - wantFiles []CommitFile - wantErr error - }{ - { - name: "successful comparison", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - files := []*github.CommitFile{ - { - Filename: github.Ptr("file1.txt"), - Status: github.Ptr("modified"), - Additions: github.Ptr(10), - Deletions: github.Ptr(5), - Changes: github.Ptr(15), - }, - { - Filename: github.Ptr("file2.txt"), - Status: github.Ptr("added"), - Additions: github.Ptr(20), - Deletions: github.Ptr(0), - Changes: github.Ptr(20), - }, - } - - require.NoError(t, json.NewEncoder(w).Encode(github.CommitsComparison{ - Files: files, - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: []CommitFile{ - &github.CommitFile{ - Filename: github.Ptr("file1.txt"), - Status: github.Ptr("modified"), - Additions: github.Ptr(10), - Deletions: github.Ptr(5), - Changes: github.Ptr(15), - }, - &github.CommitFile{ - Filename: github.Ptr("file2.txt"), - Status: github.Ptr("added"), - Additions: github.Ptr(20), - Deletions: github.Ptr(0), - Changes: github.Ptr(20), - }, - }, - wantErr: nil, - }, - { - name: "too many files", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Generate more files than the max limit - files := make([]*github.CommitFile, maxCompareFiles+1) - for i := 0; i < maxCompareFiles+1; i++ { - files[i] = &github.CommitFile{ - Filename: github.Ptr(fmt.Sprintf("file%d.txt", i)), - Status: github.Ptr("modified"), - } - } - - require.NoError(t, json.NewEncoder(w).Encode(github.CommitsComparison{ - Files: files, - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: nil, - wantErr: fmt.Errorf("too many files changed between commits (more than %d)", maxCompareFiles), - }, - { - name: "resource not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Not found", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: nil, - wantErr: ErrResourceNotFound, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: nil, - wantErr: ErrServiceUnavailable, - }, - { - name: "other error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - Message: "Internal server error", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: nil, - wantErr: errors.New("Internal server error"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - files, err := client.CompareCommits(context.Background(), tt.owner, tt.repository, tt.base, tt.head) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - assert.Nil(t, files) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.wantFiles, files) - } - }) - } -} - -func TestGetBranch(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - branchName string - wantBranch Branch - wantErr error - }{ - { - name: "get branch successfully", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - branch := &github.Branch{ - Name: github.Ptr("main"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "main", - wantBranch: Branch{ - Name: "main", - Sha: "abc123", - }, - wantErr: nil, - }, - { - name: "branch not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Branch not found", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "non-existent", - wantBranch: Branch{}, - wantErr: ErrResourceNotFound, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "main", - wantBranch: Branch{}, - wantErr: ErrServiceUnavailable, - }, - { - name: "other error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - Message: "Internal server error", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "main", - wantBranch: Branch{}, - wantErr: errors.New("unexpected status code: 500 Internal Server Error"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - branch, err := client.GetBranch(context.Background(), tt.owner, tt.repository, tt.branchName) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - assert.Equal(t, tt.wantBranch, branch) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.wantBranch, branch) - } - }) - } -} - -func TestGithubClient_CreateBranch(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - sourceBranch string - branchName string - wantErr error - }{ - { - name: "successful branch creation", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // First call checks if branch exists (should return 404) - if strings.Contains(r.URL.Path, "/new-branch") { - w.WriteHeader(http.StatusNotFound) - return - } - - // Second call gets the source branch - if strings.Contains(r.URL.Path, "/main") { - branch := &github.Branch{ - Name: github.Ptr("main"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - } - }), - ), - mockhub.WithRequestMatchHandler( - mockhub.PostReposGitRefsByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify the request body contains the correct reference - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - ref := struct { - Ref string `json:"ref"` - SHA string `json:"sha"` - }{} - require.NoError(t, json.Unmarshal(body, &ref)) - assert.Equal(t, "refs/heads/new-branch", ref.Ref) - assert.Equal(t, "abc123", ref.SHA) - - w.WriteHeader(http.StatusCreated) - require.NoError(t, json.NewEncoder(w).Encode(&github.Reference{ - Ref: github.Ptr("refs/heads/new-branch"), - Object: &github.GitObject{ - SHA: github.Ptr("abc123"), - }, - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - sourceBranch: "main", - branchName: "new-branch", - wantErr: nil, - }, - { - name: "branch already exists", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify the request URL contains the correct owner, repo, and branch - expectedPath := "/repos/test-owner/test-repo/branches/existing-branch" - assert.True(t, strings.Contains(r.URL.Path, expectedPath), - "Expected URL path to contain %s, got %s", expectedPath, r.URL.Path) - // Branch exists check returns success - branch := &github.Branch{ - Name: github.Ptr("existing-branch"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - sourceBranch: "main", - branchName: "existing-branch", - wantErr: ErrResourceAlreadyExists, - }, - { - name: "source branch not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // First call checks if branch exists (should return 404) - if strings.Contains(r.URL.Path, "/new-branch") { - w.WriteHeader(http.StatusNotFound) - return - } - - // Second call gets the source branch (not found) - if strings.Contains(r.URL.Path, "/nonexistent") { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Branch not found", - })) - } - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - sourceBranch: "nonexistent", - branchName: "new-branch", - wantErr: errors.New("get base branch"), - }, - { - name: "error creating branch ref", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // First call checks if branch exists (should return 404) - if strings.Contains(r.URL.Path, "/new-branch") { - w.WriteHeader(http.StatusNotFound) - return - } - - // Second call gets the source branch - if strings.Contains(r.URL.Path, "/main") { - branch := &github.Branch{ - Name: github.Ptr("main"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - } - }), - ), - mockhub.WithRequestMatchHandler( - mockhub.PostReposGitRefsByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - Message: "Internal server error", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - sourceBranch: "main", - branchName: "new-branch", - wantErr: errors.New("create branch ref"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.CreateBranch(context.Background(), tt.owner, tt.repository, tt.sourceBranch, tt.branchName) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestGithubClient_BranchExists(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - branchName string - want bool - wantErr bool - }{ - { - name: "branch exists", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - branch := &github.Branch{ - Name: github.Ptr("existing-branch"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "existing-branch", - want: true, - wantErr: false, - }, - { - name: "branch does not exist", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Branch not found", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "non-existent-branch", - want: false, - wantErr: false, - }, - { - name: "error response", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - Message: "Internal server error", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "some-branch", - want: false, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - got, err := client.BranchExists(context.Background(), tt.owner, tt.repository, tt.branchName) - - // Check the error - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - - // Check the result - assert.Equal(t, tt.want, got) - }) - } -} func TestGithubClient_ListWebhooks(t *testing.T) { tests := []struct { name string @@ -3721,139 +1698,3 @@ func TestDefaultListOptions(t *testing.T) { }) } } - -func TestRealRepositoryContent(t *testing.T) { - t.Run("IsDirectory", func(t *testing.T) { - tests := []struct { - name string - repoType string - want bool - }{ - { - name: "directory type", - repoType: "dir", - want: true, - }, - { - name: "file type", - repoType: "file", - want: false, - }, - { - name: "empty type", - repoType: "", - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - repoType := tt.repoType - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Type: &repoType, - }, - } - got := content.IsDirectory() - assert.Equal(t, tt.want, got) - }) - } - }) - - t.Run("GetFileContent", func(t *testing.T) { - fileContent := "test content" - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Content: &fileContent, - }, - } - got, err := content.GetFileContent() - assert.NoError(t, err) - assert.Equal(t, fileContent, got) - }) - - t.Run("IsSymlink", func(t *testing.T) { - tests := []struct { - name string - target *string - want bool - }{ - { - name: "is symlink", - target: github.Ptr("target"), - want: true, - }, - { - name: "not symlink", - target: nil, - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Target: tt.target, - }, - } - got := content.IsSymlink() - assert.Equal(t, tt.want, got) - }) - } - }) - - t.Run("GetPath", func(t *testing.T) { - path := "path/to/file" - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Path: &path, - }, - } - got := content.GetPath() - assert.Equal(t, path, got) - }) - - t.Run("GetSHA", func(t *testing.T) { - sha := "abc123" - content := realRepositoryContent{ - real: &github.RepositoryContent{ - SHA: &sha, - }, - } - got := content.GetSHA() - assert.Equal(t, sha, got) - }) - - t.Run("GetSize", func(t *testing.T) { - t.Run("with size field", func(t *testing.T) { - size := 42 - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Size: &size, - }, - } - got := content.GetSize() - assert.Equal(t, int64(size), got) - }) - - t.Run("with content field", func(t *testing.T) { - fileContent := "test content" - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Content: &fileContent, - }, - } - got := content.GetSize() - assert.Equal(t, int64(len(fileContent)), got) - }) - - t.Run("with no size or content", func(t *testing.T) { - content := realRepositoryContent{ - real: &github.RepositoryContent{}, - } - got := content.GetSize() - assert.Equal(t, int64(0), got) - }) - }) -} diff --git a/pkg/registry/apis/provisioning/repository/github/mock_client.go b/pkg/registry/apis/provisioning/repository/github/mock_client.go index 6ea39c8a67f..f70aa9f1359 100644 --- a/pkg/registry/apis/provisioning/repository/github/mock_client.go +++ b/pkg/registry/apis/provisioning/repository/github/mock_client.go @@ -21,65 +21,6 @@ func (_m *MockClient) EXPECT() *MockClient_Expecter { return &MockClient_Expecter{mock: &_m.Mock} } -// BranchExists provides a mock function with given fields: ctx, owner, repository, branchName -func (_m *MockClient) BranchExists(ctx context.Context, owner string, repository string, branchName string) (bool, error) { - ret := _m.Called(ctx, owner, repository, branchName) - - if len(ret) == 0 { - panic("no return value specified for BranchExists") - } - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (bool, error)); ok { - return rf(ctx, owner, repository, branchName) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) bool); ok { - r0 = rf(ctx, owner, repository, branchName) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { - r1 = rf(ctx, owner, repository, branchName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_BranchExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BranchExists' -type MockClient_BranchExists_Call struct { - *mock.Call -} - -// BranchExists is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - branchName string -func (_e *MockClient_Expecter) BranchExists(ctx interface{}, owner interface{}, repository interface{}, branchName interface{}) *MockClient_BranchExists_Call { - return &MockClient_BranchExists_Call{Call: _e.mock.On("BranchExists", ctx, owner, repository, branchName)} -} - -func (_c *MockClient_BranchExists_Call) Run(run func(ctx context.Context, owner string, repository string, branchName string)) *MockClient_BranchExists_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *MockClient_BranchExists_Call) Return(_a0 bool, _a1 error) *MockClient_BranchExists_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_BranchExists_Call) RunAndReturn(run func(context.Context, string, string, string) (bool, error)) *MockClient_BranchExists_Call { - _c.Call.Return(run) - return _c -} - // Commits provides a mock function with given fields: ctx, owner, repository, path, branch func (_m *MockClient) Commits(ctx context.Context, owner string, repository string, path string, branch string) ([]Commit, error) { ret := _m.Called(ctx, owner, repository, path, branch) @@ -142,170 +83,6 @@ func (_c *MockClient_Commits_Call) RunAndReturn(run func(context.Context, string return _c } -// CompareCommits provides a mock function with given fields: ctx, owner, repository, base, head -func (_m *MockClient) CompareCommits(ctx context.Context, owner string, repository string, base string, head string) ([]CommitFile, error) { - ret := _m.Called(ctx, owner, repository, base, head) - - if len(ret) == 0 { - panic("no return value specified for CompareCommits") - } - - var r0 []CommitFile - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]CommitFile, error)); ok { - return rf(ctx, owner, repository, base, head) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []CommitFile); ok { - r0 = rf(ctx, owner, repository, base, head) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]CommitFile) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { - r1 = rf(ctx, owner, repository, base, head) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_CompareCommits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompareCommits' -type MockClient_CompareCommits_Call struct { - *mock.Call -} - -// CompareCommits is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - base string -// - head string -func (_e *MockClient_Expecter) CompareCommits(ctx interface{}, owner interface{}, repository interface{}, base interface{}, head interface{}) *MockClient_CompareCommits_Call { - return &MockClient_CompareCommits_Call{Call: _e.mock.On("CompareCommits", ctx, owner, repository, base, head)} -} - -func (_c *MockClient_CompareCommits_Call) Run(run func(ctx context.Context, owner string, repository string, base string, head string)) *MockClient_CompareCommits_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) - }) - return _c -} - -func (_c *MockClient_CompareCommits_Call) Return(_a0 []CommitFile, _a1 error) *MockClient_CompareCommits_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_CompareCommits_Call) RunAndReturn(run func(context.Context, string, string, string, string) ([]CommitFile, error)) *MockClient_CompareCommits_Call { - _c.Call.Return(run) - return _c -} - -// CreateBranch provides a mock function with given fields: ctx, owner, repository, sourceBranch, branchName -func (_m *MockClient) CreateBranch(ctx context.Context, owner string, repository string, sourceBranch string, branchName string) error { - ret := _m.Called(ctx, owner, repository, sourceBranch, branchName) - - if len(ret) == 0 { - panic("no return value specified for CreateBranch") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok { - r0 = rf(ctx, owner, repository, sourceBranch, branchName) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_CreateBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBranch' -type MockClient_CreateBranch_Call struct { - *mock.Call -} - -// CreateBranch is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - sourceBranch string -// - branchName string -func (_e *MockClient_Expecter) CreateBranch(ctx interface{}, owner interface{}, repository interface{}, sourceBranch interface{}, branchName interface{}) *MockClient_CreateBranch_Call { - return &MockClient_CreateBranch_Call{Call: _e.mock.On("CreateBranch", ctx, owner, repository, sourceBranch, branchName)} -} - -func (_c *MockClient_CreateBranch_Call) Run(run func(ctx context.Context, owner string, repository string, sourceBranch string, branchName string)) *MockClient_CreateBranch_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) - }) - return _c -} - -func (_c *MockClient_CreateBranch_Call) Return(_a0 error) *MockClient_CreateBranch_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_CreateBranch_Call) RunAndReturn(run func(context.Context, string, string, string, string) error) *MockClient_CreateBranch_Call { - _c.Call.Return(run) - return _c -} - -// CreateFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, content -func (_m *MockClient) CreateFile(ctx context.Context, owner string, repository string, path string, branch string, message string, content []byte) error { - ret := _m.Called(ctx, owner, repository, path, branch, message, content) - - if len(ret) == 0 { - panic("no return value specified for CreateFile") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, []byte) error); ok { - r0 = rf(ctx, owner, repository, path, branch, message, content) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_CreateFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateFile' -type MockClient_CreateFile_Call struct { - *mock.Call -} - -// CreateFile is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - path string -// - branch string -// - message string -// - content []byte -func (_e *MockClient_Expecter) CreateFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, content interface{}) *MockClient_CreateFile_Call { - return &MockClient_CreateFile_Call{Call: _e.mock.On("CreateFile", ctx, owner, repository, path, branch, message, content)} -} - -func (_c *MockClient_CreateFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, content []byte)) *MockClient_CreateFile_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].([]byte)) - }) - return _c -} - -func (_c *MockClient_CreateFile_Call) Return(_a0 error) *MockClient_CreateFile_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_CreateFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, []byte) error) *MockClient_CreateFile_Call { - _c.Call.Return(run) - return _c -} - // CreatePullRequestComment provides a mock function with given fields: ctx, owner, repository, number, body func (_m *MockClient) CreatePullRequestComment(ctx context.Context, owner string, repository string, number int, body string) error { ret := _m.Called(ctx, owner, repository, number, body) @@ -415,58 +192,6 @@ func (_c *MockClient_CreateWebhook_Call) RunAndReturn(run func(context.Context, return _c } -// DeleteFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, hash -func (_m *MockClient) DeleteFile(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string) error { - ret := _m.Called(ctx, owner, repository, path, branch, message, hash) - - if len(ret) == 0 { - panic("no return value specified for DeleteFile") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string) error); ok { - r0 = rf(ctx, owner, repository, path, branch, message, hash) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_DeleteFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteFile' -type MockClient_DeleteFile_Call struct { - *mock.Call -} - -// DeleteFile is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - path string -// - branch string -// - message string -// - hash string -func (_e *MockClient_Expecter) DeleteFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, hash interface{}) *MockClient_DeleteFile_Call { - return &MockClient_DeleteFile_Call{Call: _e.mock.On("DeleteFile", ctx, owner, repository, path, branch, message, hash)} -} - -func (_c *MockClient_DeleteFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string)) *MockClient_DeleteFile_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string)) - }) - return _c -} - -func (_c *MockClient_DeleteFile_Call) Return(_a0 error) *MockClient_DeleteFile_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_DeleteFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, string) error) *MockClient_DeleteFile_Call { - _c.Call.Return(run) - return _c -} - // DeleteWebhook provides a mock function with given fields: ctx, owner, repository, webhookID func (_m *MockClient) DeleteWebhook(ctx context.Context, owner string, repository string, webhookID int64) error { ret := _m.Called(ctx, owner, repository, webhookID) @@ -565,206 +290,6 @@ func (_c *MockClient_EditWebhook_Call) RunAndReturn(run func(context.Context, st return _c } -// GetBranch provides a mock function with given fields: ctx, owner, repository, branchName -func (_m *MockClient) GetBranch(ctx context.Context, owner string, repository string, branchName string) (Branch, error) { - ret := _m.Called(ctx, owner, repository, branchName) - - if len(ret) == 0 { - panic("no return value specified for GetBranch") - } - - var r0 Branch - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (Branch, error)); ok { - return rf(ctx, owner, repository, branchName) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) Branch); ok { - r0 = rf(ctx, owner, repository, branchName) - } else { - r0 = ret.Get(0).(Branch) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { - r1 = rf(ctx, owner, repository, branchName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_GetBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBranch' -type MockClient_GetBranch_Call struct { - *mock.Call -} - -// GetBranch is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - branchName string -func (_e *MockClient_Expecter) GetBranch(ctx interface{}, owner interface{}, repository interface{}, branchName interface{}) *MockClient_GetBranch_Call { - return &MockClient_GetBranch_Call{Call: _e.mock.On("GetBranch", ctx, owner, repository, branchName)} -} - -func (_c *MockClient_GetBranch_Call) Run(run func(ctx context.Context, owner string, repository string, branchName string)) *MockClient_GetBranch_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *MockClient_GetBranch_Call) Return(_a0 Branch, _a1 error) *MockClient_GetBranch_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_GetBranch_Call) RunAndReturn(run func(context.Context, string, string, string) (Branch, error)) *MockClient_GetBranch_Call { - _c.Call.Return(run) - return _c -} - -// GetContents provides a mock function with given fields: ctx, owner, repository, path, ref -func (_m *MockClient) GetContents(ctx context.Context, owner string, repository string, path string, ref string) (RepositoryContent, []RepositoryContent, error) { - ret := _m.Called(ctx, owner, repository, path, ref) - - if len(ret) == 0 { - panic("no return value specified for GetContents") - } - - var r0 RepositoryContent - var r1 []RepositoryContent - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (RepositoryContent, []RepositoryContent, error)); ok { - return rf(ctx, owner, repository, path, ref) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) RepositoryContent); ok { - r0 = rf(ctx, owner, repository, path, ref) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(RepositoryContent) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) []RepositoryContent); ok { - r1 = rf(ctx, owner, repository, path, ref) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).([]RepositoryContent) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string) error); ok { - r2 = rf(ctx, owner, repository, path, ref) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// MockClient_GetContents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContents' -type MockClient_GetContents_Call struct { - *mock.Call -} - -// GetContents is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - path string -// - ref string -func (_e *MockClient_Expecter) GetContents(ctx interface{}, owner interface{}, repository interface{}, path interface{}, ref interface{}) *MockClient_GetContents_Call { - return &MockClient_GetContents_Call{Call: _e.mock.On("GetContents", ctx, owner, repository, path, ref)} -} - -func (_c *MockClient_GetContents_Call) Run(run func(ctx context.Context, owner string, repository string, path string, ref string)) *MockClient_GetContents_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) - }) - return _c -} - -func (_c *MockClient_GetContents_Call) Return(fileContents RepositoryContent, dirContents []RepositoryContent, err error) *MockClient_GetContents_Call { - _c.Call.Return(fileContents, dirContents, err) - return _c -} - -func (_c *MockClient_GetContents_Call) RunAndReturn(run func(context.Context, string, string, string, string) (RepositoryContent, []RepositoryContent, error)) *MockClient_GetContents_Call { - _c.Call.Return(run) - return _c -} - -// GetTree provides a mock function with given fields: ctx, owner, repository, basePath, ref, recursive -func (_m *MockClient) GetTree(ctx context.Context, owner string, repository string, basePath string, ref string, recursive bool) ([]RepositoryContent, bool, error) { - ret := _m.Called(ctx, owner, repository, basePath, ref, recursive) - - if len(ret) == 0 { - panic("no return value specified for GetTree") - } - - var r0 []RepositoryContent - var r1 bool - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool) ([]RepositoryContent, bool, error)); ok { - return rf(ctx, owner, repository, basePath, ref, recursive) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool) []RepositoryContent); ok { - r0 = rf(ctx, owner, repository, basePath, ref, recursive) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]RepositoryContent) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool) bool); ok { - r1 = rf(ctx, owner, repository, basePath, ref, recursive) - } else { - r1 = ret.Get(1).(bool) - } - - if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string, bool) error); ok { - r2 = rf(ctx, owner, repository, basePath, ref, recursive) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// MockClient_GetTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTree' -type MockClient_GetTree_Call struct { - *mock.Call -} - -// GetTree is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - basePath string -// - ref string -// - recursive bool -func (_e *MockClient_Expecter) GetTree(ctx interface{}, owner interface{}, repository interface{}, basePath interface{}, ref interface{}, recursive interface{}) *MockClient_GetTree_Call { - return &MockClient_GetTree_Call{Call: _e.mock.On("GetTree", ctx, owner, repository, basePath, ref, recursive)} -} - -func (_c *MockClient_GetTree_Call) Run(run func(ctx context.Context, owner string, repository string, basePath string, ref string, recursive bool)) *MockClient_GetTree_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(bool)) - }) - return _c -} - -func (_c *MockClient_GetTree_Call) Return(entries []RepositoryContent, truncated bool, err error) *MockClient_GetTree_Call { - _c.Call.Return(entries, truncated, err) - return _c -} - -func (_c *MockClient_GetTree_Call) RunAndReturn(run func(context.Context, string, string, string, string, bool) ([]RepositoryContent, bool, error)) *MockClient_GetTree_Call { - _c.Call.Return(run) - return _c -} - // GetWebhook provides a mock function with given fields: ctx, owner, repository, webhookID func (_m *MockClient) GetWebhook(ctx context.Context, owner string, repository string, webhookID int64) (WebhookConfig, error) { ret := _m.Called(ctx, owner, repository, webhookID) @@ -824,52 +349,6 @@ func (_c *MockClient_GetWebhook_Call) RunAndReturn(run func(context.Context, str return _c } -// IsAuthenticated provides a mock function with given fields: ctx -func (_m *MockClient) IsAuthenticated(ctx context.Context) error { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for IsAuthenticated") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_IsAuthenticated_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAuthenticated' -type MockClient_IsAuthenticated_Call struct { - *mock.Call -} - -// IsAuthenticated is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockClient_Expecter) IsAuthenticated(ctx interface{}) *MockClient_IsAuthenticated_Call { - return &MockClient_IsAuthenticated_Call{Call: _e.mock.On("IsAuthenticated", ctx)} -} - -func (_c *MockClient_IsAuthenticated_Call) Run(run func(ctx context.Context)) *MockClient_IsAuthenticated_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockClient_IsAuthenticated_Call) Return(_a0 error) *MockClient_IsAuthenticated_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_IsAuthenticated_Call) RunAndReturn(run func(context.Context) error) *MockClient_IsAuthenticated_Call { - _c.Call.Return(run) - return _c -} - // ListPullRequestFiles provides a mock function with given fields: ctx, owner, repository, number func (_m *MockClient) ListPullRequestFiles(ctx context.Context, owner string, repository string, number int) ([]CommitFile, error) { ret := _m.Called(ctx, owner, repository, number) @@ -991,117 +470,6 @@ func (_c *MockClient_ListWebhooks_Call) RunAndReturn(run func(context.Context, s return _c } -// RepoExists provides a mock function with given fields: ctx, owner, repository -func (_m *MockClient) RepoExists(ctx context.Context, owner string, repository string) (bool, error) { - ret := _m.Called(ctx, owner, repository) - - if len(ret) == 0 { - panic("no return value specified for RepoExists") - } - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok { - return rf(ctx, owner, repository) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok { - r0 = rf(ctx, owner, repository) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, owner, repository) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_RepoExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RepoExists' -type MockClient_RepoExists_Call struct { - *mock.Call -} - -// RepoExists is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -func (_e *MockClient_Expecter) RepoExists(ctx interface{}, owner interface{}, repository interface{}) *MockClient_RepoExists_Call { - return &MockClient_RepoExists_Call{Call: _e.mock.On("RepoExists", ctx, owner, repository)} -} - -func (_c *MockClient_RepoExists_Call) Run(run func(ctx context.Context, owner string, repository string)) *MockClient_RepoExists_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string)) - }) - return _c -} - -func (_c *MockClient_RepoExists_Call) Return(_a0 bool, _a1 error) *MockClient_RepoExists_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_RepoExists_Call) RunAndReturn(run func(context.Context, string, string) (bool, error)) *MockClient_RepoExists_Call { - _c.Call.Return(run) - return _c -} - -// UpdateFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, hash, content -func (_m *MockClient) UpdateFile(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string, content []byte) error { - ret := _m.Called(ctx, owner, repository, path, branch, message, hash, content) - - if len(ret) == 0 { - panic("no return value specified for UpdateFile") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string, []byte) error); ok { - r0 = rf(ctx, owner, repository, path, branch, message, hash, content) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_UpdateFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateFile' -type MockClient_UpdateFile_Call struct { - *mock.Call -} - -// UpdateFile is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - path string -// - branch string -// - message string -// - hash string -// - content []byte -func (_e *MockClient_Expecter) UpdateFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, hash interface{}, content interface{}) *MockClient_UpdateFile_Call { - return &MockClient_UpdateFile_Call{Call: _e.mock.On("UpdateFile", ctx, owner, repository, path, branch, message, hash, content)} -} - -func (_c *MockClient_UpdateFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string, content []byte)) *MockClient_UpdateFile_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string), args[7].([]byte)) - }) - return _c -} - -func (_c *MockClient_UpdateFile_Call) Return(_a0 error) *MockClient_UpdateFile_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_UpdateFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, string, []byte) error) *MockClient_UpdateFile_Call { - _c.Call.Return(run) - return _c -} - // NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockClient(t interface { diff --git a/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go b/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go deleted file mode 100644 index bddbaaf2558..00000000000 --- a/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go +++ /dev/null @@ -1,312 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package github - -import mock "github.com/stretchr/testify/mock" - -// MockRepositoryContent is an autogenerated mock type for the RepositoryContent type -type MockRepositoryContent struct { - mock.Mock -} - -type MockRepositoryContent_Expecter struct { - mock *mock.Mock -} - -func (_m *MockRepositoryContent) EXPECT() *MockRepositoryContent_Expecter { - return &MockRepositoryContent_Expecter{mock: &_m.Mock} -} - -// GetFileContent provides a mock function with no fields -func (_m *MockRepositoryContent) GetFileContent() (string, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetFileContent") - } - - var r0 string - var r1 error - if rf, ok := ret.Get(0).(func() (string, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockRepositoryContent_GetFileContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFileContent' -type MockRepositoryContent_GetFileContent_Call struct { - *mock.Call -} - -// GetFileContent is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) GetFileContent() *MockRepositoryContent_GetFileContent_Call { - return &MockRepositoryContent_GetFileContent_Call{Call: _e.mock.On("GetFileContent")} -} - -func (_c *MockRepositoryContent_GetFileContent_Call) Run(run func()) *MockRepositoryContent_GetFileContent_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_GetFileContent_Call) Return(_a0 string, _a1 error) *MockRepositoryContent_GetFileContent_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockRepositoryContent_GetFileContent_Call) RunAndReturn(run func() (string, error)) *MockRepositoryContent_GetFileContent_Call { - _c.Call.Return(run) - return _c -} - -// GetPath provides a mock function with no fields -func (_m *MockRepositoryContent) GetPath() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetPath") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// MockRepositoryContent_GetPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPath' -type MockRepositoryContent_GetPath_Call struct { - *mock.Call -} - -// GetPath is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) GetPath() *MockRepositoryContent_GetPath_Call { - return &MockRepositoryContent_GetPath_Call{Call: _e.mock.On("GetPath")} -} - -func (_c *MockRepositoryContent_GetPath_Call) Run(run func()) *MockRepositoryContent_GetPath_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_GetPath_Call) Return(_a0 string) *MockRepositoryContent_GetPath_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_GetPath_Call) RunAndReturn(run func() string) *MockRepositoryContent_GetPath_Call { - _c.Call.Return(run) - return _c -} - -// GetSHA provides a mock function with no fields -func (_m *MockRepositoryContent) GetSHA() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetSHA") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// MockRepositoryContent_GetSHA_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSHA' -type MockRepositoryContent_GetSHA_Call struct { - *mock.Call -} - -// GetSHA is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) GetSHA() *MockRepositoryContent_GetSHA_Call { - return &MockRepositoryContent_GetSHA_Call{Call: _e.mock.On("GetSHA")} -} - -func (_c *MockRepositoryContent_GetSHA_Call) Run(run func()) *MockRepositoryContent_GetSHA_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_GetSHA_Call) Return(_a0 string) *MockRepositoryContent_GetSHA_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_GetSHA_Call) RunAndReturn(run func() string) *MockRepositoryContent_GetSHA_Call { - _c.Call.Return(run) - return _c -} - -// GetSize provides a mock function with no fields -func (_m *MockRepositoryContent) GetSize() int64 { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetSize") - } - - var r0 int64 - if rf, ok := ret.Get(0).(func() int64); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int64) - } - - return r0 -} - -// MockRepositoryContent_GetSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSize' -type MockRepositoryContent_GetSize_Call struct { - *mock.Call -} - -// GetSize is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) GetSize() *MockRepositoryContent_GetSize_Call { - return &MockRepositoryContent_GetSize_Call{Call: _e.mock.On("GetSize")} -} - -func (_c *MockRepositoryContent_GetSize_Call) Run(run func()) *MockRepositoryContent_GetSize_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_GetSize_Call) Return(_a0 int64) *MockRepositoryContent_GetSize_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_GetSize_Call) RunAndReturn(run func() int64) *MockRepositoryContent_GetSize_Call { - _c.Call.Return(run) - return _c -} - -// IsDirectory provides a mock function with no fields -func (_m *MockRepositoryContent) IsDirectory() bool { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for IsDirectory") - } - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// MockRepositoryContent_IsDirectory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsDirectory' -type MockRepositoryContent_IsDirectory_Call struct { - *mock.Call -} - -// IsDirectory is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) IsDirectory() *MockRepositoryContent_IsDirectory_Call { - return &MockRepositoryContent_IsDirectory_Call{Call: _e.mock.On("IsDirectory")} -} - -func (_c *MockRepositoryContent_IsDirectory_Call) Run(run func()) *MockRepositoryContent_IsDirectory_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_IsDirectory_Call) Return(_a0 bool) *MockRepositoryContent_IsDirectory_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_IsDirectory_Call) RunAndReturn(run func() bool) *MockRepositoryContent_IsDirectory_Call { - _c.Call.Return(run) - return _c -} - -// IsSymlink provides a mock function with no fields -func (_m *MockRepositoryContent) IsSymlink() bool { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for IsSymlink") - } - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// MockRepositoryContent_IsSymlink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsSymlink' -type MockRepositoryContent_IsSymlink_Call struct { - *mock.Call -} - -// IsSymlink is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) IsSymlink() *MockRepositoryContent_IsSymlink_Call { - return &MockRepositoryContent_IsSymlink_Call{Call: _e.mock.On("IsSymlink")} -} - -func (_c *MockRepositoryContent_IsSymlink_Call) Run(run func()) *MockRepositoryContent_IsSymlink_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_IsSymlink_Call) Return(_a0 bool) *MockRepositoryContent_IsSymlink_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_IsSymlink_Call) RunAndReturn(run func() bool) *MockRepositoryContent_IsSymlink_Call { - _c.Call.Return(run) - return _c -} - -// NewMockRepositoryContent creates a new instance of MockRepositoryContent. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockRepositoryContent(t interface { - mock.TestingT - Cleanup(func()) -}) *MockRepositoryContent { - mock := &MockRepositoryContent{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/github/repository.go b/pkg/registry/apis/provisioning/repository/github/repository.go new file mode 100644 index 00000000000..4199a225d5a --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/repository.go @@ -0,0 +1,240 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + + "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included. +type githubRepository struct { + gitRepo git.GitRepository + config *provisioning.Repository + gh Client // assumes github.com base URL + + owner string + repo string +} + +// GithubRepository is an interface that combines all repository capabilities +// needed for GitHub repositories. + +//go:generate mockery --name GithubRepository --structname MockGithubRepository --inpackage --filename github_repository_mock.go --with-expecter +type GithubRepository interface { + repository.Repository + repository.Versioned + repository.Writer + repository.Reader + repository.RepositoryWithURLs + repository.StageableRepository + Owner() string + Repo() string + Client() Client +} + +func NewGitHub( + ctx context.Context, + config *provisioning.Repository, + gitRepo git.GitRepository, + factory *Factory, + token string, +) (GithubRepository, error) { + owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL) + if err != nil { + return nil, fmt.Errorf("parse owner and repo: %w", err) + } + + return &githubRepository{ + config: config, + gitRepo: gitRepo, + gh: factory.New(ctx, token), // TODO, baseURL from config + owner: owner, + repo: repo, + }, nil +} + +func (r *githubRepository) Config() *provisioning.Repository { + return r.gitRepo.Config() +} + +func (r *githubRepository) Owner() string { + return r.owner +} + +func (r *githubRepository) Repo() string { + return r.repo +} + +func (r *githubRepository) Client() Client { + return r.gh +} + +// Validate implements provisioning.Repository. +func (r *githubRepository) Validate() (list field.ErrorList) { + cfg := r.gitRepo.Config() + gh := cfg.Spec.GitHub + if gh == nil { + list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required")) + return list + } + if gh.URL == "" { + list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required")) + } else { + _, _, err := ParseOwnerRepoGithub(gh.URL) + if err != nil { + list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error())) + } else if !strings.HasPrefix(gh.URL, "https://github.com/") { + list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/")) + } + } + + if len(list) > 0 { + return list + } + + return r.gitRepo.Validate() +} + +func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) { + parsed, e := url.Parse(strings.TrimSuffix(giturl, ".git")) + if e != nil { + err = e + return + } + parts := strings.Split(parsed.Path, "/") + if len(parts) < 3 { + err = fmt.Errorf("unable to parse repo+owner from url") + return + } + return parts[1], parts[2], nil +} + +// Test implements provisioning.Repository. +func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { + url := r.config.Spec.GitHub.URL + _, _, err := ParseOwnerRepoGithub(url) + if err != nil { + return repository.FromFieldError(field.Invalid( + field.NewPath("spec", "github", "url"), url, err.Error())), nil + } + + return r.gitRepo.Test(ctx) +} + +// ReadResource implements provisioning.Repository. +func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) { + return r.gitRepo.Read(ctx, filePath, ref) +} + +func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { + return r.gitRepo.ReadTree(ctx, ref) +} + +func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error { + return r.gitRepo.Create(ctx, path, ref, data, comment) +} + +func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error { + return r.gitRepo.Update(ctx, path, ref, data, comment) +} + +func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { + return r.gitRepo.Write(ctx, path, ref, data, message) +} + +func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error { + return r.gitRepo.Delete(ctx, path, ref, comment) +} + +func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + + finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) + commits, err := r.gh.Commits(ctx, r.owner, r.repo, finalPath, ref) + if err != nil { + if errors.Is(err, ErrResourceNotFound) { + return nil, repository.ErrFileNotFound + } + + return nil, fmt.Errorf("get commits: %w", err) + } + + ret := make([]provisioning.HistoryItem, 0, len(commits)) + for _, commit := range commits { + authors := make([]provisioning.Author, 0) + if commit.Author != nil { + authors = append(authors, provisioning.Author{ + Name: commit.Author.Name, + Username: commit.Author.Username, + AvatarURL: commit.Author.AvatarURL, + }) + } + + if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name { + authors = append(authors, provisioning.Author{ + Name: commit.Committer.Name, + Username: commit.Committer.Username, + AvatarURL: commit.Committer.AvatarURL, + }) + } + + ret = append(ret, provisioning.HistoryItem{ + Ref: commit.Ref, + Message: commit.Message, + Authors: authors, + CreatedAt: commit.CreatedAt.UnixMilli(), + }) + } + + return ret, nil +} + +func (r *githubRepository) LatestRef(ctx context.Context) (string, error) { + return r.gitRepo.LatestRef(ctx) +} + +func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) { + return r.gitRepo.CompareFiles(ctx, base, ref) +} + +// ResourceURLs implements RepositoryWithURLs. +func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*provisioning.ResourceURLs, error) { + cfg := r.config.Spec.GitHub + if file.Path == "" || cfg == nil { + return nil, nil + } + + ref := file.Ref + if ref == "" { + ref = cfg.Branch + } + + urls := &provisioning.ResourceURLs{ + RepositoryURL: cfg.URL, + SourceURL: fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path), + } + + if ref != cfg.Branch { + urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref) + + // Create a new pull request + urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL) + } + + return urls, nil +} + +func (r *githubRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { + return r.gitRepo.Stage(ctx, opts) +} diff --git a/pkg/registry/apis/provisioning/repository/github/repository_test.go b/pkg/registry/apis/provisioning/repository/github/repository_test.go new file mode 100644 index 00000000000..374ec001a8b --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/repository_test.go @@ -0,0 +1,1014 @@ +package github + +import ( + "context" + "errors" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + field "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" +) + +func TestNewGitHub(t *testing.T) { + tests := []struct { + name string + config *provisioning.Repository + token string + expectedError string + expectedOwner string + expectedRepo string + }{ + { + name: "successful creation", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, + token: "token123", + expectedError: "", + expectedOwner: "grafana", + expectedRepo: "grafana", + }, + { + name: "invalid URL format", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "invalid-url", + Branch: "main", + }, + }, + }, + token: "token123", + expectedError: "parse owner and repo", + }, + { + name: "URL with .git extension", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana.git", + Branch: "main", + }, + }, + }, + token: "token123", + expectedError: "", + expectedOwner: "grafana", + expectedRepo: "grafana", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := ProvideFactory() + factory.Client = http.DefaultClient + + gitRepo := git.NewMockGitRepository(t) + + // Call the function under test + repo, err := NewGitHub( + context.Background(), + tt.config, + gitRepo, + factory, + tt.token, + ) + + // Check results + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, repo) + } else { + require.NoError(t, err) + require.NotNil(t, repo) + assert.Equal(t, tt.expectedOwner, repo.Owner()) + assert.Equal(t, tt.expectedRepo, repo.Repo()) + concreteRepo, ok := repo.(*githubRepository) + require.True(t, ok) + assert.Equal(t, gitRepo, concreteRepo.gitRepo) + } + }) + } +} + +func TestParseOwnerRepoGithub(t *testing.T) { + tests := []struct { + name string + url string + expectedOwner string + expectedRepo string + expectedError string + }{ + { + name: "valid GitHub URL", + url: "https://github.com/grafana/grafana", + expectedOwner: "grafana", + expectedRepo: "grafana", + }, + { + name: "valid GitHub URL with .git", + url: "https://github.com/grafana/grafana.git", + expectedOwner: "grafana", + expectedRepo: "grafana", + }, + { + name: "invalid URL format", + url: "invalid-url", + expectedError: "parse", + }, + { + name: "missing repo name", + url: "https://github.com/grafana", + expectedError: "unable to parse repo+owner from url", + }, + { + name: "URL with special characters", + url: "https://github.com/user%", + expectedError: "parse", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + owner, repo, err := ParseOwnerRepoGithub(tt.url) + + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedOwner, owner) + assert.Equal(t, tt.expectedRepo, repo) + } + }) + } +} + +func TestGitHubRepositoryValidate(t *testing.T) { + tests := []struct { + name string + config *provisioning.Repository + mockSetup func(m *git.MockGitRepository) + expectedErrors int + errorFields []string + }{ + { + name: "valid configuration", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + Path: "dashboards", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + Path: "dashboards", + }, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + }, + expectedErrors: 0, + }, + { + name: "missing GitHub config", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: nil, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: nil, + }, + }) + }, + expectedErrors: 1, + errorFields: []string{"spec.github"}, + }, + { + name: "missing URL", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "", + Branch: "main", + Token: "valid-token", + }, + }, + }) + }, + expectedErrors: 1, + errorFields: []string{"spec.github.url"}, + }, + { + name: "invalid URL format", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "invalid-url", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "invalid-url", + Branch: "main", + Token: "valid-token", + }, + }, + }) + }, + expectedErrors: 1, + errorFields: []string{"spec.github.url"}, + }, + { + name: "non-GitHub URL", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://gitlab.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://gitlab.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + }, + }, + }) + }, + expectedErrors: 1, + errorFields: []string{"spec.github.url"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + if tt.mockSetup != nil { + tt.mockSetup(mockGitRepo) + } + + repo := &githubRepository{ + config: tt.config, + gitRepo: mockGitRepo, + } + + errors := repo.Validate() + + assert.Equal(t, tt.expectedErrors, len(errors), "Expected %d errors, got %d, errors: %v", tt.expectedErrors, len(errors), errors) + + if tt.expectedErrors > 0 { + errorFields := make([]string, 0, len(errors)) + for _, err := range errors { + errorFields = append(errorFields, err.Field) + } + for _, expectedField := range tt.errorFields { + assert.Contains(t, errorFields, expectedField, "Expected error for field %s", expectedField) + } + } + + mockGitRepo.AssertExpectations(t) + }) + } +} + +func TestGitHubRepositoryTest(t *testing.T) { + tests := []struct { + name string + config *provisioning.Repository + mockSetup func(m *git.MockGitRepository) + expectedResult *provisioning.TestResults + expectedError error + }{ + { + name: "successful test", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Test", mock.Anything).Return(&provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, nil) + }, + expectedResult: &provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, + }, + { + name: "invalid URL", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "invalid-url", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(_ *git.MockGitRepository) { + // No mock calls expected as validation fails first + }, + expectedResult: &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "spec.github.url", + Detail: "parse \"invalid-url\": invalid URI for request", + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + if tt.mockSetup != nil { + tt.mockSetup(mockGitRepo) + } + + repo := &githubRepository{ + config: tt.config, + gitRepo: mockGitRepo, + owner: "grafana", + repo: "grafana", + } + + result, err := repo.Test(context.Background()) + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + + if tt.expectedResult != nil { + assert.Equal(t, tt.expectedResult.Code, result.Code) + assert.Equal(t, tt.expectedResult.Success, result.Success) + if len(tt.expectedResult.Errors) > 0 { + assert.Equal(t, len(tt.expectedResult.Errors), len(result.Errors)) + for i, expectedError := range tt.expectedResult.Errors { + assert.Equal(t, expectedError.Type, result.Errors[i].Type) + assert.Equal(t, expectedError.Field, result.Errors[i].Field) + assert.Contains(t, result.Errors[i].Detail, "parse") + } + } + } + + mockGitRepo.AssertExpectations(t) + }) + } +} + +func TestGitHubRepositoryHistory(t *testing.T) { + tests := []struct { + name string + config *provisioning.Repository + path string + ref string + mockSetup func(m *MockClient) + expectedResult []provisioning.HistoryItem + expectedError error + }{ + { + name: "successful history retrieval", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "main", + mockSetup: func(m *MockClient) { + commits := []Commit{ + { + Ref: "abc123", + Message: "Update dashboard", + Author: &CommitAuthor{ + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + Committer: &CommitAuthor{ + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(commits, nil) + }, + expectedResult: []provisioning.HistoryItem{ + { + Ref: "abc123", + Message: "Update dashboard", + Authors: []provisioning.Author{ + { + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), + }, + }, + }, + { + name: "file not found", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "nonexistent.json", + ref: "main", + mockSetup: func(m *MockClient) { + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/nonexistent.json", "main"). + Return(nil, ErrResourceNotFound) + }, + expectedError: repository.ErrFileNotFound, + }, + { + name: "use default branch when ref is empty", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "", + mockSetup: func(m *MockClient) { + commits := []Commit{ + { + Ref: "abc123", + Message: "Update dashboard", + Author: &CommitAuthor{ + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(commits, nil) + }, + expectedResult: []provisioning.HistoryItem{ + { + Ref: "abc123", + Message: "Update dashboard", + Authors: []provisioning.Author{ + { + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), + }, + }, + }, + { + name: "committer different from author", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "main", + mockSetup: func(m *MockClient) { + commits := []Commit{ + { + Ref: "abc123", + Message: "Update dashboard", + Author: &CommitAuthor{ + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + Committer: &CommitAuthor{ + Name: "Jane Smith", + Username: "janesmith", + AvatarURL: "https://example.com/avatar2.png", + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(commits, nil) + }, + expectedResult: []provisioning.HistoryItem{ + { + Ref: "abc123", + Message: "Update dashboard", + Authors: []provisioning.Author{ + { + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + { + Name: "Jane Smith", + Username: "janesmith", + AvatarURL: "https://example.com/avatar2.png", + }, + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), + }, + }, + }, + { + name: "commit with no author", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "main", + mockSetup: func(m *MockClient) { + commits := []Commit{ + { + Ref: "abc123", + Message: "Update dashboard", + Author: nil, + Committer: nil, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(commits, nil) + }, + expectedResult: []provisioning.HistoryItem{ + { + Ref: "abc123", + Message: "Update dashboard", + Authors: []provisioning.Author{}, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), + }, + }, + }, + { + name: "other API error", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "main", + mockSetup: func(m *MockClient) { + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(nil, errors.New("API error")) + }, + expectedError: errors.New("get commits: API error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := NewMockClient(t) + if tt.mockSetup != nil { + tt.mockSetup(mockClient) + } + + repo := &githubRepository{ + config: tt.config, + gh: mockClient, + owner: "grafana", + repo: "grafana", + } + + history, err := repo.History(context.Background(), tt.path, tt.ref) + + if tt.expectedError != nil { + require.Error(t, err) + var statusErr *apierrors.StatusError + if errors.As(tt.expectedError, &statusErr) { + var actualStatusErr *apierrors.StatusError + require.True(t, errors.As(err, &actualStatusErr)) + require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) + } else { + require.Equal(t, tt.expectedError.Error(), err.Error()) + } + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedResult, history) + } + + mockClient.AssertExpectations(t) + }) + } +} + +func TestGitHubRepositoryResourceURLs(t *testing.T) { + tests := []struct { + name string + file *repository.FileInfo + config *provisioning.Repository + expectedURLs *provisioning.ResourceURLs + expectedError error + }{ + { + name: "file with ref", + file: &repository.FileInfo{ + Path: "dashboards/test.json", + Ref: "feature-branch", + }, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, + expectedURLs: &provisioning.ResourceURLs{ + RepositoryURL: "https://github.com/grafana/grafana", + SourceURL: "https://github.com/grafana/grafana/blob/feature-branch/dashboards/test.json", + CompareURL: "https://github.com/grafana/grafana/compare/main...feature-branch", + NewPullRequestURL: "https://github.com/grafana/grafana/compare/main...feature-branch?quick_pull=1&labels=grafana", + }, + }, + { + name: "file without ref uses default branch", + file: &repository.FileInfo{ + Path: "dashboards/test.json", + Ref: "", + }, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, + expectedURLs: &provisioning.ResourceURLs{ + RepositoryURL: "https://github.com/grafana/grafana", + SourceURL: "https://github.com/grafana/grafana/blob/main/dashboards/test.json", + }, + }, + { + name: "empty path returns nil", + file: &repository.FileInfo{ + Path: "", + Ref: "feature-branch", + }, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, + expectedURLs: nil, + }, + { + name: "nil github config returns nil", + file: &repository.FileInfo{ + Path: "dashboards/test.json", + Ref: "feature-branch", + }, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: nil, + }, + }, + expectedURLs: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &githubRepository{ + config: tt.config, + owner: "grafana", + repo: "grafana", + } + + urls, err := repo.ResourceURLs(context.Background(), tt.file) + + if tt.expectedError != nil { + require.Error(t, err) + require.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedURLs, urls) + } + }) + } +} + +// Test simple delegation functions +func TestGitHubRepositoryDelegation(t *testing.T) { + ctx := context.Background() + + config := &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "test-token", + }, + }, + } + + t.Run("Config delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + mockGitRepo.On("Config").Return(config) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result := repo.Config() + assert.Equal(t, config, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Read delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + expectedFileInfo := &repository.FileInfo{ + Path: "test.yaml", + Data: []byte("test data"), + Ref: "main", + Hash: "abc123", + } + mockGitRepo.On("Read", ctx, "test.yaml", "main").Return(expectedFileInfo, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.Read(ctx, "test.yaml", "main") + require.NoError(t, err) + assert.Equal(t, expectedFileInfo, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("ReadTree delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + expectedEntries := []repository.FileTreeEntry{ + {Path: "file1.yaml", Size: 100, Hash: "hash1", Blob: true}, + } + mockGitRepo.On("ReadTree", ctx, "main").Return(expectedEntries, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.ReadTree(ctx, "main") + require.NoError(t, err) + assert.Equal(t, expectedEntries, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Create delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + data := []byte("test content") + mockGitRepo.On("Create", ctx, "new-file.yaml", "main", data, "Create new file").Return(nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + err := repo.Create(ctx, "new-file.yaml", "main", data, "Create new file") + require.NoError(t, err) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Update delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + data := []byte("updated content") + mockGitRepo.On("Update", ctx, "existing-file.yaml", "main", data, "Update file").Return(nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + err := repo.Update(ctx, "existing-file.yaml", "main", data, "Update file") + require.NoError(t, err) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Write delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + data := []byte("file content") + mockGitRepo.On("Write", ctx, "file.yaml", "main", data, "Write file").Return(nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + err := repo.Write(ctx, "file.yaml", "main", data, "Write file") + require.NoError(t, err) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Delete delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + mockGitRepo.On("Delete", ctx, "file.yaml", "main", "Delete file").Return(nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + err := repo.Delete(ctx, "file.yaml", "main", "Delete file") + require.NoError(t, err) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("LatestRef delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + expectedRef := "abc123def456" + mockGitRepo.On("LatestRef", ctx).Return(expectedRef, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.LatestRef(ctx) + require.NoError(t, err) + assert.Equal(t, expectedRef, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("CompareFiles delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + expectedChanges := []repository.VersionedFileChange{ + { + Action: repository.FileActionCreated, + Path: "new-file.yaml", + Ref: "feature-branch", + }, + } + mockGitRepo.On("CompareFiles", ctx, "main", "feature-branch").Return(expectedChanges, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.CompareFiles(ctx, "main", "feature-branch") + require.NoError(t, err) + assert.Equal(t, expectedChanges, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Stage delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + mockStagedRepo := repository.NewMockStagedRepository(t) + opts := repository.StageOptions{ + PushOnWrites: true, + Timeout: 10 * time.Second, + } + mockGitRepo.On("Stage", ctx, opts).Return(mockStagedRepo, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.Stage(ctx, opts) + require.NoError(t, err) + assert.Equal(t, mockStagedRepo, result) + mockGitRepo.AssertExpectations(t) + }) +} + +// Test GitHub-specific accessor methods +func TestGitHubRepositoryAccessors(t *testing.T) { + config := &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "test-token", + }, + }, + } + + t.Run("Owner returns correct owner", func(t *testing.T) { + repo := &githubRepository{ + config: config, + owner: "grafana", + repo: "grafana", + } + + result := repo.Owner() + assert.Equal(t, "grafana", result) + }) + + t.Run("Repo returns correct repo", func(t *testing.T) { + repo := &githubRepository{ + config: config, + owner: "grafana", + repo: "grafana", + } + + result := repo.Repo() + assert.Equal(t, "grafana", result) + }) + + t.Run("Client returns correct client", func(t *testing.T) { + mockClient := NewMockClient(t) + + repo := &githubRepository{ + config: config, + gh: mockClient, + owner: "grafana", + repo: "grafana", + } + + result := repo.Client() + assert.Equal(t, mockClient, result) + }) +} diff --git a/pkg/registry/apis/provisioning/repository/github_test.go b/pkg/registry/apis/provisioning/repository/github_test.go deleted file mode 100644 index 3c0e499713e..00000000000 --- a/pkg/registry/apis/provisioning/repository/github_test.go +++ /dev/null @@ -1,3143 +0,0 @@ -package repository - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - field "k8s.io/apimachinery/pkg/util/validation/field" - - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" -) - -func TestNewGitHub(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - setupMock func(m *secrets.MockService) - expectedError string - expectedRepo *githubRepository - }{ - { - name: "successful creation with token", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Token: "token123", - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - // No mock calls expected since we're using the token directly - }, - expectedError: "", - expectedRepo: &githubRepository{ - owner: "grafana", - repo: "grafana", - }, - }, - { - name: "successful creation with encrypted token", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - EncryptedToken: []byte("encrypted-token"), - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - m.On("Decrypt", mock.Anything, []byte("encrypted-token")). - Return([]byte("decrypted-token"), nil) - }, - expectedError: "", - expectedRepo: &githubRepository{ - owner: "grafana", - repo: "grafana", - }, - }, - { - name: "error decrypting token", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - EncryptedToken: []byte("encrypted-token"), - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - m.On("Decrypt", mock.Anything, []byte("encrypted-token")). - Return(nil, fmt.Errorf("decryption error")) - }, - expectedError: "decrypt token: decryption error", - }, - { - name: "invalid URL format", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "invalid-url", - Token: "token123", - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - // No mock calls expected - }, - expectedError: "parse owner and repo", - expectedRepo: &githubRepository{ - owner: "", - repo: "", - }, - }, - { - name: "URL with .git extension", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana.git", - Token: "token123", - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - // No mock calls expected - }, - expectedError: "", - expectedRepo: &githubRepository{ - owner: "grafana", - repo: "grafana", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mocks - mockSecrets := secrets.NewMockService(t) - if tt.setupMock != nil { - tt.setupMock(mockSecrets) - } - - factory := pgh.ProvideFactory() - factory.Client = http.DefaultClient - - // Create a mock clone function - cloneFn := func(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - return nil, nil - } - - // Call the function under test - repo, err := NewGitHub( - context.Background(), - tt.config, - factory, - mockSecrets, - cloneFn, - ) - - // Check results - if tt.expectedError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - assert.Nil(t, repo) - } else { - require.NoError(t, err) - require.NotNil(t, repo) - assert.Equal(t, tt.expectedRepo.owner, repo.Owner()) - assert.Equal(t, tt.expectedRepo.repo, repo.Repo()) - assert.Equal(t, tt.config, repo.Config()) - concreteRepo, ok := repo.(*githubRepository) - require.True(t, ok) - assert.Equal(t, mockSecrets, concreteRepo.secrets) - assert.NotNil(t, concreteRepo.cloneFn) - } - - // Verify all mock expectations were met - mockSecrets.AssertExpectations(t) - }) - } -} - -func TestIsValidGitBranchName(t *testing.T) { - tests := []struct { - name string - branch string - expected bool - }{ - {"Valid branch name", "feature/add-tests", true}, - {"Valid branch name with numbers", "feature/123-add-tests", true}, - {"Valid branch name with dots", "feature.add.tests", true}, - {"Valid branch name with hyphens", "feature-add-tests", true}, - {"Valid branch name with underscores", "feature_add_tests", true}, - {"Valid branch name with mixed characters", "feature/add_tests-123", true}, - {"Starts with /", "/feature", false}, - {"Ends with /", "feature/", false}, - {"Ends with .", "feature.", false}, - {"Ends with space", "feature ", false}, - {"Contains consecutive slashes", "feature//branch", false}, - {"Contains consecutive dots", "feature..branch", false}, - {"Contains @{", "feature@{branch", false}, - {"Contains invalid character ~", "feature~branch", false}, - {"Contains invalid character ^", "feature^branch", false}, - {"Contains invalid character :", "feature:branch", false}, - {"Contains invalid character ?", "feature?branch", false}, - {"Contains invalid character *", "feature*branch", false}, - {"Contains invalid character [", "feature[branch", false}, - {"Contains invalid character ]", "feature]branch", false}, - {"Contains invalid character \\", "feature\\branch", false}, - {"Empty branch name", "", false}, - {"Only whitespace", " ", false}, - {"Single valid character", "a", true}, - {"Ends with .lock", "feature.lock", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, IsValidGitBranchName(tt.branch)) - }) - } -} -func TestGitHubRepositoryValidate(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - expectedErrors int - errorFields []string - }{ - { - name: "Valid configuration", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 0, - }, - { - name: "Valid configuration with .git suffix", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana.git", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 0, - }, - { - name: "Missing GitHub config", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: nil, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github"}, - }, - { - name: "Missing URL", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "Invalid URL format", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "invalid-url", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "Fail to parse URL", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/user%", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "URL not starting with https://github.com/", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://gitlab.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "Missing repo name", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "Missing branch", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.branch"}, - }, - { - name: "Invalid branch name", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "feature//invalid", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.branch"}, - }, - { - name: "Missing token", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.token"}, - }, - { - name: "Unsafe path", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - Path: "../dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.prefix"}, - }, - { - name: "Absolute path", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - Path: "/dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.prefix"}, - }, - { - name: "Multiple errors", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "", - Branch: "", - Token: "", - Path: "/dashboards", - }, - }, - }, - expectedErrors: 4, - errorFields: []string{"spec.github.url", "spec.github.branch", "spec.github.token", "spec.github.prefix"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a GitHub repository with the test config - repo := &githubRepository{ - config: tt.config, - } - - // Validate the configuration - errors := repo.Validate() - - // Check the number of errors - assert.Equal(t, tt.expectedErrors, len(errors), "Expected %d errors, got %d, errors: %v", tt.expectedErrors, len(errors), errors) - - // If we expect errors, check that they are for the right fields - if tt.expectedErrors > 0 { - errorFields := make([]string, 0, len(errors)) - for _, err := range errors { - errorFields = append(errorFields, err.Field) - } - for _, expectedField := range tt.errorFields { - assert.Contains(t, errorFields, expectedField, "Expected error for field %s", expectedField) - } - } - }) - } -} - -func TestGitHubRepository_Test(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - mockSetup func(t *testing.T, client *pgh.MockClient) - expectedResult *provisioning.TestResults - expectedError error - }{ - { - name: "Authentication failure", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "invalid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(errors.New("authentication failed")) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueInvalid, - Field: "spec.github.token", - Detail: "authentication failed", - }}, - }, - expectedError: nil, - }, - { - name: "Invalid URL", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/invalid", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueInvalid, - Field: "spec.github.url", - Detail: "unable to parse repo+owner from url", - }}, - }, - expectedError: nil, - }, - { - name: "Failed to check if repo exists", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/nonexistent", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "nonexistent").Return(false, errors.New("failed to check if repo exists")) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(field.ErrorTypeInvalid), - Field: "spec.github.url", - Detail: "failed to check if repo exists", - }}, - }, - expectedError: nil, - }, - { - name: "Repository does not exist", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/nonexistent", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "nonexistent").Return(false, nil) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(field.ErrorTypeNotFound), - Field: "spec.github.url", - }}, - }, - expectedError: nil, - }, - { - name: "Branch does not exist", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "nonexistent-branch", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "grafana").Return(true, nil) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "nonexistent-branch").Return(false, nil) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(field.ErrorTypeNotFound), - Field: "spec.github.branch", - }}, - }, - expectedError: nil, - }, - { - name: "Branch check error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "grafana").Return(true, nil) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "main").Return(false, errors.New("API rate limit exceeded")) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(field.ErrorTypeInvalid), - Field: "spec.github.branch", - Detail: "API rate limit exceeded", - }}, - }, - expectedError: nil, - }, - { - name: "Successful test", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "grafana").Return(true, nil) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "main").Return(true, nil) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusOK, - Success: true, - }, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // If the config has a different URL, parse and set the owner/repo - if tt.config.Spec.GitHub.URL != "https://github.com/grafana/grafana" { - owner, githubRepo, _ := ParseOwnerRepoGithub(tt.config.Spec.GitHub.URL) - repo.owner = owner - repo.repo = githubRepo - } - - // Test the repository - result, err := repo.Test(context.Background()) - - // Check the error - if tt.expectedError != nil { - assert.Error(t, err) - assert.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - assert.NoError(t, err) - } - - // Check the result - if tt.expectedResult != nil { - assert.Equal(t, tt.expectedResult.Code, result.Code) - assert.Equal(t, tt.expectedResult.Success, result.Success) - - if len(tt.expectedResult.Errors) > 0 { - assert.Equal(t, len(tt.expectedResult.Errors), len(result.Errors)) - - for i, expectedError := range tt.expectedResult.Errors { - assert.Equal(t, expectedError.Type, result.Errors[i].Type) - assert.Equal(t, expectedError.Field, result.Errors[i].Field) - assert.Equal(t, expectedError.Detail, result.Errors[i].Detail) - } - } - } - - // Verify all expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestReadTree(t *testing.T) { - tests := []struct { - name string - path string - ref string - expectedRef string - tree []pgh.RepositoryContent - expected []FileTreeEntry - getTreeErr error - truncated bool - expectedError error - }{ - {name: "empty ref", ref: "", expectedRef: "develop", tree: []pgh.RepositoryContent{}, expected: []FileTreeEntry{}}, - {name: "unknown error to get tree", ref: "develop", expectedRef: "develop", tree: []pgh.RepositoryContent{}, getTreeErr: errors.New("unknown error"), expectedError: errors.New("get tree: unknown error")}, - {name: "tree not found error", ref: "develop", expectedRef: "develop", tree: []pgh.RepositoryContent{}, getTreeErr: pgh.ErrResourceNotFound, expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "tree not found; ref=develop", - Code: http.StatusNotFound, - }, - }}, - {name: "tree truncated", ref: "develop", expectedRef: "develop", tree: []pgh.RepositoryContent{}, truncated: true, expectedError: errors.New("tree truncated")}, - {name: "empty tree", ref: "develop", expectedRef: "develop", tree: []pgh.RepositoryContent{}, expected: []FileTreeEntry{}}, - {name: "single file", ref: "develop", expectedRef: "develop", tree: func() []pgh.RepositoryContent { - content := pgh.NewMockRepositoryContent(t) - content.EXPECT().GetPath().Return("file.txt") - content.EXPECT().GetSize().Return(int64(100)) - content.EXPECT().GetSHA().Return("abc123") - content.EXPECT().IsDirectory().Return(false) - return []pgh.RepositoryContent{content} - }(), expected: []FileTreeEntry{ - {Path: "file.txt", Size: 100, Hash: "abc123", Blob: true}, - }}, - {name: "single directory", ref: "develop", expectedRef: "develop", tree: func() []pgh.RepositoryContent { - content := pgh.NewMockRepositoryContent(t) - content.EXPECT().GetPath().Return("dir") - content.EXPECT().IsDirectory().Return(true) - content.EXPECT().GetSize().Return(int64(0)) - content.EXPECT().GetSHA().Return("") - - return []pgh.RepositoryContent{content} - }(), expected: []FileTreeEntry{ - {Path: "dir/", Blob: false}, - }}, - {name: "mixed content", ref: "develop", expectedRef: "develop", tree: func() []pgh.RepositoryContent { - file1 := pgh.NewMockRepositoryContent(t) - file1.EXPECT().GetPath().Return("file1.txt") - file1.EXPECT().GetSize().Return(int64(100)) - file1.EXPECT().GetSHA().Return("abc123") - file1.EXPECT().IsDirectory().Return(false) - - dir := pgh.NewMockRepositoryContent(t) - dir.EXPECT().GetPath().Return("dir") - dir.EXPECT().IsDirectory().Return(true) - dir.EXPECT().GetSize().Return(int64(0)) - dir.EXPECT().GetSHA().Return("") - - file2 := pgh.NewMockRepositoryContent(t) - file2.EXPECT().GetPath().Return("file2.txt") - file2.EXPECT().GetSize().Return(int64(200)) - file2.EXPECT().GetSHA().Return("def456") - file2.EXPECT().IsDirectory().Return(false) - - return []pgh.RepositoryContent{file1, dir, file2} - }(), expected: []FileTreeEntry{ - {Path: "file1.txt", Size: 100, Hash: "abc123", Blob: true}, - {Path: "dir/", Blob: false}, - {Path: "file2.txt", Size: 200, Hash: "def456", Blob: true}, - }}, - {name: "with path prefix", ref: "develop", expectedRef: "develop", tree: func() []pgh.RepositoryContent { - file := pgh.NewMockRepositoryContent(t) - file.EXPECT().GetPath().Return("file.txt") - file.EXPECT().GetSize().Return(int64(100)) - file.EXPECT().GetSHA().Return("abc123") - file.EXPECT().IsDirectory().Return(false) - - dir := pgh.NewMockRepositoryContent(t) - dir.EXPECT().GetPath().Return("dir") - dir.EXPECT().GetSize().Return(int64(0)) - dir.EXPECT().GetSHA().Return("") - dir.EXPECT().IsDirectory().Return(true) - - return []pgh.RepositoryContent{file, dir} - }(), expected: []FileTreeEntry{ - {Path: "file.txt", Size: 100, Hash: "abc123", Blob: true}, - {Path: "dir/", Blob: false}, - }}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ghMock := pgh.NewMockClient(t) - gh := &githubRepository{ - owner: "owner", - repo: "repo", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: tt.path, - Branch: "develop", - }, - }, - }, - gh: ghMock, - } - - ghMock.On("GetTree", mock.Anything, "owner", "repo", tt.path, tt.expectedRef, true).Return(tt.tree, tt.truncated, tt.getTreeErr) - tree, err := gh.ReadTree(context.Background(), tt.ref) - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, tt.expected, tree) - } - }) - } -} - -func TestGitHubRepository_Read(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - filePath string - ref string - mockSetup func(t *testing.T, client *pgh.MockClient) - expectedResult *FileInfo - expectedError error - }{ - { - name: "File found successfully", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("file content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboard.json", "main"). - Return(fileContent, nil, nil) - }, - expectedResult: &FileInfo{ - Path: "dashboard.json", - Ref: "main", - Data: []byte("file content"), - Hash: "abc123", - }, - expectedError: nil, - }, - { - name: "Directory found successfully", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "dashboards", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - dirContent := []pgh.RepositoryContent{ - // Directory contents not used in this test - } - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboards", "main"). - Return(nil, dirContent, nil) - }, - expectedResult: &FileInfo{ - Path: "dashboards", - Ref: "main", - }, - expectedError: nil, - }, - { - name: "File not found", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "nonexistent.json", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/nonexistent.json", "main"). - Return(nil, nil, pgh.ErrResourceNotFound) - }, - expectedResult: nil, - expectedError: ErrFileNotFound, - }, - { - name: "Error getting file content", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("", errors.New("failed to decode content")) - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboard.json", "main"). - Return(fileContent, nil, nil) - }, - expectedResult: nil, - expectedError: fmt.Errorf("get content: %w", errors.New("failed to decode content")), - }, - { - name: "GitHub API error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboard.json", "main"). - Return(nil, nil, errors.New("API rate limit exceeded")) - }, - expectedResult: nil, - expectedError: fmt.Errorf("get contents: %w", errors.New("API rate limit exceeded")), - }, - { - name: "Use default branch when ref is empty", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "develop", - }, - }, - }, - filePath: "dashboard.json", - ref: "", // Empty ref should use default branch - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("file content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboard.json", "develop"). - Return(fileContent, nil, nil) - }, - expectedResult: &FileInfo{ - Path: "dashboard.json", - Ref: "develop", - Data: []byte("file content"), - Hash: "abc123", - }, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Read method - result, err := repo.Read(context.Background(), tt.filePath, tt.ref) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.expectedError, &statusErr) { - var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - } else { - require.Equal(t, tt.expectedError.Error(), err.Error()) - } - } else { - require.NoError(t, err) - } - - // Check the result - if tt.expectedResult != nil { - require.Equal(t, tt.expectedResult.Path, result.Path) - require.Equal(t, tt.expectedResult.Ref, result.Ref) - require.Equal(t, tt.expectedResult.Data, result.Data) - require.Equal(t, tt.expectedResult.Hash, result.Hash) - } else { - require.Nil(t, result) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_Create(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - data []byte - comment string - mockSetup func(t *testing.T, mockClient *pgh.MockClient) - expectedError error - }{ - { - name: "successful file creation", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "feature-branch", "Add new dashboard", []byte("dashboard content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "create with default branch", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", "Add new dashboard", []byte("dashboard content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "branch already exists error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - mockClient.EXPECT().CreateBranch(mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(pgh.ErrResourceAlreadyExists) - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusConflict, - Message: "branch already exists", - }, - }, - }, - { - name: "branch does not exist error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - mockClient.EXPECT().CreateBranch(mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(fmt.Errorf("failed to create branch")) - }, - expectedError: fmt.Errorf("create branch: %w", fmt.Errorf("failed to create branch")), - }, - { - name: "branch does not exist but it's created", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - mockClient.EXPECT().CreateBranch(mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "feature-branch", "Add new dashboard", []byte("dashboard content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "invalid branch name", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature//branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - // No mock expectations needed as validation should fail before any GitHub API calls - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusBadRequest, - Message: "invalid branch name", - }, - }, - }, - { - name: "branch exists check fails", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(false, fmt.Errorf("failed to check branch")) - }, - expectedError: fmt.Errorf("check branch exists: %w", fmt.Errorf("failed to check branch")), - }, - { - name: "file already exists", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", "Add new dashboard", []byte("dashboard content")).Return(pgh.ErrResourceAlreadyExists) - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "file already exists", - Code: http.StatusConflict, - }, - }, - }, - { - name: "create directory with .keep file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboards/", - ref: "main", - data: nil, - comment: "Add dashboards directory", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/.keep", "main", "Add dashboards directory", []byte{}).Return(nil) - }, - expectedError: nil, - }, - { - name: "error when providing data for directory", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboards/", - ref: "main", - data: []byte("some data"), - comment: "Add dashboards directory", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - }, - expectedError: apierrors.NewBadRequest("data cannot be provided for a directory"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Create method - err := repo.Create(context.Background(), tt.path, tt.ref, tt.data, tt.comment) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.expectedError, &statusErr) { - var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - } else { - require.Equal(t, tt.expectedError.Error(), err.Error()) - } - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_Update(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - data []byte - comment string - mockSetup func(t *testing.T, client *pgh.MockClient) - expectedError error - }{ - { - name: "Successfully update file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(fileContent, nil, nil) - client.On("UpdateFile", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch", - "Update test file", "abc123", []byte("updated content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "Use default branch when ref is empty", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "main").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "main"). - Return(fileContent, nil, nil) - client.On("UpdateFile", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "main", - "Update test file", "abc123", []byte("updated content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "Branch does not exist", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - client.On("CreateBranch", mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(errors.New("failed to create branch")) - }, - expectedError: errors.New("create branch: failed to create branch"), - }, - { - name: "Invalid branch name", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "invalid//branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - // No mock calls expected - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusBadRequest, - Message: "invalid branch name", - }, - }, - }, - { - name: "Branch exists check fails", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(false, errors.New("failed to check branch")) - }, - expectedError: errors.New("check branch exists: failed to check branch"), - }, - { - name: "Branch does not exist but it's created successfully", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - client.On("CreateBranch", mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(nil) - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(fileContent, nil, nil) - client.On("UpdateFile", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch", - "Update test file", "abc123", []byte("updated content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "Branch already exists error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - client.On("CreateBranch", mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(pgh.ErrResourceAlreadyExists) - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusConflict, - Message: "branch already exists", - }, - }, - }, - { - name: "File not found", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(nil, nil, pgh.ErrResourceNotFound) - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "file not found", - Code: http.StatusNotFound, - }, - }, - }, - { - name: "Error getting file contents", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(nil, nil, errors.New("API error")) - }, - expectedError: errors.New("get content before file update: API error"), - }, - { - name: "Cannot update directory", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/directory", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test directory", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - - // Create a directory file - dirFile := pgh.NewMockRepositoryContent(t) - dirFile.EXPECT().IsDirectory().Return(true) - - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/directory", "feature-branch"). - Return(dirFile, nil, nil) - }, - expectedError: apierrors.NewBadRequest("cannot update a directory"), - }, - { - name: "Error updating file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(fileContent, nil, nil) - client.On("UpdateFile", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch", - "Update test file", "abc123", []byte("updated content")).Return(errors.New("update failed")) - }, - expectedError: errors.New("update file: update failed"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Update method - err := repo.Update(context.Background(), tt.path, tt.ref, tt.data, tt.comment) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.expectedError, &statusErr) { - var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - } else { - require.Equal(t, tt.expectedError.Error(), err.Error()) - } - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_Write(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - data []byte - message string - mockSetup func(t *testing.T, mockClient *pgh.MockClient) - expectedError error - }{ - { - name: "write to existing file (update)", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - data: []byte("updated content"), - message: "Update dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("existing content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().UpdateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Update dashboard", "abc123", []byte("updated content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "write to non-existing file (create)", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "new-dashboard.json", - ref: "main", - data: []byte("new content"), - message: "Create new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/new-dashboard.json", "main"). - Return(nil, nil, pgh.ErrResourceNotFound) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/new-dashboard.json", "main", - "Create new dashboard", []byte("new content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "write with default branch", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "", - data: []byte("content"), - message: "Update dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("existing content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().UpdateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Update dashboard", "abc123", []byte("content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "error checking if file exists", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - data: []byte("content"), - message: "Update dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(nil, nil, errors.New("connection error")) - }, - expectedError: errors.New("check if file exists before writing: get contents: connection error"), - }, - { - name: "error during update", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - data: []byte("updated content"), - message: "Update dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("existing content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().UpdateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Update dashboard", "abc123", []byte("updated content")).Return(errors.New("update failed")) - }, - expectedError: errors.New("update file: update failed"), - }, - { - name: "error during create", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "new-dashboard.json", - ref: "main", - data: []byte("new content"), - message: "Create new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/new-dashboard.json", "main"). - Return(nil, nil, pgh.ErrResourceNotFound) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/new-dashboard.json", "main", - "Create new dashboard", []byte("new content")).Return(errors.New("create failed")) - }, - expectedError: errors.New("create failed"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Write method - err := repo.Write(context.Background(), tt.path, tt.ref, tt.data, tt.message) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_Delete(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - comment string - mockSetup func(t *testing.T, mockClient *pgh.MockClient) - expectedError error - }{ - { - name: "delete existing file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().IsDirectory().Return(false) - fileContent.EXPECT().GetSHA().Return("abc123") - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Delete dashboard", "abc123").Return(nil) - }, - expectedError: nil, - }, - { - name: "delete with default branch", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().IsDirectory().Return(false) - fileContent.EXPECT().GetSHA().Return("abc123") - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Delete dashboard", "abc123").Return(nil) - }, - expectedError: nil, - }, - { - name: "delete directory recursively", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboards", - ref: "main", - comment: "Delete dashboards directory", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - dirContent := pgh.NewMockRepositoryContent(t) - dirContent.EXPECT().IsDirectory().Return(true) - - // Directory contents - file1Content := pgh.NewMockRepositoryContent(t) - file1Content.EXPECT().GetPath().Return("grafana/dashboards/dashboard1.json") - file1Content.EXPECT().IsDirectory().Return(false) - file1Content.EXPECT().GetSHA().Return("file1-sha") - - file2Content := pgh.NewMockRepositoryContent(t) - file2Content.EXPECT().GetPath().Return("grafana/dashboards/dashboard2.json") - file2Content.EXPECT().IsDirectory().Return(false) - file2Content.EXPECT().GetSHA().Return("file2-sha") - - subDirContent := pgh.NewMockRepositoryContent(t) - subDirContent.EXPECT().GetPath().Return("grafana/dashboards/subfolder") - subDirContent.EXPECT().IsDirectory().Return(true) - - // Subfolder contents - subFile1Content := pgh.NewMockRepositoryContent(t) - subFile1Content.EXPECT().GetPath().Return("grafana/dashboards/subfolder/subdashboard.json") - subFile1Content.EXPECT().IsDirectory().Return(false) - subFile1Content.EXPECT().GetSHA().Return("subfile-sha") - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - - // Get main directory - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboards", "main"). - Return(dirContent, []pgh.RepositoryContent{file1Content, file2Content, subDirContent}, nil) - - // Get subfolder contents - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder", "main"). - Return(subDirContent, []pgh.RepositoryContent{subFile1Content}, nil) - - // Delete files in reverse order (depth-first) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder/subdashboard.json", "main", - "Delete dashboards directory", "subfile-sha").Return(nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/dashboard2.json", "main", - "Delete dashboards directory", "file2-sha").Return(nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/dashboard1.json", "main", - "Delete dashboards directory", "file1-sha").Return(nil) - }, - expectedError: nil, - }, - { - name: "delete directory recursively fails in the middle", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboards", - ref: "main", - comment: "Delete dashboards directory", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - dirContent := pgh.NewMockRepositoryContent(t) - dirContent.EXPECT().IsDirectory().Return(true) - - // Directory contents - file1Content := pgh.NewMockRepositoryContent(t) - file1Content.EXPECT().GetPath().Return("grafana/dashboards/dashboard1.json") - file1Content.EXPECT().IsDirectory().Return(false) - file1Content.EXPECT().GetSHA().Return("file1-sha") - - file2Content := pgh.NewMockRepositoryContent(t) - file2Content.EXPECT().GetPath().Return("grafana/dashboards/dashboard2.json") - file2Content.EXPECT().IsDirectory().Return(false) - file2Content.EXPECT().GetSHA().Return("file2-sha") - - subDirContent := pgh.NewMockRepositoryContent(t) - subDirContent.EXPECT().IsDirectory().Return(true) - subDirContent.EXPECT().GetPath().Return("grafana/dashboards/subfolder") - - // Subfolder contents - subFile1Content := pgh.NewMockRepositoryContent(t) - subFile1Content.EXPECT().GetPath().Return("grafana/dashboards/subfolder/subdashboard.json") - subFile1Content.EXPECT().IsDirectory().Return(false) - subFile1Content.EXPECT().GetSHA().Return("subfile-sha") - - subFile2Content := pgh.NewMockRepositoryContent(t) - subFile2Content.EXPECT().GetPath().Return("grafana/dashboards/subfolder/subdashboard2.json") - subFile2Content.EXPECT().IsDirectory().Return(false) - subFile2Content.EXPECT().GetSHA().Return("subfile2-sha") - - subFile3Content := pgh.NewMockRepositoryContent(t) - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - - // Get main directory - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboards", "main"). - Return(dirContent, []pgh.RepositoryContent{file1Content, file2Content, subDirContent}, nil) - - // Get subfolder contents - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder", "main"). - Return(subDirContent, []pgh.RepositoryContent{subFile1Content, subFile2Content, subFile3Content}, nil) - - // Delete first file successfully - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/dashboard1.json", "main", - "Delete dashboards directory", "file1-sha").Return(nil) - - // Second file deletion fails - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/dashboard2.json", "main", - "Delete dashboards directory", "file2-sha").Return(nil) - - // Delete subfolder files - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder/subdashboard.json", "main", - "Delete dashboards directory", "subfile-sha").Return(nil) - - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder/subdashboard2.json", "main", - "Delete dashboards directory", "subfile2-sha").Return(errors.New("permission denied")) - }, - expectedError: errors.New("delete directory recursively: delete file: permission denied"), - }, - { - name: "file not found", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "nonexistent.json", - ref: "main", - comment: "Delete nonexistent file", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/nonexistent.json", "main"). - Return(nil, nil, pgh.ErrResourceNotFound) - }, - expectedError: ErrFileNotFound, - }, - { - name: "branch does not exist and creation fails", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature").Return(false, nil) - mockClient.EXPECT().CreateBranch(mock.Anything, "grafana", "grafana", "main", "feature"). - Return(errors.New("failed to create branch")) - }, - expectedError: errors.New("create branch: failed to create branch"), - }, - { - name: "error checking if branch exists", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main"). - Return(false, errors.New("API error")) - }, - expectedError: errors.New("check branch exists: API error"), - }, - { - name: "error getting file content", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(nil, nil, errors.New("API rate limit exceeded")) - }, - expectedError: fmt.Errorf("find file to delete: %w", errors.New("API rate limit exceeded")), - }, - { - name: "error deleting file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().IsDirectory().Return(false) - fileContent.EXPECT().GetSHA().Return("abc123") - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Delete dashboard", "abc123").Return(errors.New("delete failed")) - }, - expectedError: errors.New("delete failed"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Delete method - err := repo.Delete(context.Background(), tt.path, tt.ref, tt.comment) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_History(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - mockSetup func(t *testing.T, mockClient *pgh.MockClient) - expected []provisioning.HistoryItem - expectedError error - }{ - { - name: "successful history retrieval", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - commits := []pgh.Commit{ - { - Ref: "abc123", - Message: "Update dashboard", - Author: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - Committer: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), - }, - { - Ref: "def456", - Message: "Initial commit", - Author: &pgh.CommitAuthor{ - Name: "Jane Smith", - Username: "janesmith", - AvatarURL: "https://example.com/avatar2.png", - }, - Committer: &pgh.CommitAuthor{ - Name: "Bob Johnson", - Username: "bjohnson", - AvatarURL: "https://example.com/avatar3.png", - }, - CreatedAt: time.Date(2022, 12, 31, 10, 0, 0, 0, time.UTC), - }, - } - - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "dashboard.json", "main"). - Return(commits, nil) - }, - expected: []provisioning.HistoryItem{ - { - Ref: "abc123", - Message: "Update dashboard", - Authors: []provisioning.Author{ - { - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), - }, - { - Ref: "def456", - Message: "Initial commit", - Authors: []provisioning.Author{ - { - Name: "Jane Smith", - Username: "janesmith", - AvatarURL: "https://example.com/avatar2.png", - }, - { - Name: "Bob Johnson", - Username: "bjohnson", - AvatarURL: "https://example.com/avatar3.png", - }, - }, - CreatedAt: time.Date(2022, 12, 31, 10, 0, 0, 0, time.UTC).UnixMilli(), - }, - }, - }, - { - name: "committer same as author", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - commits := []pgh.Commit{ - { - Ref: "abc123", - Message: "Update dashboard", - Author: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - Committer: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), - }, - } - - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(commits, nil) - }, - expected: []provisioning.HistoryItem{ - { - Ref: "abc123", - Message: "Update dashboard", - Authors: []provisioning.Author{ - { - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), - }, - }, - }, - { - name: "file not found", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "nonexistent.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "grafana/nonexistent.json", "main"). - Return(nil, pgh.ErrResourceNotFound) - }, - expectedError: ErrFileNotFound, - }, - { - name: "prefixed path", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "custom/prefix", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - commits := []pgh.Commit{ - { - Ref: "abc123", - Message: "Update dashboard", - Author: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), - }, - } - - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "custom/prefix/dashboard.json", "main"). - Return(commits, nil) - }, - expected: []provisioning.HistoryItem{ - { - Ref: "abc123", - Message: "Update dashboard", - Authors: []provisioning.Author{ - { - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), - }, - }, - }, - { - name: "other error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(nil, errors.New("api error")) - }, - expectedError: errors.New("get commits: api error"), - }, - { - name: "use default branch when ref is empty", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - commits := []pgh.Commit{ - { - Ref: "abc123", - Message: "Update dashboard", - Author: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), - }, - } - - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(commits, nil) - }, - expected: []provisioning.HistoryItem{ - { - Ref: "abc123", - Message: "Update dashboard", - Authors: []provisioning.Author{ - { - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the History method - history, err := repo.History(context.Background(), tt.path, tt.ref) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.expectedError, &statusErr) { - var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - } else { - require.Equal(t, tt.expectedError.Error(), err.Error()) - } - } else { - require.NoError(t, err) - require.Equal(t, tt.expected, history) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_LatestRef(t *testing.T) { - tests := []struct { - name string - setupMock func(mock *pgh.MockClient) - expectedRef string - expectedError error - }{ - { - name: "successful retrieval of latest ref", - setupMock: func(m *pgh.MockClient) { - m.On("GetBranch", mock.Anything, "grafana", "grafana", "main"). - Return(pgh.Branch{Sha: "abc123"}, nil) - }, - expectedRef: "abc123", - expectedError: nil, - }, - { - name: "error getting branch", - setupMock: func(m *pgh.MockClient) { - m.On("GetBranch", mock.Anything, "grafana", "grafana", "main"). - Return(pgh.Branch{}, fmt.Errorf("branch not found")) - }, - expectedRef: "", - expectedError: fmt.Errorf("get branch: branch not found"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) - tt.setupMock(mockGH) - - // Create repository with mock - repo := &githubRepository{ - gh: mockGH, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - owner: "grafana", - repo: "grafana", - } - - // Call the LatestRef method - ref, err := repo.LatestRef(context.Background()) - - // Check results - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, tt.expectedRef, ref) - } - - // Verify all mock expectations were met - mockGH.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_CompareFiles(t *testing.T) { - tests := []struct { - name string - setupMock func(m *pgh.MockClient) - base string - ref string - expectedFiles []VersionedFileChange - expectedError error - shouldGetLatest bool - }{ - { - name: "successfully compare files", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/test.json") - commitFile1.On("GetStatus").Return("added") - - commitFile2 := pgh.NewMockCommitFile(t) - commitFile2.On("GetFilename").Return("dashboards/modified.json") - commitFile2.On("GetStatus").Return("modified") - - commitFile3 := pgh.NewMockCommitFile(t) - commitFile3.On("GetFilename").Return("dashboards/renamed.json") - commitFile3.On("GetStatus").Return("renamed") - commitFile3.On("GetPreviousFilename").Return("dashboards/old.json") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - commitFile2, - commitFile3, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "test.json", - Ref: "def456", - Action: FileActionCreated, - }, - { - Path: "modified.json", - Ref: "def456", - Action: FileActionUpdated, - }, - { - Path: "renamed.json", - Ref: "def456", - Action: FileActionRenamed, - PreviousPath: "old.json", - }, - }, - expectedError: nil, - }, - { - name: "error comparing commits", - setupMock: func(m *pgh.MockClient) { - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return(nil, fmt.Errorf("failed to compare commits")) - }, - base: "abc123", - ref: "def456", - expectedFiles: nil, - expectedError: fmt.Errorf("compare commits: failed to compare commits"), - }, - { - name: "file outside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/path.json") - commitFile1.On("GetStatus").Return("added") - - commitFile2 := pgh.NewMockCommitFile(t) - commitFile2.On("GetFilename").Return("dashboards/valid.json") - commitFile2.On("GetStatus").Return("added") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - commitFile2, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "valid.json", - Ref: "def456", - Action: FileActionCreated, - }, - }, - expectedError: nil, - }, - { - name: "modified file outside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/modified.json") - commitFile1.On("GetStatus").Return("modified") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "copied file status", - setupMock: func(m *pgh.MockClient) { - // File inside configured path - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/copied.json") - commitFile1.On("GetStatus").Return("copied") - - // File outside configured path - commitFile2 := pgh.NewMockCommitFile(t) - commitFile2.On("GetFilename").Return("../outside/copied.json") - commitFile2.On("GetStatus").Return("copied") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - commitFile2, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "copied.json", - Ref: "def456", - Action: FileActionCreated, - }, - }, - expectedError: nil, - }, - { - name: "removed file status - inside path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/removed.json") - commitFile1.On("GetStatus").Return("removed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "removed.json", - PreviousPath: "removed.json", - Ref: "def456", - PreviousRef: "abc123", - Action: FileActionDeleted, - }, - }, - expectedError: nil, - }, - { - name: "renamed file status - both paths outside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/renamed.json") - commitFile1.On("GetPreviousFilename").Return("../outside/original.json") - commitFile1.On("GetStatus").Return("renamed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "renamed file status - both paths inside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/renamed.json") - commitFile1.On("GetPreviousFilename").Return("dashboards/original.json") - commitFile1.On("GetStatus").Return("renamed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "renamed.json", - PreviousPath: "original.json", - Ref: "def456", - PreviousRef: "abc123", - Action: FileActionRenamed, - }, - }, - expectedError: nil, - }, - { - name: "renamed file status - moving out of configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/renamed.json") - commitFile1.On("GetPreviousFilename").Return("dashboards/original.json") - commitFile1.On("GetStatus").Return("renamed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "original.json", - Ref: "abc123", - Action: FileActionDeleted, - }, - }, - expectedError: nil, - }, - { - name: "renamed file status - moving into configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/renamed.json") - commitFile1.On("GetPreviousFilename").Return("../outside/original.json") - commitFile1.On("GetStatus").Return("renamed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "renamed.json", - Ref: "def456", - Action: FileActionCreated, - }, - }, - expectedError: nil, - }, - { - name: "removed file status - outside path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/removed.json") - commitFile1.On("GetStatus").Return("removed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "changed file outside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/changed.json") - commitFile1.On("GetStatus").Return("changed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "get latest ref when ref is empty", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/test.json") - commitFile1.On("GetStatus").Return("added") - - m.On("GetBranch", mock.Anything, "grafana", "grafana", "main"). - Return(pgh.Branch{Sha: "latest123"}, nil) - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "latest123"). - Return([]pgh.CommitFile{commitFile1}, nil) - }, - base: "abc123", - ref: "", - shouldGetLatest: true, - expectedFiles: []VersionedFileChange{ - { - Path: "test.json", - Ref: "latest123", - Action: FileActionCreated, - }, - }, - expectedError: nil, - }, - { - name: "unchanged file status", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetStatus").Return("unchanged") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "unknown file status", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/unknown.json") - commitFile1.On("GetStatus").Return("unknown_status") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "error getting latest ref", - setupMock: func(m *pgh.MockClient) { - m.On("GetBranch", mock.Anything, "grafana", "grafana", "main"). - Return(pgh.Branch{}, fmt.Errorf("branch not found")) - }, - base: "abc123", - ref: "", - shouldGetLatest: true, - expectedFiles: nil, - expectedError: fmt.Errorf("get latest ref: get branch: branch not found"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) - tt.setupMock(mockGH) - - // Create repository with mock - repo := &githubRepository{ - gh: mockGH, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "dashboards", - }, - }, - }, - owner: "grafana", - repo: "grafana", - } - - // Call the CompareFiles method - files, err := repo.CompareFiles(context.Background(), tt.base, tt.ref) - - // Check results - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, len(tt.expectedFiles), len(files)) - - for i, expectedFile := range tt.expectedFiles { - require.Equal(t, expectedFile.Path, files[i].Path) - require.Equal(t, expectedFile.Ref, files[i].Ref) - require.Equal(t, expectedFile.Action, files[i].Action) - require.Equal(t, expectedFile.PreviousPath, files[i].PreviousPath) - } - } - - // Verify all mock expectations were met - mockGH.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_ResourceURLs(t *testing.T) { - tests := []struct { - name string - file *FileInfo - config *provisioning.Repository - expectedURLs *provisioning.ResourceURLs - expectedError error - }{ - { - name: "file with ref", - file: &FileInfo{ - Path: "dashboards/test.json", - Ref: "feature-branch", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - }, - }, - }, - expectedURLs: &provisioning.ResourceURLs{ - RepositoryURL: "https://github.com/grafana/grafana", - SourceURL: "https://github.com/grafana/grafana/blob/feature-branch/dashboards/test.json", - CompareURL: "https://github.com/grafana/grafana/compare/main...feature-branch", - NewPullRequestURL: "https://github.com/grafana/grafana/compare/main...feature-branch?quick_pull=1&labels=grafana", - }, - expectedError: nil, - }, - { - name: "file without ref uses default branch", - file: &FileInfo{ - Path: "dashboards/test.json", - Ref: "", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - }, - }, - }, - expectedURLs: &provisioning.ResourceURLs{ - RepositoryURL: "https://github.com/grafana/grafana", - SourceURL: "https://github.com/grafana/grafana/blob/main/dashboards/test.json", - }, - expectedError: nil, - }, - { - name: "file with ref same as branch", - file: &FileInfo{ - Path: "dashboards/test.json", - Ref: "main", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - }, - }, - }, - expectedURLs: &provisioning.ResourceURLs{ - RepositoryURL: "https://github.com/grafana/grafana", - SourceURL: "https://github.com/grafana/grafana/blob/main/dashboards/test.json", - }, - expectedError: nil, - }, - { - name: "empty path returns nil", - file: &FileInfo{ - Path: "", - Ref: "feature-branch", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - }, - }, - }, - expectedURLs: nil, - expectedError: nil, - }, - { - name: "nil github config returns nil", - file: &FileInfo{ - Path: "dashboards/test.json", - Ref: "feature-branch", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: nil, - }, - }, - expectedURLs: nil, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create repository - repo := &githubRepository{ - config: tt.config, - owner: "grafana", - repo: "grafana", - } - - // Call the ResourceURLs method - urls, err := repo.ResourceURLs(context.Background(), tt.file) - - // Check results - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, tt.expectedURLs, urls) - } - }) - } -} - -func TestGitHubRepository_Clone(t *testing.T) { - tests := []struct { - name string - setupMock func(m *MockCloneFn) - config *provisioning.Repository - expectedError error - }{ - { - name: "successfully clone repository", - setupMock: func(m *MockCloneFn) { - m.On("Execute", mock.Anything, CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, - MaxSize: 1024 * 1024 * 10, // 10MB - Timeout: 10 * time.Second, - Progress: io.Discard, - BeforeFn: nil, - }).Return(nil, nil) - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - expectedError: nil, - }, - { - name: "error cloning repository", - setupMock: func(m *MockCloneFn) { - m.On("Execute", mock.Anything, CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, - MaxSize: 1024 * 1024 * 10, // 10MB - Timeout: 10 * time.Second, - Progress: io.Discard, - BeforeFn: nil, - }).Return(nil, fmt.Errorf("failed to clone repository")) - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - expectedError: fmt.Errorf("failed to clone repository"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockCloneFn := NewMockCloneFn(t) - - tt.setupMock(mockCloneFn) - - // Create repository with mock - repo := &githubRepository{ - cloneFn: mockCloneFn.Execute, - config: tt.config, - owner: "grafana", - repo: "grafana", - } - - // Call the Clone method with a placeholder directory path - _, err := repo.Clone(context.Background(), CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, - MaxSize: 1024 * 1024 * 10, // 10MB - Timeout: 10 * time.Second, - Progress: io.Discard, - BeforeFn: nil, - }) - - // Check results - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockCloneFn.AssertExpectations(t) - }) - } -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/progress.go b/pkg/registry/apis/provisioning/repository/go-git/progress.go deleted file mode 100644 index 29e840499ad..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/progress.go +++ /dev/null @@ -1,47 +0,0 @@ -package gogit - -import ( - "bufio" - "bytes" - "io" -) - -func Progress(lines func(line string), final string) io.WriteCloser { - reader, writer := io.Pipe() - scanner := bufio.NewScanner(reader) - scanner.Split(scanLines) - go func() { - for scanner.Scan() { - line := scanner.Text() - if line != "" { - lines(line) - } - } - lines(final) - }() - return writer -} - -// Copied from bufio.ScanLines and modifed to accept standalone \r as input -func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - if i := bytes.IndexByte(data, '\r'); i >= 0 { - // We have a full newline-terminated line. - return i + 1, data[0:i], nil - } - - // Support standalone newlines also - if i := bytes.IndexByte(data, '\n'); i >= 0 { - // We have a full newline-terminated line. - return i + 1, data[0:i], nil - } - - // If we're at EOF, we have a final, non-terminated line. Return it. - if atEOF { - return len(data), data, nil - } - // Request more data. - return 0, nil, nil -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/progress_test.go b/pkg/registry/apis/provisioning/repository/go-git/progress_test.go deleted file mode 100644 index d52168c8cd5..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/progress_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package gogit - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestProgressParsing(t *testing.T) { - tests := []struct { - name string - input string - expect []string - }{ - { - name: "no breaks", - input: "some text", - expect: []string{"some text"}, - }, - { - name: "with cr", - input: "hello\rworld", - expect: []string{"hello", "world"}, - }, - { - name: "with nl", - input: "hello\nworld", - expect: []string{"hello", "world"}, - }, - { - name: "with cr+nl", - input: "hello\r\nworld", - expect: []string{"hello", "world"}, - }, - } - for _, tt := range tests { - lastLine := "***LAST*LINE***" - t.Run(tt.name, func(t *testing.T) { - lines := []string{} - writer := Progress(func(line string) { - lines = append(lines, line) - }, lastLine) - _, _ = writer.Write([]byte(tt.input)) - err := writer.Close() - require.NoError(t, err) - - assert.EventuallyWithT(t, func(c *assert.CollectT) { - assert.NotEmpty(c, lines) - assert.Equal(c, lastLine, lines[len(lines)-1]) - - // Compare the results - require.Equal(c, tt.expect, lines[0:len(lines)-1]) - }, time.Millisecond*100, time.Microsecond*50) - }) - } -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/repository_mock.go b/pkg/registry/apis/provisioning/repository/go-git/repository_mock.go deleted file mode 100644 index da1f2a005cf..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/repository_mock.go +++ /dev/null @@ -1,84 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package gogit - -import ( - context "context" - - git "github.com/go-git/go-git/v5" - mock "github.com/stretchr/testify/mock" -) - -// MockRepository is an autogenerated mock type for the Repository type -type MockRepository struct { - mock.Mock -} - -type MockRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *MockRepository) EXPECT() *MockRepository_Expecter { - return &MockRepository_Expecter{mock: &_m.Mock} -} - -// PushContext provides a mock function with given fields: ctx, o -func (_m *MockRepository) PushContext(ctx context.Context, o *git.PushOptions) error { - ret := _m.Called(ctx, o) - - if len(ret) == 0 { - panic("no return value specified for PushContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *git.PushOptions) error); ok { - r0 = rf(ctx, o) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockRepository_PushContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PushContext' -type MockRepository_PushContext_Call struct { - *mock.Call -} - -// PushContext is a helper method to define mock.On call -// - ctx context.Context -// - o *git.PushOptions -func (_e *MockRepository_Expecter) PushContext(ctx interface{}, o interface{}) *MockRepository_PushContext_Call { - return &MockRepository_PushContext_Call{Call: _e.mock.On("PushContext", ctx, o)} -} - -func (_c *MockRepository_PushContext_Call) Run(run func(ctx context.Context, o *git.PushOptions)) *MockRepository_PushContext_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*git.PushOptions)) - }) - return _c -} - -func (_c *MockRepository_PushContext_Call) Return(_a0 error) *MockRepository_PushContext_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepository_PushContext_Call) RunAndReturn(run func(context.Context, *git.PushOptions) error) *MockRepository_PushContext_Call { - _c.Call.Return(run) - return _c -} - -// NewMockRepository creates a new instance of MockRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *MockRepository { - mock := &MockRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/transport.go b/pkg/registry/apis/provisioning/repository/go-git/transport.go deleted file mode 100644 index 03d9feeb798..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/transport.go +++ /dev/null @@ -1,73 +0,0 @@ -package gogit - -import ( - "fmt" - "io" - "net/http" - "sync/atomic" - - "github.com/grafana/grafana/pkg/util/httpclient" -) - -var errBytesLimitExceeded = fmt.Errorf("bytes limit exceeded") - -// ByteLimitedTransport wraps http.RoundTripper to enforce a max byte limit -type ByteLimitedTransport struct { - Transport http.RoundTripper - Limit int64 - Bytes int64 -} - -// NewByteLimitedTransport creates a new ByteLimitedTransport with the specified transport and byte limit. -// If transport is nil, a new http.Transport modeled after http.DefaultTransport will be used. -func NewByteLimitedTransport(transport http.RoundTripper, limit int64) *ByteLimitedTransport { - if transport == nil { - transport = httpclient.NewHTTPTransport() - } - return &ByteLimitedTransport{ - Transport: transport, - Limit: limit, - Bytes: 0, - } -} - -// RoundTrip tracks downloaded bytes and aborts if limit is exceeded -func (b *ByteLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { - resp, err := b.Transport.RoundTrip(req) - if err != nil { - return nil, err - } - - // Wrap response body to track bytes read - resp.Body = &byteLimitedReader{ - reader: resp.Body, - limit: b.Limit, - bytes: &b.Bytes, - } - - return resp, nil -} - -// byteLimitedReader tracks and enforces a download limit -type byteLimitedReader struct { - reader io.ReadCloser - limit int64 - bytes *int64 -} - -func (r *byteLimitedReader) Read(p []byte) (int, error) { - n, err := r.reader.Read(p) - if err != nil { - return n, err - } - - if atomic.AddInt64(r.bytes, int64(n)) > r.limit { - return 0, errBytesLimitExceeded - } - - return n, nil -} - -func (r *byteLimitedReader) Close() error { - return r.reader.Close() -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/transport_test.go b/pkg/registry/apis/provisioning/repository/go-git/transport_test.go deleted file mode 100644 index 292046cf42f..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/transport_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package gogit - -import ( - "bytes" - "errors" - "io" - "net/http" - "sync/atomic" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type mockTransport struct { - response *http.Response - err error -} - -func (m *mockTransport) RoundTrip(*http.Request) (*http.Response, error) { - return m.response, m.err -} - -func TestNewByteLimitedTransport(t *testing.T) { - tests := []struct { - name string - transport http.RoundTripper - limit int64 - }{ - { - name: "with custom transport", - transport: &mockTransport{}, - limit: 1000, - }, - { - name: "with nil transport", - transport: nil, - limit: 1000, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - blt := NewByteLimitedTransport(tt.transport, tt.limit) - assert.NotNil(t, blt) - assert.Equal(t, tt.limit, blt.Limit) - assert.Equal(t, int64(0), blt.Bytes) - - if tt.transport == nil { - assert.NotNil(t, blt.Transport) - assert.NotEqual(t, http.DefaultTransport, blt.Transport) - } else { - assert.Equal(t, tt.transport, blt.Transport) - } - }) - } -} - -func TestByteLimitedTransport_RoundTrip(t *testing.T) { - tests := []struct { - name string - responseBody string - limit int64 - expectedError error - }{ - { - name: "under limit", - responseBody: "small response", - limit: 100, - expectedError: nil, - }, - { - name: "exceeds limit", - responseBody: "this response will exceed the byte limit", - limit: 10, - expectedError: errBytesLimitExceeded, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockResp := &http.Response{ - Body: io.NopCloser(bytes.NewBufferString(tt.responseBody)), - } - mockTransport := &mockTransport{response: mockResp} - - blt := NewByteLimitedTransport(mockTransport, tt.limit) - resp, err := blt.RoundTrip(&http.Request{}) - require.NoError(t, err) - defer func() { - closeErr := resp.Body.Close() - assert.NoError(t, closeErr, "failed to close response body") - }() - - data, err := io.ReadAll(resp.Body) - if tt.expectedError != nil { - assert.True(t, errors.Is(err, tt.expectedError), "expected error %v, got %v", tt.expectedError, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.responseBody, string(data)) - } - }) - } -} - -func TestByteLimitedReader_Close(t *testing.T) { - mockBody := io.NopCloser(bytes.NewBufferString("test")) - var byteCount int64 - reader := &byteLimitedReader{ - reader: mockBody, - limit: 100, - bytes: &byteCount, - } - - err := reader.Close() - assert.NoError(t, err) -} - -func TestByteLimitedReader_AtomicCounting(t *testing.T) { - var byteCount int64 - reader := &byteLimitedReader{ - reader: io.NopCloser(bytes.NewBufferString("test data")), - limit: 5, - bytes: &byteCount, - } - - // First read should succeed - buf := make([]byte, 4) - n, err := reader.Read(buf) - assert.NoError(t, err) - assert.Equal(t, 4, n) - - // Second read should fail due to limit - n, err = reader.Read(buf) - assert.True(t, errors.Is(err, errBytesLimitExceeded), "expected error %v, got %v", errBytesLimitExceeded, err) - assert.Equal(t, 0, n) - - // Verify atomic counter - assert.Greater(t, atomic.LoadInt64(&byteCount), int64(5)) -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/worktree_mock.go b/pkg/registry/apis/provisioning/repository/go-git/worktree_mock.go deleted file mode 100644 index 0a85a14e12b..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/worktree_mock.go +++ /dev/null @@ -1,261 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package gogit - -import ( - billy "github.com/go-git/go-billy/v5" - git "github.com/go-git/go-git/v5" - - mock "github.com/stretchr/testify/mock" - - plumbing "github.com/go-git/go-git/v5/plumbing" -) - -// MockWorktree is an autogenerated mock type for the Worktree type -type MockWorktree struct { - mock.Mock -} - -type MockWorktree_Expecter struct { - mock *mock.Mock -} - -func (_m *MockWorktree) EXPECT() *MockWorktree_Expecter { - return &MockWorktree_Expecter{mock: &_m.Mock} -} - -// Add provides a mock function with given fields: path -func (_m *MockWorktree) Add(path string) (plumbing.Hash, error) { - ret := _m.Called(path) - - if len(ret) == 0 { - panic("no return value specified for Add") - } - - var r0 plumbing.Hash - var r1 error - if rf, ok := ret.Get(0).(func(string) (plumbing.Hash, error)); ok { - return rf(path) - } - if rf, ok := ret.Get(0).(func(string) plumbing.Hash); ok { - r0 = rf(path) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(plumbing.Hash) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(path) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockWorktree_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' -type MockWorktree_Add_Call struct { - *mock.Call -} - -// Add is a helper method to define mock.On call -// - path string -func (_e *MockWorktree_Expecter) Add(path interface{}) *MockWorktree_Add_Call { - return &MockWorktree_Add_Call{Call: _e.mock.On("Add", path)} -} - -func (_c *MockWorktree_Add_Call) Run(run func(path string)) *MockWorktree_Add_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) - }) - return _c -} - -func (_c *MockWorktree_Add_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Add_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockWorktree_Add_Call) RunAndReturn(run func(string) (plumbing.Hash, error)) *MockWorktree_Add_Call { - _c.Call.Return(run) - return _c -} - -// Commit provides a mock function with given fields: message, opts -func (_m *MockWorktree) Commit(message string, opts *git.CommitOptions) (plumbing.Hash, error) { - ret := _m.Called(message, opts) - - if len(ret) == 0 { - panic("no return value specified for Commit") - } - - var r0 plumbing.Hash - var r1 error - if rf, ok := ret.Get(0).(func(string, *git.CommitOptions) (plumbing.Hash, error)); ok { - return rf(message, opts) - } - if rf, ok := ret.Get(0).(func(string, *git.CommitOptions) plumbing.Hash); ok { - r0 = rf(message, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(plumbing.Hash) - } - } - - if rf, ok := ret.Get(1).(func(string, *git.CommitOptions) error); ok { - r1 = rf(message, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockWorktree_Commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commit' -type MockWorktree_Commit_Call struct { - *mock.Call -} - -// Commit is a helper method to define mock.On call -// - message string -// - opts *git.CommitOptions -func (_e *MockWorktree_Expecter) Commit(message interface{}, opts interface{}) *MockWorktree_Commit_Call { - return &MockWorktree_Commit_Call{Call: _e.mock.On("Commit", message, opts)} -} - -func (_c *MockWorktree_Commit_Call) Run(run func(message string, opts *git.CommitOptions)) *MockWorktree_Commit_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(*git.CommitOptions)) - }) - return _c -} - -func (_c *MockWorktree_Commit_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Commit_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockWorktree_Commit_Call) RunAndReturn(run func(string, *git.CommitOptions) (plumbing.Hash, error)) *MockWorktree_Commit_Call { - _c.Call.Return(run) - return _c -} - -// Filesystem provides a mock function with no fields -func (_m *MockWorktree) Filesystem() billy.Filesystem { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Filesystem") - } - - var r0 billy.Filesystem - if rf, ok := ret.Get(0).(func() billy.Filesystem); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(billy.Filesystem) - } - } - - return r0 -} - -// MockWorktree_Filesystem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Filesystem' -type MockWorktree_Filesystem_Call struct { - *mock.Call -} - -// Filesystem is a helper method to define mock.On call -func (_e *MockWorktree_Expecter) Filesystem() *MockWorktree_Filesystem_Call { - return &MockWorktree_Filesystem_Call{Call: _e.mock.On("Filesystem")} -} - -func (_c *MockWorktree_Filesystem_Call) Run(run func()) *MockWorktree_Filesystem_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockWorktree_Filesystem_Call) Return(_a0 billy.Filesystem) *MockWorktree_Filesystem_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockWorktree_Filesystem_Call) RunAndReturn(run func() billy.Filesystem) *MockWorktree_Filesystem_Call { - _c.Call.Return(run) - return _c -} - -// Remove provides a mock function with given fields: path -func (_m *MockWorktree) Remove(path string) (plumbing.Hash, error) { - ret := _m.Called(path) - - if len(ret) == 0 { - panic("no return value specified for Remove") - } - - var r0 plumbing.Hash - var r1 error - if rf, ok := ret.Get(0).(func(string) (plumbing.Hash, error)); ok { - return rf(path) - } - if rf, ok := ret.Get(0).(func(string) plumbing.Hash); ok { - r0 = rf(path) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(plumbing.Hash) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(path) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockWorktree_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' -type MockWorktree_Remove_Call struct { - *mock.Call -} - -// Remove is a helper method to define mock.On call -// - path string -func (_e *MockWorktree_Expecter) Remove(path interface{}) *MockWorktree_Remove_Call { - return &MockWorktree_Remove_Call{Call: _e.mock.On("Remove", path)} -} - -func (_c *MockWorktree_Remove_Call) Run(run func(path string)) *MockWorktree_Remove_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) - }) - return _c -} - -func (_c *MockWorktree_Remove_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Remove_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockWorktree_Remove_Call) RunAndReturn(run func(string) (plumbing.Hash, error)) *MockWorktree_Remove_Call { - _c.Call.Return(run) - return _c -} - -// NewMockWorktree creates a new instance of MockWorktree. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockWorktree(t interface { - mock.TestingT - Cleanup(func()) -}) *MockWorktree { - mock := &MockWorktree{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/wrapper.go b/pkg/registry/apis/provisioning/repository/go-git/wrapper.go deleted file mode 100644 index e6954d27b81..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/wrapper.go +++ /dev/null @@ -1,468 +0,0 @@ -package gogit - -import ( - "context" - "errors" - "fmt" - "io" - "io/fs" - "net/http" - "os" - "strings" - "time" - - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/util" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport/client" - githttp "github.com/go-git/go-git/v5/plumbing/transport/http" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/validation/field" - - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" - "github.com/grafana/grafana/pkg/util/httpclient" -) - -const ( - // maxOperationBytes is the maximum size of a git operation in bytes (1 GB) - maxOperationBytes = int64(1 << 30) - maxOperationTimeout = 10 * time.Minute -) - -func init() { - // Create a size-limited writer that will cancel the context if size is exceeded - limitedTransport := NewByteLimitedTransport(httpclient.NewHTTPTransport(), maxOperationBytes) - httpClient := githttp.NewClient(&http.Client{ - Transport: limitedTransport, - }) - client.InstallProtocol("https", httpClient) - client.InstallProtocol("http", httpClient) -} - -//go:generate mockery --name=Worktree --output=mocks --inpackage --filename=worktree_mock.go --with-expecter -type Worktree interface { - Commit(message string, opts *git.CommitOptions) (plumbing.Hash, error) - Remove(path string) (plumbing.Hash, error) - Add(path string) (plumbing.Hash, error) - Filesystem() billy.Filesystem -} - -type worktree struct { - *git.Worktree -} - -//go:generate mockery --name=Repository --output=mocks --inpackage --filename=repository_mock.go --with-expecter -type Repository interface { - PushContext(ctx context.Context, o *git.PushOptions) error -} - -func (w *worktree) Filesystem() billy.Filesystem { - return w.Worktree.Filesystem -} - -var _ repository.Repository = (*GoGitRepo)(nil) - -type GoGitRepo struct { - config *provisioning.Repository - decryptedPassword string - opts repository.CloneOptions - - repo Repository - tree Worktree - dir string // file path to worktree root (necessary? should use billy) -} - -// This will create a new clone every time -// As structured, it is valid for one context and should not be shared across multiple requests -func Clone( - ctx context.Context, - root string, - config *provisioning.Repository, - opts repository.CloneOptions, - secrets secrets.Service, -) (repository.ClonedRepository, error) { - if root == "" { - return nil, fmt.Errorf("missing root config") - } - - if config.Namespace == "" { - return nil, fmt.Errorf("config is missing namespace") - } - - if config.Name == "" { - return nil, fmt.Errorf("config is missing name") - } - - if opts.BeforeFn != nil { - if err := opts.BeforeFn(); err != nil { - return nil, err - } - } - - // add a timeout to the operation - timeout := maxOperationTimeout - if opts.Timeout > 0 { - timeout = opts.Timeout - } - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken) - if err != nil { - return nil, fmt.Errorf("error decrypting token: %w", err) - } - - if err := os.MkdirAll(root, 0700); err != nil { - return nil, fmt.Errorf("create root dir: %w", err) - } - - dir, err := os.MkdirTemp(root, fmt.Sprintf("clone-%s-%s-", config.Namespace, config.Name)) - if err != nil { - return nil, fmt.Errorf("create temp clone dir: %w", err) - } - - progress := opts.Progress - if progress == nil { - progress = io.Discard - } - - repo, tree, err := clone(ctx, config, opts, decrypted, dir, progress) - if err != nil { - if err := os.RemoveAll(dir); err != nil { - return nil, fmt.Errorf("remove temp clone dir after clone failed: %w", err) - } - - return nil, fmt.Errorf("clone: %w", err) - } - - return &GoGitRepo{ - config: config, - tree: &worktree{Worktree: tree}, - opts: opts, - decryptedPassword: string(decrypted), - repo: repo, - dir: dir, - }, nil -} - -func clone(ctx context.Context, config *provisioning.Repository, opts repository.CloneOptions, decrypted []byte, dir string, progress io.Writer) (*git.Repository, *git.Worktree, error) { - gitcfg := config.Spec.GitHub - url := gitcfg.URL - if !strings.HasPrefix(url, "file://") { - url = fmt.Sprintf("%s.git", url) - } - - branch := plumbing.NewBranchReferenceName(gitcfg.Branch) - cloneOpts := &git.CloneOptions{ - ReferenceName: branch, - Auth: &githttp.BasicAuth{ - Username: "grafana", // this can be anything except an empty string for PAT - Password: string(decrypted), // TODO... will need to get from a service! - }, - URL: url, - Progress: progress, - } - - repo, err := git.PlainCloneContext(ctx, dir, false, cloneOpts) - if errors.Is(err, plumbing.ErrReferenceNotFound) && opts.CreateIfNotExists { - cloneOpts.ReferenceName = "" // empty - repo, err = git.PlainCloneContext(ctx, dir, false, cloneOpts) - if err == nil { - worktree, err := repo.Worktree() - if err != nil { - return nil, nil, err - } - err = worktree.Checkout(&git.CheckoutOptions{ - Branch: branch, - Force: true, - Create: true, - }) - if err != nil { - return nil, nil, fmt.Errorf("unable to create new branch: %w", err) - } - } - } else if err != nil { - return nil, nil, fmt.Errorf("clone error: %w", err) - } - - rcfg, err := repo.Config() - if err != nil { - return nil, nil, fmt.Errorf("error reading repository config %w", err) - } - - origin := rcfg.Remotes["origin"] - if origin == nil { - return nil, nil, fmt.Errorf("missing origin remote %w", err) - } - - if url != origin.URLs[0] { - return nil, nil, fmt.Errorf("unexpected remote (expected: %s, found: %s)", url, origin.URLs[0]) - } - - worktree, err := repo.Worktree() - if err != nil { - return nil, nil, fmt.Errorf("get worktree: %w", err) - } - - return repo, worktree, nil -} - -// After making changes to the worktree, push changes -func (g *GoGitRepo) Push(ctx context.Context, opts repository.PushOptions) error { - timeout := maxOperationTimeout - if opts.Timeout > 0 { - timeout = opts.Timeout - } - - progress := opts.Progress - if progress == nil { - progress = io.Discard - } - - if opts.BeforeFn != nil { - if err := opts.BeforeFn(); err != nil { - return err - } - } - - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - if !g.opts.PushOnWrites { - _, err := g.tree.Commit("exported from grafana", &git.CommitOptions{ - All: true, // Add everything that changed - }) - if err != nil { - // empty commit is fine -- no change - if !errors.Is(err, git.ErrEmptyCommit) { - return err - } - } - } - - err := g.repo.PushContext(ctx, &git.PushOptions{ - Progress: progress, - Force: true, // avoid fast-forward-errors - Auth: &githttp.BasicAuth{ // reuse logic from clone? - Username: "grafana", - Password: g.decryptedPassword, - }, - }) - if errors.Is(err, git.NoErrAlreadyUpToDate) { - return nil // same as the target - } - return err -} - -func (g *GoGitRepo) Remove(ctx context.Context) error { - return os.RemoveAll(g.dir) -} - -// Config implements repository.Repository. -func (g *GoGitRepo) Config() *provisioning.Repository { - return g.config -} - -// ReadTree implements repository.Repository. -func (g *GoGitRepo) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { - var treePath string - if g.config.Spec.GitHub.Path != "" { - treePath = g.config.Spec.GitHub.Path - } - treePath = safepath.Clean(treePath) - - entries := make([]repository.FileTreeEntry, 0, 100) - err := util.Walk(g.tree.Filesystem(), treePath, func(path string, info fs.FileInfo, err error) error { - // We already have an error, just pass it onwards. - if err != nil || - // This is the root of the repository (or should pretend to be) - safepath.Clean(path) == "" || path == treePath || - // This is the Git data - (treePath == "" && (strings.HasPrefix(path, ".git/") || path == ".git")) { - return err - } - if treePath != "" { - path = strings.TrimPrefix(path, treePath) - } - entry := repository.FileTreeEntry{ - Path: strings.TrimLeft(path, "/"), - Size: info.Size(), - } - if !info.IsDir() { - entry.Blob = true - // For a real instance, this will likely be based on: - // https://github.com/go-git/go-git/blob/main/_examples/ls/main.go#L25 - entry.Hash = fmt.Sprintf("TODO/%d", info.Size()) // but not used for - } - entries = append(entries, entry) - return err - }) - if errors.Is(err, fs.ErrNotExist) { - // We intentionally ignore this case, as it is expected - } else if err != nil { - return nil, fmt.Errorf("walk tree for ref '%s': %w", ref, err) - } - return entries, nil -} - -func (g *GoGitRepo) Test(ctx context.Context) (*provisioning.TestResults, error) { - return &provisioning.TestResults{ - Success: g.tree != nil, - }, nil -} - -// Update implements repository.Repository. -func (g *GoGitRepo) Update(ctx context.Context, path string, ref string, data []byte, message string) error { - return g.Write(ctx, path, ref, data, message) -} - -// Create implements repository.Repository. -func (g *GoGitRepo) Create(ctx context.Context, path string, ref string, data []byte, message string) error { - // FIXME: this means we would override files - return g.Write(ctx, path, ref, data, message) -} - -// Write implements repository.Repository. -func (g *GoGitRepo) Write(ctx context.Context, fpath string, ref string, data []byte, message string) error { - if err := verifyPathWithoutRef(fpath, ref); err != nil { - return err - } - fpath = safepath.Join(g.config.Spec.GitHub.Path, fpath) - - // FIXME: this means that won't export empty folders - // should we create them with a .keep file? - // For folders, just create the folder and ignore the commit - if safepath.IsDir(fpath) { - return g.tree.Filesystem().MkdirAll(fpath, 0750) - } - - dir := safepath.Dir(fpath) - if dir != "" { - err := g.tree.Filesystem().MkdirAll(dir, 0750) - if err != nil { - return err - } - } - - file, err := g.tree.Filesystem().Create(fpath) - if err != nil { - return err - } - _, err = file.Write(data) - if err != nil { - return err - } - - _, err = g.tree.Add(fpath) - if err != nil { - return err - } - return g.maybeCommit(ctx, message) -} - -func (g *GoGitRepo) maybeCommit(ctx context.Context, message string) error { - // Skip commit for each file - if !g.opts.PushOnWrites { - return nil - } - - opts := &git.CommitOptions{ - Author: &object.Signature{ - Name: "grafana", - }, - } - sig := repository.GetAuthorSignature(ctx) - if sig != nil && sig.Name != "" { - opts.Author.Name = sig.Name - opts.Author.Email = sig.Email - opts.Author.When = sig.When - } - if opts.Author.When.IsZero() { - opts.Author.When = time.Now() - } - - _, err := g.tree.Commit(message, opts) - if errors.Is(err, git.ErrEmptyCommit) { - return nil // empty commit is fine -- no change - } - return err -} - -// Delete implements repository.Repository. -func (g *GoGitRepo) Delete(ctx context.Context, fpath string, ref string, message string) error { - if err := verifyPathWithoutRef(fpath, ref); err != nil { - return err - } - - fpath = safepath.Join(g.config.Spec.GitHub.Path, fpath) - if _, err := g.tree.Remove(fpath); err != nil { - if errors.Is(err, fs.ErrNotExist) { - return repository.ErrFileNotFound - } - - return err - } - return g.maybeCommit(ctx, message) -} - -// Read implements repository.Repository. -func (g *GoGitRepo) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) { - if err := verifyPathWithoutRef(path, ref); err != nil { - return nil, err - } - readPath := safepath.Join(g.config.Spec.GitHub.Path, path) - stat, err := g.tree.Filesystem().Lstat(readPath) - if errors.Is(err, fs.ErrNotExist) { - return nil, repository.ErrFileNotFound - } else if err != nil { - return nil, fmt.Errorf("stat path '%s': %w", readPath, err) - } - info := &repository.FileInfo{ - Path: path, - Modified: &metav1.Time{ - Time: stat.ModTime(), - }, - } - if !stat.IsDir() { - f, err := g.tree.Filesystem().Open(readPath) - if err != nil { - return nil, fmt.Errorf("open file '%s': %w", readPath, err) - } - info.Data, err = io.ReadAll(f) - if err != nil { - return nil, fmt.Errorf("read file '%s': %w", readPath, err) - } - } - return info, err -} - -func verifyPathWithoutRef(path string, ref string) error { - if path == "" { - return fmt.Errorf("expected path") - } - if ref != "" { - return fmt.Errorf("ref unsupported") - } - return nil -} - -// History implements repository.Repository. -func (g *GoGitRepo) History(ctx context.Context, path string, ref string) ([]provisioning.HistoryItem, error) { - return nil, &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "history is not yet implemented", - Code: http.StatusNotImplemented, - }, - } -} - -// Validate implements repository.Repository. -func (g *GoGitRepo) Validate() field.ErrorList { - return nil -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go b/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go deleted file mode 100644 index 79a2951e8c8..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go +++ /dev/null @@ -1,1642 +0,0 @@ -package gogit - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "sort" - "testing" - "time" - - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5" - plumbing "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport/client" - githttp "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/plumbing/transport/server" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/go-git/go-git/v5/storage/memory" - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" -) - -type dummySecret struct{} - -func (d *dummySecret) Decrypt(ctx context.Context, encrypted []byte) ([]byte, error) { - token, ok := os.LookupEnv("gitwraptoken") - if !ok { - return nil, fmt.Errorf("missing token in environment") - } - return []byte(token), nil -} - -func (d *dummySecret) Encrypt(ctx context.Context, plain []byte) ([]byte, error) { - panic("not implemented") -} - -// FIXME!! NOTE!!!!! -// This is really just a sketchpad while trying to get things working -// the test makes destructive changes to a real git repository :) -// this should be removed before committing to main (likely sooner) -// and replaced with integration tests that check the more specific results -func TestGoGitWrapper(t *testing.T) { - _, ok := os.LookupEnv("gitwraptoken") - if !ok { - t.Skipf("no token found in environment") - } - - ctx := context.Background() - wrap, err := Clone(ctx, "testdata/clone", &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "ns", - Name: "unit-tester", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/git-ui-sync-demo", - Branch: "ryan-test", - }, - }, - }, - repository.CloneOptions{ - PushOnWrites: false, - CreateIfNotExists: true, - Progress: os.Stdout, - }, - &dummySecret{}, - ) - require.NoError(t, err) - - tree, err := wrap.ReadTree(ctx, "") - require.NoError(t, err) - - jj, err := json.MarshalIndent(tree, "", " ") - require.NoError(t, err) - - fmt.Printf("TREE:%s\n", string(jj)) - - ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{ - Name: "xxxxx", - Email: "rrr@yyyy.zzz", - When: time.Now(), - }) - - for i := 0; i < 10; i++ { - fname := fmt.Sprintf("deep/path/in/test_%d.txt", i) - fmt.Printf("Write:%s\n", fname) - err = wrap.Write(ctx, fname, "", []byte(fmt.Sprintf("body/%d %s", i, time.Now())), "the commit message") - require.NoError(t, err) - } - - fmt.Printf("push...\n") - err = wrap.Push(ctx, repository.PushOptions{ - Timeout: 10, - Progress: os.Stdout, - }) - require.NoError(t, err) -} - -func TestReadTree(t *testing.T) { - dir := t.TempDir() - gitRepo, err := git.PlainInit(dir, false) - require.NoError(t, err, "failed to init a new git repository") - tree, err := gitRepo.Worktree() - require.NoError(t, err, "failed to get worktree") - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: v0alpha1.RepositorySpec{ - Title: "test", - Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow}, - Type: v0alpha1.GitHubRepositoryType, - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/__unit-test", - Path: "grafana/", - Branch: "main", - }, - }, - Status: v0alpha1.RepositoryStatus{}, - }, - decryptedPassword: "password", - - repo: gitRepo, - tree: &worktree{ - Worktree: tree, - }, - dir: dir, - } - - err = os.WriteFile(filepath.Join(dir, "test.txt"), []byte("test"), 0644) - require.NoError(t, err, "failed to write test file") - - err = os.Mkdir(filepath.Join(dir, "grafana"), 0750) - require.NoError(t, err, "failed to mkdir grafana") - - err = os.WriteFile(filepath.Join(dir, "grafana", "test2.txt"), []byte("test"), 0644) - require.NoError(t, err, "failed to write grafana/test2 file") - - ctx := context.Background() - entries, err := repo.ReadTree(ctx, "HEAD") - require.NoError(t, err, "failed to read tree") - - // Here is the meat of why this test exists: the ReadTree call should only read the config.Spec.GitHub.Path files. - // All prefixes are removed (i.e. a file is just its name, not ${Path}/${Name}). - // And it does not include the directory in the listing, as it pretends to be the root. - require.Len(t, entries, 1, "entries from ReadTree") - require.Equal(t, entries[0].Path, "test2.txt", "entry path") -} - -func TestGoGitRepo_History(t *testing.T) { - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - } - - // Test History method - ctx := context.Background() - _, err := repo.History(ctx, "test.txt", "") - require.Error(t, err, "History should return an error as it's not implemented") - require.Contains(t, err.Error(), "history is not yet implemented") -} - -func TestGoGitRepo_Validate(t *testing.T) { - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - } - - // Test Validate method - errs := repo.Validate() - require.Empty(t, errs, "Validate should return no errors") -} - -func TestGoGitRepo_Read(t *testing.T) { - // Setup test cases - tests := []struct { - name string - path string - ref string - setupMock func(fs billy.Filesystem) - expectError bool - errorType error - checkResult func(t *testing.T, info *repository.FileInfo) - }{ - { - name: "successfully read file", - path: "test.txt", - ref: "", - setupMock: func(fs billy.Filesystem) { - // Create a test file - f, err := fs.Create("grafana/test.txt") - require.NoError(t, err, "failed to create test file") - _, err = f.Write([]byte("test content")) - require.NoError(t, err, "failed to write test content") - err = f.Close() - require.NoError(t, err, "failed to close test file") - }, - expectError: false, - checkResult: func(t *testing.T, info *repository.FileInfo) { - require.Equal(t, "test.txt", info.Path) - require.Equal(t, "test content", string(info.Data)) - require.NotNil(t, info.Modified) - }, - }, - { - name: "empty path", - path: "", - ref: "", - setupMock: func(fs billy.Filesystem) {}, - expectError: true, - errorType: fmt.Errorf("expected path"), - }, - { - name: "ref not supported", - path: "test.txt", - ref: "main", - setupMock: func(fs billy.Filesystem) {}, - expectError: true, - errorType: fmt.Errorf("ref unsupported"), - }, - { - name: "file not found", - path: "nonexistent.txt", - ref: "", - setupMock: func(fs billy.Filesystem) { - // Don't create the file - }, - expectError: true, - errorType: repository.ErrFileNotFound, - }, - { - name: "read directory", - path: "testdir", - ref: "", - setupMock: func(fs billy.Filesystem) { - // Create a test directory - err := fs.MkdirAll("grafana/testdir", 0755) - require.NoError(t, err, "failed to create test directory") - }, - expectError: false, - checkResult: func(t *testing.T, info *repository.FileInfo) { - require.Equal(t, "testdir", info.Path) - require.Nil(t, info.Data) - require.NotNil(t, info.Modified) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup filesystem and repo - fs := memfs.New() - tt.setupMock(fs) - - // Create a worktree with the filesystem - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: &worktree{ - Worktree: &git.Worktree{ - Filesystem: fs, - }, - }, - } - - // Test Read method - ctx := context.Background() - info, err := repo.Read(ctx, tt.path, tt.ref) - - // Check results - if tt.expectError { - require.Error(t, err) - if tt.errorType != nil { - if errors.Is(tt.errorType, repository.ErrFileNotFound) { - require.ErrorIs(t, err, repository.ErrFileNotFound) - } else { - require.Contains(t, err.Error(), tt.errorType.Error()) - } - } - } else { - require.NoError(t, err) - require.NotNil(t, info) - tt.checkResult(t, info) - } - }) - } -} - -func TestGoGitRepo_Delete(t *testing.T) { - tests := []struct { - name string - path string - ref string - pushOnWrite bool - setupMock func(mockTree *MockWorktree) - expectError bool - errorType error - }{ - { - name: "delete existing file", - path: "testfile.txt", - ref: "", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil) - }, - expectError: false, - }, - { - name: "delete non-existent file", - path: "nonexistent.txt", - ref: "", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/nonexistent.txt").Return(plumbing.Hash{}, fs.ErrNotExist) - }, - expectError: true, - errorType: repository.ErrFileNotFound, - }, - { - name: "delete with other error", - path: "testfile.txt", - ref: "", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, fmt.Errorf("some other error")) - }, - expectError: true, - errorType: fmt.Errorf("some other error"), - }, - { - name: "empty path", - path: "", - ref: "", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) {}, - expectError: true, - errorType: fmt.Errorf("expected path"), - }, - { - name: "with ref", - path: "testfile.txt", - ref: "main", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - }, - expectError: true, - errorType: fmt.Errorf("ref unsupported"), - }, - { - name: "delete with push on write enabled", - path: "testfile.txt", - ref: "", - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil) - mockTree.On("Commit", "test delete", mock.MatchedBy(func(opts *git.CommitOptions) bool { - return opts.Author != nil && - opts.Author.Name == "Test User" && - opts.Author.Email == "test@example.com" && - opts.Author.When.After(time.Now().Add(-time.Minute)) && - opts.Author.When.Before(time.Now().Add(time.Minute)) - })).Return(plumbing.Hash{}, nil) - }, - expectError: false, - }, - { - name: "delete with empty commit", - path: "testfile.txt", - ref: "", - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil) - mockTree.On("Commit", "test delete", mock.MatchedBy(func(opts *git.CommitOptions) bool { - return opts.Author != nil && - opts.Author.Name == "Test User" && - opts.Author.Email == "test@example.com" && - opts.Author.When.After(time.Now().Add(-time.Minute)) && - opts.Author.When.Before(time.Now().Add(time.Minute)) - })).Return(plumbing.Hash{}, git.ErrEmptyCommit) - }, - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup filesystem and repo - - mockTree := NewMockWorktree(t) - tt.setupMock(mockTree) - - // Create a worktree with the filesystem - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: mockTree, - opts: repository.CloneOptions{ - PushOnWrites: tt.pushOnWrite, - }, - } - - // Test Delete method - ctx := context.Background() - // Set author signature for the test - ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{ - Name: "Test User", - Email: "test@example.com", - When: time.Now(), - }) - - err := repo.Delete(ctx, tt.path, tt.ref, "test delete") - - // Check results - if tt.expectError { - require.Error(t, err) - if tt.errorType != nil { - if errors.Is(tt.errorType, repository.ErrFileNotFound) { - require.ErrorIs(t, err, repository.ErrFileNotFound) - } else { - require.Contains(t, err.Error(), tt.errorType.Error()) - } - } - } else { - require.NoError(t, err) - } - - mockTree.AssertExpectations(t) - }) - } -} - -// FIXME: missing coverage for Update / Create because we use Write for both -// when I think it shouldn't be the case as it's inconsistent with the other repository implementations -func TestGoGitRepo_Write(t *testing.T) { - tests := []struct { - name string - path string - ref string - data []byte - pushOnWrite bool - setupMock func(mockTree *MockWorktree) - expectError bool - errorType error - }{ - { - name: "successful write", - path: "test.txt", - ref: "", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash("abc123"), nil) - mockTree.On("Commit", "test write", mock.MatchedBy(func(opts *git.CommitOptions) bool { - return opts.Author != nil && - opts.Author.Name == "Test User" && - opts.Author.Email == "test@example.com" && - opts.Author.When.After(time.Now().Add(-time.Minute)) && - opts.Author.When.Before(time.Now().Add(time.Minute)) - })).Return(plumbing.NewHash("def456"), nil) - }, - expectError: false, - }, - { - name: "create folder only", - path: "testdir/", - ref: "", - data: []byte{}, - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - // No Add or Commit calls expected for directory creation - }, - expectError: false, - }, - { - name: "successful write without commit", - path: "test.txt", - ref: "", - data: []byte("test content"), - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash("abc123"), nil) - }, - expectError: false, - }, - { - name: "write with directory creation", - path: "dir/test.txt", - ref: "", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - mockTree.On("Add", "grafana/dir/test.txt").Return(plumbing.NewHash("abc123"), nil) - mockTree.On("Commit", "test write", mock.Anything).Return(plumbing.NewHash("def456"), nil) - }, - expectError: false, - }, - { - name: "error on add", - path: "test.txt", - ref: "", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash(""), fmt.Errorf("add error")) - }, - expectError: true, - errorType: fmt.Errorf("add error"), - }, - { - name: "error with ref", - path: "test.txt", - ref: "main", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - // No mock setup needed as it should fail before using the mock - }, - expectError: true, - errorType: fmt.Errorf("ref unsupported"), - }, - { - name: "empty path", - path: "", - ref: "", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - // No mock setup needed as it should fail before using the mock - }, - expectError: true, - errorType: fmt.Errorf("expected path"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup filesystem and repo - mockTree := NewMockWorktree(t) - tt.setupMock(mockTree) - - // Create a worktree with the filesystem - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: mockTree, - opts: repository.CloneOptions{ - PushOnWrites: tt.pushOnWrite, - }, - } - - // Test Write method - ctx := context.Background() - // Set author signature for the test - ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{ - Name: "Test User", - Email: "test@example.com", - When: time.Now(), - }) - - err := repo.Update(ctx, tt.path, tt.ref, tt.data, "test write") - - // Check results - if tt.expectError { - require.Error(t, err) - if tt.errorType != nil { - require.Contains(t, err.Error(), tt.errorType.Error()) - } - } else { - require.NoError(t, err) - } - - mockTree.AssertExpectations(t) - }) - } -} - -func TestGoGitRepo_Test(t *testing.T) { - tests := []struct { - name string - treeInitialized bool - expectedResult bool - }{ - { - name: "tree is initialized", - treeInitialized: true, - expectedResult: true, - }, - { - name: "tree is not initialized", - treeInitialized: false, - expectedResult: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mock tree - mockTree := NewMockWorktree(t) - - // Create repo with or without initialized tree - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: nil, - } - - if tt.treeInitialized { - repo.tree = mockTree - } - - // Test the Test method - ctx := context.Background() - result, err := repo.Test(ctx) - - // Verify results - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, tt.expectedResult, result.Success) - }) - } -} - -func TestGoGitRepo_Config(t *testing.T) { - // Create a test repository configuration - testConfig := &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - } - - // Create a repository instance with the test configuration - repo := &GoGitRepo{ - config: testConfig, - tree: NewMockWorktree(t), - } - - // Call the Config method - result := repo.Config() - - // Verify the result - require.NotNil(t, result) - require.Equal(t, testConfig, result) - require.Equal(t, "test-repo", result.Name) - require.Equal(t, "test-namespace", result.Namespace) - require.Equal(t, "grafana/", result.Spec.GitHub.Path) -} - -func TestGoGitRepo_Remove(t *testing.T) { - tests := []struct { - name string - setupMock func(t *testing.T) (*GoGitRepo, string) - expectError bool - expectedErrMsg string - }{ - { - name: "successful removal", - setupMock: func(t *testing.T) (*GoGitRepo, string) { - // Create a temporary directory that will be removed - tempDir, err := os.MkdirTemp("", "test-repo-*") - require.NoError(t, err) - - // Create a repository instance - repo := &GoGitRepo{ - dir: tempDir, - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - }, - } - - return repo, tempDir - }, - expectError: false, - }, - { - name: "directory already removed", - setupMock: func(t *testing.T) (*GoGitRepo, string) { - // Create a temporary directory - tempDir, err := os.MkdirTemp("", "test-repo-*") - require.NoError(t, err) - - // Remove it immediately to simulate it being already gone - err = os.RemoveAll(tempDir) - require.NoError(t, err) - - // Create a repository instance pointing to the removed directory - repo := &GoGitRepo{ - dir: tempDir, - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - }, - } - - return repo, tempDir - }, - expectError: false, // RemoveAll doesn't error if directory doesn't exist - }, - { - name: "invalid directory path", - setupMock: func(t *testing.T) (*GoGitRepo, string) { - // Create a repository instance with an invalid directory path - // that should cause an error when trying to remove - invalidPath := string([]byte{0}) - - repo := &GoGitRepo{ - dir: invalidPath, - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - }, - } - - return repo, invalidPath - }, - expectError: true, - expectedErrMsg: "invalid argument", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup the test - repo, _ := tt.setupMock(t) - - // Test the Remove method - ctx := context.Background() - err := repo.Remove(ctx) - - // Verify results - if tt.expectError { - require.Error(t, err) - if tt.expectedErrMsg != "" { - require.Contains(t, err.Error(), tt.expectedErrMsg) - } - } else { - require.NoError(t, err) - // Verify the directory no longer exists - _, statErr := os.Stat(repo.dir) - require.True(t, os.IsNotExist(statErr), "Directory should not exist after removal") - } - }) - } -} - -func TestGoGitRepo_Push(t *testing.T) { - tests := []struct { - name string - setupMock func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) - pushOpts repository.PushOptions - expectError bool - errorType error - }{ - { - name: "successful push", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.MatchedBy(func(o *git.PushOptions) bool { - if o.Auth == nil { - return false - } - // Verify we're using basic auth with expected credentials - basicAuth, ok := o.Auth.(*githttp.BasicAuth) - if !ok { - return false - } - return basicAuth.Username == "grafana" && basicAuth.Password == "test-token" - })).Return(nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{}, - expectError: false, - }, - { - name: "push error", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(fmt.Errorf("network error")) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{}, - expectError: true, - errorType: fmt.Errorf("network error"), - }, - { - name: "already up to date", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(git.NoErrAlreadyUpToDate) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{}, - expectError: false, - }, - { - name: "push with custom timeout", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{ - Timeout: 5 * time.Minute, - }, - expectError: false, - }, - { - name: "push with custom progress writer", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.MatchedBy(func(o *git.PushOptions) bool { - return o.Progress != nil && o.Progress != io.Discard - })).Return(nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{ - Progress: &bytes.Buffer{}, - }, - expectError: false, - }, - { - name: "push with BeforeFn success", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{ - BeforeFn: func() error { - return nil - }, - }, - expectError: false, - }, - { - name: "push with BeforeFn error", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - // No mock expectations since BeforeFn will fail before PushContext is called - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: NewMockRepository(t), - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, repo.repo.(*MockRepository), nil - }, - pushOpts: repository.PushOptions{ - BeforeFn: func() error { - return fmt.Errorf("before function failed") - }, - }, - expectError: true, - errorType: fmt.Errorf("before function failed"), - }, - { - name: "push with PushOnWrites=false commits changes", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil) - - mockTree := NewMockWorktree(t) - mockTree.On("Commit", "exported from grafana", mock.MatchedBy(func(o *git.CommitOptions) bool { - return o.All == true - })).Return(plumbing.NewHash("abc123"), nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - tree: mockTree, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: false, - }, - } - - return repo, mockRepo, mockTree - }, - pushOpts: repository.PushOptions{}, - expectError: false, - }, - { - name: "push with PushOnWrites=false and empty commit", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil) - - mockTree := NewMockWorktree(t) - mockTree.On("Commit", "exported from grafana", mock.Anything).Return(plumbing.ZeroHash, git.ErrEmptyCommit) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - tree: mockTree, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: false, - }, - } - - return repo, mockRepo, mockTree - }, - pushOpts: repository.PushOptions{}, - expectError: false, - }, - { - name: "push with PushOnWrites=false and commit error", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockTree := NewMockWorktree(t) - mockTree.On("Commit", "exported from grafana", mock.Anything).Return(plumbing.ZeroHash, fmt.Errorf("commit error")) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: NewMockRepository(t), - tree: mockTree, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: false, - }, - } - - return repo, repo.repo.(*MockRepository), mockTree - }, - pushOpts: repository.PushOptions{}, - expectError: true, - errorType: fmt.Errorf("commit error"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup the test - repo, mockRepo, mockTree := tt.setupMock(t) - - // Test the Push method - ctx := context.Background() - err := repo.Push(ctx, tt.pushOpts) - - // Verify results - if tt.expectError { - require.Error(t, err) - if tt.errorType != nil { - require.Contains(t, err.Error(), tt.errorType.Error()) - } - } else { - require.NoError(t, err) - } - - // Verify mock expectations if mocks were created - if mockRepo != nil { - mockRepo.AssertExpectations(t) - } - if mockTree != nil { - mockTree.AssertExpectations(t) - } - }) - } -} - -func TestGoGitRepo_ReadTree(t *testing.T) { - tests := []struct { - name string - setupMock func(t *testing.T) *GoGitRepo - ref string - expectError bool - expectedErrMsg string - expectedFiles []repository.FileTreeEntry - }{ - { - name: "successful read with files", - setupMock: func(t *testing.T) *GoGitRepo { - mockFS := memfs.New() - - // Create test files in the mock filesystem - require.NoError(t, mockFS.MkdirAll("grafana/folder1", 0750)) - file1, err := mockFS.Create("grafana/file1.txt") - require.NoError(t, err) - _, err = file1.Write([]byte("test content")) - require.NoError(t, err) - require.NoError(t, file1.Close()) - - file2, err := mockFS.Create("grafana/folder1/file2.txt") - require.NoError(t, err) - _, err = file2.Write([]byte("nested file content")) - require.NoError(t, err) - require.NoError(t, file2.Close()) - - mockTree := NewMockWorktree(t) - mockTree.On("Filesystem").Return(mockFS) - - return &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: mockTree, - } - }, - ref: "main", - expectError: false, - expectedFiles: []repository.FileTreeEntry{ - {Path: "file1.txt", Size: 12, Blob: true, Hash: "TODO/12"}, - {Path: "folder1", Size: 0, Blob: false}, - {Path: "folder1/file2.txt", Size: 19, Blob: true, Hash: "TODO/19"}, - }, - }, - { - name: "filesystem error", - setupMock: func(t *testing.T) *GoGitRepo { - mockTree := NewMockWorktree(t) - mockFS := memfs.New() - - // Create a filesystem that will return an error when accessed - mockTree.On("Filesystem").Return(mockFS) - - return &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "non-existent-path/", - }, - }, - }, - tree: mockTree, - } - }, - ref: "main", - expectError: false, // ReadTree handles fs.ErrNotExist by returning empty entries - expectedFiles: []repository.FileTreeEntry{}, - }, - { - name: "successful read with empty path", - setupMock: func(t *testing.T) *GoGitRepo { - mockFS := memfs.New() - - // Create test files in the mock filesystem - file1, err := mockFS.Create("file1.txt") - require.NoError(t, err) - _, err = file1.Write([]byte("test content")) - require.NoError(t, err) - require.NoError(t, file1.Close()) - - require.NoError(t, mockFS.MkdirAll("folder1", 0750)) - file2, err := mockFS.Create("folder1/file2.txt") - require.NoError(t, err) - _, err = file2.Write([]byte("nested file content")) - require.NoError(t, err) - require.NoError(t, file2.Close()) - - // Create .git directory which should be ignored - require.NoError(t, mockFS.MkdirAll(".git", 0750)) - gitFile, err := mockFS.Create(".git/config") - require.NoError(t, err) - _, err = gitFile.Write([]byte("git config")) - require.NoError(t, err) - require.NoError(t, gitFile.Close()) - - mockTree := NewMockWorktree(t) - mockTree.On("Filesystem").Return(mockFS) - - return &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "", - }, - }, - }, - tree: mockTree, - } - }, - ref: "main", - expectError: false, - expectedFiles: []repository.FileTreeEntry{ - {Path: "file1.txt", Size: 12, Blob: true, Hash: "TODO/12"}, - {Path: "folder1", Size: 0, Blob: false}, - {Path: "folder1/file2.txt", Size: 19, Blob: true, Hash: "TODO/19"}, - }, - }, - { - name: "filesystem error", - setupMock: func(t *testing.T) *GoGitRepo { - mockTree := NewMockWorktree(t) - mockFS := memfs.New() - - // Create a filesystem that will return an error when accessed - mockTree.On("Filesystem").Return(mockFS) - - return &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "non-existent-path/", - }, - }, - }, - tree: mockTree, - } - }, - ref: "main", - expectError: false, // ReadTree handles fs.ErrNotExist by returning empty entries - expectedFiles: []repository.FileTreeEntry{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup the test - repo := tt.setupMock(t) - - // Test the ReadTree method - ctx := context.Background() - entries, err := repo.ReadTree(ctx, tt.ref) - - // Verify results - if tt.expectError { - require.Error(t, err) - if tt.expectedErrMsg != "" { - require.Contains(t, err.Error(), tt.expectedErrMsg) - } - } else { - require.NoError(t, err) - - // Sort entries for consistent comparison - sort.Slice(entries, func(i, j int) bool { - return entries[i].Path < entries[j].Path - }) - sort.Slice(tt.expectedFiles, func(i, j int) bool { - return tt.expectedFiles[i].Path < tt.expectedFiles[j].Path - }) - - require.Equal(t, len(tt.expectedFiles), len(entries), "Number of entries should match") - for i, expected := range tt.expectedFiles { - require.Equal(t, expected.Path, entries[i].Path, "Path should match") - require.Equal(t, expected.Size, entries[i].Size, "Size should match") - require.Equal(t, expected.Blob, entries[i].Blob, "Blob flag should match") - if expected.Blob { - require.Equal(t, expected.Hash, entries[i].Hash, "Hash should match") - } - } - } - - // Verify mock expectations - repo.tree.(*MockWorktree).AssertExpectations(t) - }) - } -} - -func TestClone(t *testing.T) { - tests := []struct { - name string - root string - config *v0alpha1.Repository - createRepo bool - opts repository.CloneOptions - setupMock func(secrets *secrets.MockService) - expectError bool - errorMsg string - }{ - { - name: "successful clone", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - }, - createRepo: true, - opts: repository.CloneOptions{ - PushOnWrites: false, - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil) - }, - expectError: false, - }, - { - name: "successful clone with create if not exists", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "non-existent-branch", - }, - }, - }, - createRepo: true, - opts: repository.CloneOptions{ - PushOnWrites: false, - CreateIfNotExists: true, - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil) - }, - expectError: false, - }, - { - name: "timeout cancellation", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - }, - opts: repository.CloneOptions{ - Timeout: 1 * time.Millisecond, // Very short timeout to trigger cancellation - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil) - // Simulate a slow operation that will be cancelled by timeout - time.Sleep(20 * time.Millisecond) - }, - expectError: true, - errorMsg: "context deadline exceeded", - }, - { - name: "empty root", - root: "", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - }, - setupMock: func(mockSecrets *secrets.MockService) {}, - expectError: true, - errorMsg: "missing root config", - }, - { - name: "missing namespace", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - }, - }, - setupMock: func(mockSecrets *secrets.MockService) {}, - expectError: true, - errorMsg: "missing namespace", - }, - { - name: "missing name", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - }, - }, - setupMock: func(mockSecrets *secrets.MockService) {}, - expectError: true, - errorMsg: "missing name", - }, - { - name: "beforeFn error", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - }, - opts: repository.CloneOptions{ - BeforeFn: func() error { - return fmt.Errorf("beforeFn error") - }, - }, - setupMock: func(mockSecrets *secrets.MockService) {}, - expectError: true, - errorMsg: "beforeFn error", - }, - { - name: "secret decryption error", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - EncryptedToken: []byte("test-token"), - }, - }, - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), fmt.Errorf("error decrypting token")) - }, - expectError: true, - errorMsg: "error decrypting token", - }, - { - name: "clone error", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil) - }, - expectError: true, - errorMsg: "clone error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup test environment - mockSecrets := secrets.NewMockService(t) - tt.setupMock(mockSecrets) - - // Create a temporary directory for each test - if tt.root != "" { - tempDir := t.TempDir() - tt.root = tempDir - } - if tt.createRepo { - tt.config.Spec.GitHub.URL = createTestRepo(t) - } - - // Execute the test - ctx := context.Background() - repo, err := Clone(ctx, tt.root, tt.config, tt.opts, mockSecrets) - - // Verify results - if tt.expectError { - require.Error(t, err) - require.Contains(t, err.Error(), tt.errorMsg) - require.Nil(t, repo) - } else { - require.NoError(t, err) - require.NotNil(t, repo) - - // Verify the returned repository - gitRepo, ok := repo.(*GoGitRepo) - require.True(t, ok) - require.Equal(t, tt.config, gitRepo.config) - require.NotEmpty(t, gitRepo.dir) - require.NotNil(t, gitRepo.tree) - require.NotNil(t, gitRepo.repo) - - // Clean up - err = repo.Remove(ctx) - require.NoError(t, err) - } - mockSecrets.AssertExpectations(t) - }) - } -} - -func createTestRepo(t *testing.T) string { - // Create memory filesystem - fs := memfs.New() - - // Initialize new repo - repo, err := git.Init(memory.NewStorage(), fs) - require.NoError(t, err, "Failed to init test repo") - - w, err := repo.Worktree() - require.NoError(t, err, "Failed to get worktree") - - // Create a dummy file - f, err := fs.Create("README.md") - require.NoError(t, err, "Failed to create file") - _, err = f.Write([]byte("Hello, world!")) - require.NoError(t, err, "Failed to write content") - err = f.Close() - require.NoError(t, err, "Failed to close file") - - // Add and commit the file - _, err = w.Add("README.md") - require.NoError(t, err, "Failed to add file") - - // Create initial commit - _, err = w.Commit("initial commit", &git.CommitOptions{ - Author: &object.Signature{ - Name: "Test User", - Email: "test@example.com", - When: time.Now(), - }, - }) - require.NoError(t, err, "Failed to commit") - // Create a branch - headRef, err := repo.Head() - require.NoError(t, err, "Failed to get HEAD reference") - - // Create a new branch reference pointing to the current HEAD commit - branchRef := plumbing.NewBranchReferenceName("main") - ref := plumbing.NewHashReference(branchRef, headRef.Hash()) - - // Save the reference to create the branch - err = repo.Storer.SetReference(ref) - require.NoError(t, err, "Failed to create branch") - - // Checkout the new branch - err = w.Checkout(&git.CheckoutOptions{ - Branch: branchRef, - }) - require.NoError(t, err, "Failed to checkout branch") - - // Create a map of repositories for the server - repos := make(map[string]*git.Repository) - repos["test-repo.git"] = repo - - // Create and install the server - loader := server.MapLoader{ - "file://test-repo.git": repo.Storer, - } - srv := server.NewServer(loader) - client.InstallProtocol("file", srv) - - return "file://test-repo.git" -} diff --git a/pkg/registry/apis/provisioning/repository/local.go b/pkg/registry/apis/provisioning/repository/local/local.go similarity index 87% rename from pkg/registry/apis/provisioning/repository/local.go rename to pkg/registry/apis/provisioning/repository/local/local.go index fc7bd18c99f..5f63b83cfa1 100644 --- a/pkg/registry/apis/provisioning/repository/local.go +++ b/pkg/registry/apis/provisioning/repository/local/local.go @@ -1,4 +1,4 @@ -package repository +package local import ( "context" @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" ) @@ -75,9 +76,9 @@ func (r *LocalFolderResolver) LocalPath(p string) (string, error) { } var ( - _ Repository = (*localRepository)(nil) - _ Writer = (*localRepository)(nil) - _ Reader = (*localRepository)(nil) + _ repository.Repository = (*localRepository)(nil) + _ repository.Writer = (*localRepository)(nil) + _ repository.Reader = (*localRepository)(nil) ) type localRepository struct { @@ -147,17 +148,17 @@ func (r *localRepository) Validate() field.ErrorList { func (r *localRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { path := field.NewPath("spec", "local", "path") if r.config.Spec.Local.Path == "" { - return fromFieldError(field.Required(path, "no path is configured")), nil + return repository.FromFieldError(field.Required(path, "no path is configured")), nil } _, err := r.resolver.LocalPath(r.config.Spec.Local.Path) if err != nil { - return fromFieldError(field.Invalid(path, r.config.Spec.Local.Path, err.Error())), nil + return repository.FromFieldError(field.Invalid(path, r.config.Spec.Local.Path, err.Error())), nil } _, err = os.Stat(r.path) if errors.Is(err, os.ErrNotExist) { - return fromFieldError(field.NotFound(path, r.config.Spec.Local.Path)), nil + return repository.FromFieldError(field.NotFound(path, r.config.Spec.Local.Path)), nil } return &provisioning.TestResults{ @@ -176,7 +177,7 @@ func (r *localRepository) validateRequest(ref string) error { } // ReadResource implements provisioning.Repository. -func (r *localRepository) Read(ctx context.Context, filePath string, ref string) (*FileInfo, error) { +func (r *localRepository) Read(ctx context.Context, filePath string, ref string) (*repository.FileInfo, error) { if err := r.validateRequest(ref); err != nil { return nil, err } @@ -184,13 +185,13 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string) actualPath := safepath.Join(r.path, filePath) info, err := os.Stat(actualPath) if errors.Is(err, os.ErrNotExist) { - return nil, ErrFileNotFound + return nil, repository.ErrFileNotFound } else if err != nil { return nil, fmt.Errorf("stat file: %w", err) } if info.IsDir() { - return &FileInfo{ + return &repository.FileInfo{ Path: filePath, Modified: &metav1.Time{ Time: info.ModTime(), @@ -209,7 +210,7 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string) return nil, fmt.Errorf("calculate hash of file: %w", err) } - return &FileInfo{ + return &repository.FileInfo{ Path: filePath, Data: data, Hash: hash, @@ -220,7 +221,7 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string) } // ReadResource implements provisioning.Repository. -func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { +func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { if err := r.validateRequest(ref); err != nil { return nil, err } @@ -228,16 +229,16 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE // Return an empty list when folder does not exist _, err := os.Stat(r.path) if errors.Is(err, fs.ErrNotExist) { - return []FileTreeEntry{}, nil + return []repository.FileTreeEntry{}, nil } rootlen := len(r.path) - entries := make([]FileTreeEntry, 0, 100) + entries := make([]repository.FileTreeEntry, 0, 100) err = filepath.Walk(r.path, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } - entry := FileTreeEntry{ + entry := repository.FileTreeEntry{ Path: strings.TrimLeft(path[rootlen:], "/"), Size: info.Size(), } @@ -251,8 +252,10 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE if err != nil { return fmt.Errorf("read and calculate hash of path %s: %w", path, err) } + } else if !strings.HasSuffix(entry.Path, "/") { + // ensure trailing slash for directories + entry.Path = entry.Path + "/" } - // TODO: do folders have a trailing slash? entries = append(entries, entry) return err }) @@ -263,7 +266,7 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE func (r *localRepository) calculateFileHash(path string) (string, int64, error) { // Treats https://securego.io/docs/rules/g304.html if !safepath.InDir(path, r.path) { - return "", 0, ErrFileNotFound + return "", 0, repository.ErrFileNotFound } // We've already made sure the path is safe, so we'll ignore the gosec lint. @@ -329,7 +332,7 @@ func (r *localRepository) Update(ctx context.Context, path string, ref string, d f, err := os.Stat(path) if err != nil && errors.Is(err, os.ErrNotExist) { - return ErrFileNotFound + return repository.ErrFileNotFound } if f.IsDir() { return apierrors.NewBadRequest("path exists but it is a directory") @@ -360,5 +363,12 @@ func (r *localRepository) Delete(ctx context.Context, path string, ref string, c return err } - return os.Remove(safepath.Join(r.path, path)) + fullPath := safepath.Join(r.path, path) + + if safepath.IsDir(path) { + // if it is a folder, delete all of its contents + return os.RemoveAll(fullPath) + } + + return os.Remove(fullPath) } diff --git a/pkg/registry/apis/provisioning/repository/local_test.go b/pkg/registry/apis/provisioning/repository/local/local_test.go similarity index 96% rename from pkg/registry/apis/provisioning/repository/local_test.go rename to pkg/registry/apis/provisioning/repository/local/local_test.go index 7aabc542402..7895da24995 100644 --- a/pkg/registry/apis/provisioning/repository/local_test.go +++ b/pkg/registry/apis/provisioning/repository/local/local_test.go @@ -1,4 +1,4 @@ -package repository +package local import ( "context" @@ -19,6 +19,7 @@ import ( field "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" ) func TestLocalResolver(t *testing.T) { @@ -91,14 +92,14 @@ func TestLocalResolver(t *testing.T) { // Verify all directories and files are present expectedPaths := []string{ - "another", - "another/path", + "another/", + "another/path/", "another/path/file.txt", - "level1", + "level1/", "level1/file1.txt", - "level1/level2", + "level1/level2/", "level1/level2/file2.txt", - "level1/level2/level3", + "level1/level2/level3/", "level1/level2/level3/file3.txt", "root.txt", } @@ -542,6 +543,41 @@ func TestLocalRepository_Delete(t *testing.T) { comment: "test delete with ref", expectedErr: apierrors.NewBadRequest("local repository does not support ref"), }, + { + name: "delete folder with nested files", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + nestedFolderPath := filepath.Join(tempDir, "folder") + err := os.MkdirAll(nestedFolderPath, 0700) + require.NoError(t, err) + subFolderPath := filepath.Join(nestedFolderPath, "nested-folder") + err = os.MkdirAll(subFolderPath, 0700) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(nestedFolderPath, "nested-dash.txt"), []byte("content1"), 0600) + require.NoError(t, err) + + // Create repository with the temp directory as permitted prefix + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + path: "folder/", + ref: "", + comment: "test delete folder with nested content", + expectedErr: nil, + }, } for _, tc := range testCases { @@ -668,7 +704,7 @@ func TestLocalRepository_Update(t *testing.T) { ref: "", data: []byte("content"), comment: "", - expectedErr: ErrFileNotFound, + expectedErr: repository.ErrFileNotFound, }, { name: "update directory", @@ -1139,7 +1175,7 @@ func TestLocalRepository_Read(t *testing.T) { path string ref string expectedErr error - expected *FileInfo + expected *repository.FileInfo }{ { name: "read existing file", @@ -1168,7 +1204,7 @@ func TestLocalRepository_Read(t *testing.T) { return tempDir, repo }, path: "test-file.txt", - expected: &FileInfo{ + expected: &repository.FileInfo{ Path: "test-file.txt", Modified: &metav1.Time{Time: time.Now()}, Data: []byte("test content"), @@ -1196,7 +1232,7 @@ func TestLocalRepository_Read(t *testing.T) { return tempDir, repo }, path: "non-existent-file.txt", - expectedErr: ErrFileNotFound, + expectedErr: repository.ErrFileNotFound, }, { name: "read with ref should fail", @@ -1254,7 +1290,7 @@ func TestLocalRepository_Read(t *testing.T) { return tempDir, repo }, path: "test-dir", - expected: &FileInfo{ + expected: &repository.FileInfo{ Path: "test-dir", Modified: &metav1.Time{Time: time.Now()}, }, @@ -1292,7 +1328,7 @@ func TestLocalRepository_ReadTree(t *testing.T) { setup func(t *testing.T) (string, *localRepository) ref string expectedErr error - expected []FileTreeEntry + expected []repository.FileTreeEntry }{ { name: "read empty directory", @@ -1314,7 +1350,7 @@ func TestLocalRepository_ReadTree(t *testing.T) { return tempDir, repo }, - expected: []FileTreeEntry{}, + expected: []repository.FileTreeEntry{}, expectedErr: nil, }, { @@ -1344,10 +1380,10 @@ func TestLocalRepository_ReadTree(t *testing.T) { return tempDir, repo }, - expected: []FileTreeEntry{ + expected: []repository.FileTreeEntry{ {Path: "file1.txt", Blob: true, Size: 8}, {Path: "file2.txt", Blob: true, Size: 8}, - {Path: "subdir", Blob: false}, + {Path: "subdir/", Blob: false}, {Path: "subdir/file3.txt", Blob: true, Size: 8}, }, expectedErr: nil, @@ -1397,7 +1433,7 @@ func TestLocalRepository_ReadTree(t *testing.T) { return tempDir, repo }, - expected: []FileTreeEntry{}, + expected: []repository.FileTreeEntry{}, expectedErr: nil, }, } diff --git a/pkg/registry/apis/provisioning/repository/nanogit/github.go b/pkg/registry/apis/provisioning/repository/nanogit/github.go deleted file mode 100644 index 1305a79a014..00000000000 --- a/pkg/registry/apis/provisioning/repository/nanogit/github.go +++ /dev/null @@ -1,125 +0,0 @@ -package nanogit - -import ( - "context" - "strings" - - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "k8s.io/apimachinery/pkg/util/validation/field" - - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" -) - -// githubRepository is a repository implementation that integrates both a GitHub API-backed repository and a nanogit-based repository. -// It combines the features of the GitHub API with those of a standard Git repository. -// This is an interim solution to support both backends within a single repository abstraction. -// Once nanogit is fully integrated, functionality from GithubRepository should be migrated here, and this type should extend the nanogit.GitRepository interface. -type githubRepository struct { - apiRepo repository.GithubRepository - nanogitRepo repository.GitRepository -} - -func NewGithubRepository( - apiRepo repository.GithubRepository, - nanogitRepo repository.GitRepository, -) repository.GithubRepository { - return &githubRepository{ - apiRepo: apiRepo, - nanogitRepo: nanogitRepo, - } -} - -func (r *githubRepository) Config() *provisioning.Repository { - return r.nanogitRepo.Config() -} - -func (r *githubRepository) Owner() string { - return r.apiRepo.Owner() -} - -func (r *githubRepository) Repo() string { - return r.apiRepo.Repo() -} - -func (r *githubRepository) Client() pgh.Client { - return r.apiRepo.Client() -} - -// Validate extends the nanogit repo validation with github specific validation -func (r *githubRepository) Validate() (list field.ErrorList) { - cfg := r.nanogitRepo.Config() - gh := cfg.Spec.GitHub - if gh == nil { - list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required")) - return list - } - if gh.URL == "" { - list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required")) - } else { - _, _, err := repository.ParseOwnerRepoGithub(gh.URL) - if err != nil { - list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error())) - } else if !strings.HasPrefix(gh.URL, "https://github.com/") { - list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/")) - } - } - - if len(list) > 0 { - return list - } - - return r.nanogitRepo.Validate() -} - -// Test implements provisioning.Repository. -func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { - return r.apiRepo.Test(ctx) -} - -// ReadResource implements provisioning.Repository. -func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) { - return r.nanogitRepo.Read(ctx, filePath, ref) -} - -func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { - return r.nanogitRepo.ReadTree(ctx, ref) -} - -func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error { - return r.nanogitRepo.Create(ctx, path, ref, data, comment) -} - -func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error { - return r.nanogitRepo.Update(ctx, path, ref, data, comment) -} - -func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { - return r.nanogitRepo.Write(ctx, path, ref, data, message) -} - -func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error { - return r.nanogitRepo.Delete(ctx, path, ref, comment) -} - -func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) { - // Github API provides avatar URLs which nanogit does not, so we delegate to the github repo. - return r.apiRepo.History(ctx, path, ref) -} - -func (r *githubRepository) LatestRef(ctx context.Context) (string, error) { - return r.nanogitRepo.LatestRef(ctx) -} - -func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) { - return r.nanogitRepo.CompareFiles(ctx, base, ref) -} - -// ResourceURLs implements RepositoryWithURLs. -func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*provisioning.ResourceURLs, error) { - return r.apiRepo.ResourceURLs(ctx, file) -} - -func (r *githubRepository) Clone(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) { - return r.nanogitRepo.Clone(ctx, opts) -} diff --git a/pkg/registry/apis/provisioning/repository/nanogit/github_test.go b/pkg/registry/apis/provisioning/repository/nanogit/github_test.go deleted file mode 100644 index ef878a27c5c..00000000000 --- a/pkg/registry/apis/provisioning/repository/nanogit/github_test.go +++ /dev/null @@ -1,356 +0,0 @@ -package nanogit - -import ( - "context" - "testing" - - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -func TestGithubRepository(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - // Create a proper config for testing - expectedConfig := &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - } - - // Set up mock expectations for the methods that exist - gitRepo.EXPECT().Config().Return(expectedConfig) - apiRepo.EXPECT().Owner().Return("test") - apiRepo.EXPECT().Repo().Return("repo") - mockClient := pgh.NewMockClient(t) - apiRepo.EXPECT().Client().Return(mockClient) - - repo := NewGithubRepository(apiRepo, gitRepo) - - t.Run("delegates config to nanogit repo", func(t *testing.T) { - result := repo.Config() - require.Equal(t, expectedConfig, result) - }) - - t.Run("delegates owner to api repo", func(t *testing.T) { - result := repo.Owner() - require.Equal(t, "test", result) - }) - - t.Run("delegates repo to api repo", func(t *testing.T) { - result := repo.Repo() - require.Equal(t, "repo", result) - }) - - t.Run("delegates client to api repo", func(t *testing.T) { - result := repo.Client() - require.Equal(t, mockClient, result) - }) -} - -func TestGithubRepositoryDelegation(t *testing.T) { - ctx := context.Background() - - t.Run("delegates test to api repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedResult := &provisioning.TestResults{ - Code: 200, - Success: true, - } - - apiRepo.EXPECT().Test(ctx).Return(expectedResult, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.Test(ctx) - - require.NoError(t, err) - require.Equal(t, expectedResult, result) - }) - - t.Run("delegates read to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedFileInfo := &repository.FileInfo{ - Path: "test.yaml", - Data: []byte("test data"), - Ref: "main", - Hash: "abc123", - } - - gitRepo.EXPECT().Read(ctx, "test.yaml", "main").Return(expectedFileInfo, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.Read(ctx, "test.yaml", "main") - - require.NoError(t, err) - require.Equal(t, expectedFileInfo, result) - }) - - t.Run("delegates read tree to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedEntries := []repository.FileTreeEntry{ - {Path: "file1.yaml", Size: 100, Hash: "hash1", Blob: true}, - {Path: "dir/", Size: 0, Hash: "hash2", Blob: false}, - } - - gitRepo.EXPECT().ReadTree(ctx, "main").Return(expectedEntries, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.ReadTree(ctx, "main") - - require.NoError(t, err) - require.Equal(t, expectedEntries, result) - }) - - t.Run("delegates create to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - data := []byte("test content") - gitRepo.EXPECT().Create(ctx, "new-file.yaml", "main", data, "Create new file").Return(nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - err := repo.Create(ctx, "new-file.yaml", "main", data, "Create new file") - - require.NoError(t, err) - }) - - t.Run("delegates update to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - data := []byte("updated content") - gitRepo.EXPECT().Update(ctx, "existing-file.yaml", "main", data, "Update file").Return(nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - err := repo.Update(ctx, "existing-file.yaml", "main", data, "Update file") - - require.NoError(t, err) - }) - - t.Run("delegates write to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - data := []byte("file content") - gitRepo.EXPECT().Write(ctx, "file.yaml", "main", data, "Write file").Return(nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - err := repo.Write(ctx, "file.yaml", "main", data, "Write file") - - require.NoError(t, err) - }) - - t.Run("delegates delete to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - gitRepo.EXPECT().Delete(ctx, "file.yaml", "main", "Delete file").Return(nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - err := repo.Delete(ctx, "file.yaml", "main", "Delete file") - - require.NoError(t, err) - }) - - t.Run("delegates history to api repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedHistory := []provisioning.HistoryItem{ - { - Ref: "commit1", - Message: "First commit", - Authors: []provisioning.Author{{Name: "Test User"}}, - }, - } - - apiRepo.EXPECT().History(ctx, "file.yaml", "main").Return(expectedHistory, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.History(ctx, "file.yaml", "main") - - require.NoError(t, err) - require.Equal(t, expectedHistory, result) - }) - - t.Run("delegates latest ref to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedRef := "abc123def456" - gitRepo.EXPECT().LatestRef(ctx).Return(expectedRef, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.LatestRef(ctx) - - require.NoError(t, err) - require.Equal(t, expectedRef, result) - }) - - t.Run("delegates compare files to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedChanges := []repository.VersionedFileChange{ - { - Action: repository.FileActionCreated, - Path: "new-file.yaml", - Ref: "feature-branch", - }, - } - - gitRepo.EXPECT().CompareFiles(ctx, "main", "feature-branch").Return(expectedChanges, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.CompareFiles(ctx, "main", "feature-branch") - - require.NoError(t, err) - require.Equal(t, expectedChanges, result) - }) - - t.Run("delegates resource URLs to api repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - fileInfo := &repository.FileInfo{ - Path: "dashboard.json", - Ref: "main", - Hash: "hash123", - } - - expectedURLs := &provisioning.ResourceURLs{ - SourceURL: "https://github.com/test/repo/blob/main/dashboard.json", - RepositoryURL: "https://github.com/test/repo", - NewPullRequestURL: "https://github.com/test/repo/compare/main...feature", - } - - apiRepo.EXPECT().ResourceURLs(ctx, fileInfo).Return(expectedURLs, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.ResourceURLs(ctx, fileInfo) - - require.NoError(t, err) - require.Equal(t, expectedURLs, result) - }) - - t.Run("delegates clone to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - mockClonedRepo := repository.NewMockClonedRepository(t) - - opts := repository.CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, - } - - gitRepo.EXPECT().Clone(ctx, opts).Return(mockClonedRepo, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.Clone(ctx, opts) - - require.NoError(t, err) - require.Equal(t, mockClonedRepo, result) - }) -} - -func TestGithubRepositoryValidation(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - expectedErrors int - }{ - { - name: "missing github config", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - }, - }, - expectedErrors: 1, - }, - { - name: "missing github url", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - expectedErrors: 1, - }, - { - name: "invalid github url", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "invalid-url", - Branch: "main", - }, - }, - }, - expectedErrors: 1, - }, - { - name: "non-github url", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://gitlab.com/test/repo", - Branch: "main", - }, - }, - }, - expectedErrors: 1, - }, - { - name: "valid github config", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - }, - expectedErrors: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - // Set up mock expectations - gitRepo.EXPECT().Config().Return(tt.config) - if tt.expectedErrors == 0 { - // If no validation errors expected, nanogit validation should be called - gitRepo.EXPECT().Validate().Return(field.ErrorList{}) - } - - repo := NewGithubRepository(apiRepo, gitRepo) - - result := repo.Validate() - require.Len(t, result, tt.expectedErrors) - }) - } -} diff --git a/pkg/registry/apis/provisioning/repository/repository.go b/pkg/registry/apis/provisioning/repository/repository.go index fcd40d5ba80..9edddb03fad 100644 --- a/pkg/registry/apis/provisioning/repository/repository.go +++ b/pkg/registry/apis/provisioning/repository/repository.go @@ -2,9 +2,7 @@ package repository import ( "context" - "io" "net/http" - "time" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -67,47 +65,6 @@ type FileInfo struct { Modified *metav1.Time } -//go:generate mockery --name CloneFn --structname MockCloneFn --inpackage --filename clone_fn_mock.go --with-expecter -type CloneFn func(ctx context.Context, opts CloneOptions) (ClonedRepository, error) - -type CloneOptions struct { - // If the branch does not exist, create it - CreateIfNotExists bool - - // Push on every write - PushOnWrites bool - - // Maximum allowed size for repository clone in bytes (0 means no limit) - MaxSize int64 - - // Maximum time allowed for clone operation in seconds (0 means no limit) - Timeout time.Duration - - // Progress is the writer to report progress to - Progress io.Writer - - // BeforeFn is called before the clone operation starts - BeforeFn func() error -} - -//go:generate mockery --name ClonableRepository --structname MockClonableRepository --inpackage --filename clonable_repository_mock.go --with-expecter -type ClonableRepository interface { - Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) -} - -type PushOptions struct { - Timeout time.Duration - Progress io.Writer - BeforeFn func() error -} - -//go:generate mockery --name ClonedRepository --structname MockClonedRepository --inpackage --filename cloned_repository_mock.go --with-expecter -type ClonedRepository interface { - ReaderWriter - Push(ctx context.Context, opts PushOptions) error - Remove(ctx context.Context) error -} - // An entry in the file tree, as returned by 'ReadFileTree'. Like FileInfo, but contains less information. type FileTreeEntry struct { // The path to the file from the base path given (if any). diff --git a/pkg/registry/apis/provisioning/repository/stageable_repository_mock.go b/pkg/registry/apis/provisioning/repository/stageable_repository_mock.go new file mode 100644 index 00000000000..e1f7d619823 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/stageable_repository_mock.go @@ -0,0 +1,95 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package repository + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockStageableRepository is an autogenerated mock type for the StageableRepository type +type MockStageableRepository struct { + mock.Mock +} + +type MockStageableRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *MockStageableRepository) EXPECT() *MockStageableRepository_Expecter { + return &MockStageableRepository_Expecter{mock: &_m.Mock} +} + +// Stage provides a mock function with given fields: ctx, opts +func (_m *MockStageableRepository) Stage(ctx context.Context, opts StageOptions) (StagedRepository, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Stage") + } + + var r0 StagedRepository + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, StageOptions) (StagedRepository, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, StageOptions) StagedRepository); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(StagedRepository) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, StageOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockStageableRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage' +type MockStageableRepository_Stage_Call struct { + *mock.Call +} + +// Stage is a helper method to define mock.On call +// - ctx context.Context +// - opts StageOptions +func (_e *MockStageableRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockStageableRepository_Stage_Call { + return &MockStageableRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)} +} + +func (_c *MockStageableRepository_Stage_Call) Run(run func(ctx context.Context, opts StageOptions)) *MockStageableRepository_Stage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(StageOptions)) + }) + return _c +} + +func (_c *MockStageableRepository_Stage_Call) Return(_a0 StagedRepository, _a1 error) *MockStageableRepository_Stage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockStageableRepository_Stage_Call) RunAndReturn(run func(context.Context, StageOptions) (StagedRepository, error)) *MockStageableRepository_Stage_Call { + _c.Call.Return(run) + return _c +} + +// NewMockStageableRepository creates a new instance of MockStageableRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockStageableRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *MockStageableRepository { + mock := &MockStageableRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/repository/staged.go b/pkg/registry/apis/provisioning/repository/staged.go new file mode 100644 index 00000000000..6eb0aed357a --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/staged.go @@ -0,0 +1,71 @@ +package repository + +import ( + context "context" + "errors" + "fmt" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/nanogit" +) + +type StageOptions struct { + // Push on every write + PushOnWrites bool + // Maximum time allowed for clone operation in seconds (0 means no limit) + Timeout time.Duration +} + +//go:generate mockery --name StageableRepository --structname MockStageableRepository --inpackage --filename stageable_repository_mock.go --with-expecter +type StageableRepository interface { + Stage(ctx context.Context, opts StageOptions) (StagedRepository, error) +} + +//go:generate mockery --name StagedRepository --structname MockStagedRepository --inpackage --filename staged_repository_mock.go --with-expecter +type StagedRepository interface { + ReaderWriter + Push(ctx context.Context) error + Remove(ctx context.Context) error +} + +// WrapWithStageAndPushIfPossible attempts to stage the given repository. If staging is supported, +// it runs the provided function on the staged repository, then pushes any changes and cleans up the staged repository. +// If staging is not supported, it runs the function on the original repository without pushing. +// The 'staged' argument to the function indicates whether a staged repository was used. +func WrapWithStageAndPushIfPossible( + ctx context.Context, + repo Repository, + stageOptions StageOptions, + fn func(repo Repository, staged bool) error, +) error { + stageable, ok := repo.(StageableRepository) + if !ok { + return fn(repo, false) + } + + staged, err := stageable.Stage(ctx, stageOptions) + if err != nil { + return fmt.Errorf("stage repository: %w", err) + } + + // We don't, we simply log it + // FIXME: should we handle this differently? + defer func() { + if err := staged.Remove(ctx); err != nil { + logging.FromContext(ctx).Error("failed to remove staged repository after export", "err", err) + } + }() + + if err := fn(staged, true); err != nil { + return err + } + + if err = staged.Push(ctx); err != nil { + if errors.Is(err, nanogit.ErrNothingToPush) { + return nil // OK, already pushed + } + return fmt.Errorf("wrapped push error: %w", err) + } + return nil +} diff --git a/pkg/registry/apis/provisioning/repository/cloned_repository_mock.go b/pkg/registry/apis/provisioning/repository/staged_repository_mock.go similarity index 55% rename from pkg/registry/apis/provisioning/repository/cloned_repository_mock.go rename to pkg/registry/apis/provisioning/repository/staged_repository_mock.go index 7dc46ddeb77..916b6cc678e 100644 --- a/pkg/registry/apis/provisioning/repository/cloned_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/staged_repository_mock.go @@ -11,21 +11,21 @@ import ( v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" ) -// MockClonedRepository is an autogenerated mock type for the ClonedRepository type -type MockClonedRepository struct { +// MockStagedRepository is an autogenerated mock type for the StagedRepository type +type MockStagedRepository struct { mock.Mock } -type MockClonedRepository_Expecter struct { +type MockStagedRepository_Expecter struct { mock *mock.Mock } -func (_m *MockClonedRepository) EXPECT() *MockClonedRepository_Expecter { - return &MockClonedRepository_Expecter{mock: &_m.Mock} +func (_m *MockStagedRepository) EXPECT() *MockStagedRepository_Expecter { + return &MockStagedRepository_Expecter{mock: &_m.Mock} } // Config provides a mock function with no fields -func (_m *MockClonedRepository) Config() *v0alpha1.Repository { +func (_m *MockStagedRepository) Config() *v0alpha1.Repository { ret := _m.Called() if len(ret) == 0 { @@ -44,35 +44,35 @@ func (_m *MockClonedRepository) Config() *v0alpha1.Repository { return r0 } -// MockClonedRepository_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config' -type MockClonedRepository_Config_Call struct { +// MockStagedRepository_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config' +type MockStagedRepository_Config_Call struct { *mock.Call } // Config is a helper method to define mock.On call -func (_e *MockClonedRepository_Expecter) Config() *MockClonedRepository_Config_Call { - return &MockClonedRepository_Config_Call{Call: _e.mock.On("Config")} +func (_e *MockStagedRepository_Expecter) Config() *MockStagedRepository_Config_Call { + return &MockStagedRepository_Config_Call{Call: _e.mock.On("Config")} } -func (_c *MockClonedRepository_Config_Call) Run(run func()) *MockClonedRepository_Config_Call { +func (_c *MockStagedRepository_Config_Call) Run(run func()) *MockStagedRepository_Config_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockClonedRepository_Config_Call) Return(_a0 *v0alpha1.Repository) *MockClonedRepository_Config_Call { +func (_c *MockStagedRepository_Config_Call) Return(_a0 *v0alpha1.Repository) *MockStagedRepository_Config_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockClonedRepository_Config_Call { +func (_c *MockStagedRepository_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockStagedRepository_Config_Call { _c.Call.Return(run) return _c } // Create provides a mock function with given fields: ctx, path, ref, data, message -func (_m *MockClonedRepository) Create(ctx context.Context, path string, ref string, data []byte, message string) error { +func (_m *MockStagedRepository) Create(ctx context.Context, path string, ref string, data []byte, message string) error { ret := _m.Called(ctx, path, ref, data, message) if len(ret) == 0 { @@ -89,8 +89,8 @@ func (_m *MockClonedRepository) Create(ctx context.Context, path string, ref str return r0 } -// MockClonedRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type MockClonedRepository_Create_Call struct { +// MockStagedRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockStagedRepository_Create_Call struct { *mock.Call } @@ -100,29 +100,29 @@ type MockClonedRepository_Create_Call struct { // - ref string // - data []byte // - message string -func (_e *MockClonedRepository_Expecter) Create(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Create_Call { - return &MockClonedRepository_Create_Call{Call: _e.mock.On("Create", ctx, path, ref, data, message)} +func (_e *MockStagedRepository_Expecter) Create(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Create_Call { + return &MockStagedRepository_Create_Call{Call: _e.mock.On("Create", ctx, path, ref, data, message)} } -func (_c *MockClonedRepository_Create_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Create_Call { +func (_c *MockStagedRepository_Create_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_Create_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string)) }) return _c } -func (_c *MockClonedRepository_Create_Call) Return(_a0 error) *MockClonedRepository_Create_Call { +func (_c *MockStagedRepository_Create_Call) Return(_a0 error) *MockStagedRepository_Create_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Create_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Create_Call { +func (_c *MockStagedRepository_Create_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Create_Call { _c.Call.Return(run) return _c } // Delete provides a mock function with given fields: ctx, path, ref, message -func (_m *MockClonedRepository) Delete(ctx context.Context, path string, ref string, message string) error { +func (_m *MockStagedRepository) Delete(ctx context.Context, path string, ref string, message string) error { ret := _m.Called(ctx, path, ref, message) if len(ret) == 0 { @@ -139,8 +139,8 @@ func (_m *MockClonedRepository) Delete(ctx context.Context, path string, ref str return r0 } -// MockClonedRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type MockClonedRepository_Delete_Call struct { +// MockStagedRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockStagedRepository_Delete_Call struct { *mock.Call } @@ -149,38 +149,38 @@ type MockClonedRepository_Delete_Call struct { // - path string // - ref string // - message string -func (_e *MockClonedRepository_Expecter) Delete(ctx interface{}, path interface{}, ref interface{}, message interface{}) *MockClonedRepository_Delete_Call { - return &MockClonedRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, path, ref, message)} +func (_e *MockStagedRepository_Expecter) Delete(ctx interface{}, path interface{}, ref interface{}, message interface{}) *MockStagedRepository_Delete_Call { + return &MockStagedRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, path, ref, message)} } -func (_c *MockClonedRepository_Delete_Call) Run(run func(ctx context.Context, path string, ref string, message string)) *MockClonedRepository_Delete_Call { +func (_c *MockStagedRepository_Delete_Call) Run(run func(ctx context.Context, path string, ref string, message string)) *MockStagedRepository_Delete_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) }) return _c } -func (_c *MockClonedRepository_Delete_Call) Return(_a0 error) *MockClonedRepository_Delete_Call { +func (_c *MockStagedRepository_Delete_Call) Return(_a0 error) *MockStagedRepository_Delete_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockClonedRepository_Delete_Call { +func (_c *MockStagedRepository_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockStagedRepository_Delete_Call { _c.Call.Return(run) return _c } -// Push provides a mock function with given fields: ctx, opts -func (_m *MockClonedRepository) Push(ctx context.Context, opts PushOptions) error { - ret := _m.Called(ctx, opts) +// Push provides a mock function with given fields: ctx +func (_m *MockStagedRepository) Push(ctx context.Context) error { + ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for Push") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, PushOptions) error); ok { - r0 = rf(ctx, opts) + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) } else { r0 = ret.Error(0) } @@ -188,37 +188,36 @@ func (_m *MockClonedRepository) Push(ctx context.Context, opts PushOptions) erro return r0 } -// MockClonedRepository_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push' -type MockClonedRepository_Push_Call struct { +// MockStagedRepository_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push' +type MockStagedRepository_Push_Call struct { *mock.Call } // Push is a helper method to define mock.On call // - ctx context.Context -// - opts PushOptions -func (_e *MockClonedRepository_Expecter) Push(ctx interface{}, opts interface{}) *MockClonedRepository_Push_Call { - return &MockClonedRepository_Push_Call{Call: _e.mock.On("Push", ctx, opts)} +func (_e *MockStagedRepository_Expecter) Push(ctx interface{}) *MockStagedRepository_Push_Call { + return &MockStagedRepository_Push_Call{Call: _e.mock.On("Push", ctx)} } -func (_c *MockClonedRepository_Push_Call) Run(run func(ctx context.Context, opts PushOptions)) *MockClonedRepository_Push_Call { +func (_c *MockStagedRepository_Push_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Push_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(PushOptions)) + run(args[0].(context.Context)) }) return _c } -func (_c *MockClonedRepository_Push_Call) Return(_a0 error) *MockClonedRepository_Push_Call { +func (_c *MockStagedRepository_Push_Call) Return(_a0 error) *MockStagedRepository_Push_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Push_Call) RunAndReturn(run func(context.Context, PushOptions) error) *MockClonedRepository_Push_Call { +func (_c *MockStagedRepository_Push_Call) RunAndReturn(run func(context.Context) error) *MockStagedRepository_Push_Call { _c.Call.Return(run) return _c } // Read provides a mock function with given fields: ctx, path, ref -func (_m *MockClonedRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { +func (_m *MockStagedRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { ret := _m.Called(ctx, path, ref) if len(ret) == 0 { @@ -247,8 +246,8 @@ func (_m *MockClonedRepository) Read(ctx context.Context, path string, ref strin return r0, r1 } -// MockClonedRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' -type MockClonedRepository_Read_Call struct { +// MockStagedRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockStagedRepository_Read_Call struct { *mock.Call } @@ -256,29 +255,29 @@ type MockClonedRepository_Read_Call struct { // - ctx context.Context // - path string // - ref string -func (_e *MockClonedRepository_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockClonedRepository_Read_Call { - return &MockClonedRepository_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)} +func (_e *MockStagedRepository_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockStagedRepository_Read_Call { + return &MockStagedRepository_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)} } -func (_c *MockClonedRepository_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockClonedRepository_Read_Call { +func (_c *MockStagedRepository_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockStagedRepository_Read_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) return _c } -func (_c *MockClonedRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockClonedRepository_Read_Call { +func (_c *MockStagedRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockStagedRepository_Read_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClonedRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockClonedRepository_Read_Call { +func (_c *MockStagedRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockStagedRepository_Read_Call { _c.Call.Return(run) return _c } // ReadTree provides a mock function with given fields: ctx, ref -func (_m *MockClonedRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { +func (_m *MockStagedRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { ret := _m.Called(ctx, ref) if len(ret) == 0 { @@ -307,37 +306,37 @@ func (_m *MockClonedRepository) ReadTree(ctx context.Context, ref string) ([]Fil return r0, r1 } -// MockClonedRepository_ReadTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadTree' -type MockClonedRepository_ReadTree_Call struct { +// MockStagedRepository_ReadTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadTree' +type MockStagedRepository_ReadTree_Call struct { *mock.Call } // ReadTree is a helper method to define mock.On call // - ctx context.Context // - ref string -func (_e *MockClonedRepository_Expecter) ReadTree(ctx interface{}, ref interface{}) *MockClonedRepository_ReadTree_Call { - return &MockClonedRepository_ReadTree_Call{Call: _e.mock.On("ReadTree", ctx, ref)} +func (_e *MockStagedRepository_Expecter) ReadTree(ctx interface{}, ref interface{}) *MockStagedRepository_ReadTree_Call { + return &MockStagedRepository_ReadTree_Call{Call: _e.mock.On("ReadTree", ctx, ref)} } -func (_c *MockClonedRepository_ReadTree_Call) Run(run func(ctx context.Context, ref string)) *MockClonedRepository_ReadTree_Call { +func (_c *MockStagedRepository_ReadTree_Call) Run(run func(ctx context.Context, ref string)) *MockStagedRepository_ReadTree_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string)) }) return _c } -func (_c *MockClonedRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockClonedRepository_ReadTree_Call { +func (_c *MockStagedRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockStagedRepository_ReadTree_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClonedRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockClonedRepository_ReadTree_Call { +func (_c *MockStagedRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockStagedRepository_ReadTree_Call { _c.Call.Return(run) return _c } // Remove provides a mock function with given fields: ctx -func (_m *MockClonedRepository) Remove(ctx context.Context) error { +func (_m *MockStagedRepository) Remove(ctx context.Context) error { ret := _m.Called(ctx) if len(ret) == 0 { @@ -354,36 +353,36 @@ func (_m *MockClonedRepository) Remove(ctx context.Context) error { return r0 } -// MockClonedRepository_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' -type MockClonedRepository_Remove_Call struct { +// MockStagedRepository_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' +type MockStagedRepository_Remove_Call struct { *mock.Call } // Remove is a helper method to define mock.On call // - ctx context.Context -func (_e *MockClonedRepository_Expecter) Remove(ctx interface{}) *MockClonedRepository_Remove_Call { - return &MockClonedRepository_Remove_Call{Call: _e.mock.On("Remove", ctx)} +func (_e *MockStagedRepository_Expecter) Remove(ctx interface{}) *MockStagedRepository_Remove_Call { + return &MockStagedRepository_Remove_Call{Call: _e.mock.On("Remove", ctx)} } -func (_c *MockClonedRepository_Remove_Call) Run(run func(ctx context.Context)) *MockClonedRepository_Remove_Call { +func (_c *MockStagedRepository_Remove_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Remove_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) return _c } -func (_c *MockClonedRepository_Remove_Call) Return(_a0 error) *MockClonedRepository_Remove_Call { +func (_c *MockStagedRepository_Remove_Call) Return(_a0 error) *MockStagedRepository_Remove_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Remove_Call) RunAndReturn(run func(context.Context) error) *MockClonedRepository_Remove_Call { +func (_c *MockStagedRepository_Remove_Call) RunAndReturn(run func(context.Context) error) *MockStagedRepository_Remove_Call { _c.Call.Return(run) return _c } // Test provides a mock function with given fields: ctx -func (_m *MockClonedRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) { +func (_m *MockStagedRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) { ret := _m.Called(ctx) if len(ret) == 0 { @@ -412,36 +411,36 @@ func (_m *MockClonedRepository) Test(ctx context.Context) (*v0alpha1.TestResults return r0, r1 } -// MockClonedRepository_Test_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Test' -type MockClonedRepository_Test_Call struct { +// MockStagedRepository_Test_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Test' +type MockStagedRepository_Test_Call struct { *mock.Call } // Test is a helper method to define mock.On call // - ctx context.Context -func (_e *MockClonedRepository_Expecter) Test(ctx interface{}) *MockClonedRepository_Test_Call { - return &MockClonedRepository_Test_Call{Call: _e.mock.On("Test", ctx)} +func (_e *MockStagedRepository_Expecter) Test(ctx interface{}) *MockStagedRepository_Test_Call { + return &MockStagedRepository_Test_Call{Call: _e.mock.On("Test", ctx)} } -func (_c *MockClonedRepository_Test_Call) Run(run func(ctx context.Context)) *MockClonedRepository_Test_Call { +func (_c *MockStagedRepository_Test_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Test_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) return _c } -func (_c *MockClonedRepository_Test_Call) Return(_a0 *v0alpha1.TestResults, _a1 error) *MockClonedRepository_Test_Call { +func (_c *MockStagedRepository_Test_Call) Return(_a0 *v0alpha1.TestResults, _a1 error) *MockStagedRepository_Test_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClonedRepository_Test_Call) RunAndReturn(run func(context.Context) (*v0alpha1.TestResults, error)) *MockClonedRepository_Test_Call { +func (_c *MockStagedRepository_Test_Call) RunAndReturn(run func(context.Context) (*v0alpha1.TestResults, error)) *MockStagedRepository_Test_Call { _c.Call.Return(run) return _c } // Update provides a mock function with given fields: ctx, path, ref, data, message -func (_m *MockClonedRepository) Update(ctx context.Context, path string, ref string, data []byte, message string) error { +func (_m *MockStagedRepository) Update(ctx context.Context, path string, ref string, data []byte, message string) error { ret := _m.Called(ctx, path, ref, data, message) if len(ret) == 0 { @@ -458,8 +457,8 @@ func (_m *MockClonedRepository) Update(ctx context.Context, path string, ref str return r0 } -// MockClonedRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type MockClonedRepository_Update_Call struct { +// MockStagedRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockStagedRepository_Update_Call struct { *mock.Call } @@ -469,29 +468,29 @@ type MockClonedRepository_Update_Call struct { // - ref string // - data []byte // - message string -func (_e *MockClonedRepository_Expecter) Update(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Update_Call { - return &MockClonedRepository_Update_Call{Call: _e.mock.On("Update", ctx, path, ref, data, message)} +func (_e *MockStagedRepository_Expecter) Update(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Update_Call { + return &MockStagedRepository_Update_Call{Call: _e.mock.On("Update", ctx, path, ref, data, message)} } -func (_c *MockClonedRepository_Update_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Update_Call { +func (_c *MockStagedRepository_Update_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_Update_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string)) }) return _c } -func (_c *MockClonedRepository_Update_Call) Return(_a0 error) *MockClonedRepository_Update_Call { +func (_c *MockStagedRepository_Update_Call) Return(_a0 error) *MockStagedRepository_Update_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Update_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Update_Call { +func (_c *MockStagedRepository_Update_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Update_Call { _c.Call.Return(run) return _c } // Validate provides a mock function with no fields -func (_m *MockClonedRepository) Validate() field.ErrorList { +func (_m *MockStagedRepository) Validate() field.ErrorList { ret := _m.Called() if len(ret) == 0 { @@ -510,35 +509,35 @@ func (_m *MockClonedRepository) Validate() field.ErrorList { return r0 } -// MockClonedRepository_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate' -type MockClonedRepository_Validate_Call struct { +// MockStagedRepository_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate' +type MockStagedRepository_Validate_Call struct { *mock.Call } // Validate is a helper method to define mock.On call -func (_e *MockClonedRepository_Expecter) Validate() *MockClonedRepository_Validate_Call { - return &MockClonedRepository_Validate_Call{Call: _e.mock.On("Validate")} +func (_e *MockStagedRepository_Expecter) Validate() *MockStagedRepository_Validate_Call { + return &MockStagedRepository_Validate_Call{Call: _e.mock.On("Validate")} } -func (_c *MockClonedRepository_Validate_Call) Run(run func()) *MockClonedRepository_Validate_Call { +func (_c *MockStagedRepository_Validate_Call) Run(run func()) *MockStagedRepository_Validate_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockClonedRepository_Validate_Call) Return(_a0 field.ErrorList) *MockClonedRepository_Validate_Call { +func (_c *MockStagedRepository_Validate_Call) Return(_a0 field.ErrorList) *MockStagedRepository_Validate_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Validate_Call) RunAndReturn(run func() field.ErrorList) *MockClonedRepository_Validate_Call { +func (_c *MockStagedRepository_Validate_Call) RunAndReturn(run func() field.ErrorList) *MockStagedRepository_Validate_Call { _c.Call.Return(run) return _c } // Write provides a mock function with given fields: ctx, path, ref, data, message -func (_m *MockClonedRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { +func (_m *MockStagedRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { ret := _m.Called(ctx, path, ref, data, message) if len(ret) == 0 { @@ -555,8 +554,8 @@ func (_m *MockClonedRepository) Write(ctx context.Context, path string, ref stri return r0 } -// MockClonedRepository_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' -type MockClonedRepository_Write_Call struct { +// MockStagedRepository_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' +type MockStagedRepository_Write_Call struct { *mock.Call } @@ -566,34 +565,34 @@ type MockClonedRepository_Write_Call struct { // - ref string // - data []byte // - message string -func (_e *MockClonedRepository_Expecter) Write(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Write_Call { - return &MockClonedRepository_Write_Call{Call: _e.mock.On("Write", ctx, path, ref, data, message)} +func (_e *MockStagedRepository_Expecter) Write(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Write_Call { + return &MockStagedRepository_Write_Call{Call: _e.mock.On("Write", ctx, path, ref, data, message)} } -func (_c *MockClonedRepository_Write_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Write_Call { +func (_c *MockStagedRepository_Write_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_Write_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string)) }) return _c } -func (_c *MockClonedRepository_Write_Call) Return(_a0 error) *MockClonedRepository_Write_Call { +func (_c *MockStagedRepository_Write_Call) Return(_a0 error) *MockStagedRepository_Write_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Write_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Write_Call { +func (_c *MockStagedRepository_Write_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Write_Call { _c.Call.Return(run) return _c } -// NewMockClonedRepository creates a new instance of MockClonedRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockStagedRepository creates a new instance of MockStagedRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockClonedRepository(t interface { +func NewMockStagedRepository(t interface { mock.TestingT Cleanup(func()) -}) *MockClonedRepository { - mock := &MockClonedRepository{} +}) *MockStagedRepository { + mock := &MockStagedRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/pkg/registry/apis/provisioning/repository/staged_test.go b/pkg/registry/apis/provisioning/repository/staged_test.go new file mode 100644 index 00000000000..3efe8ebbd5e --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/staged_test.go @@ -0,0 +1,144 @@ +package repository + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type mockStagedRepo struct { + *MockStageableRepository + *MockStagedRepository +} + +func Test_WrapWithStageAndPushIfPossible_NonStageableRepository(t *testing.T) { + nonStageable := NewMockRepository(t) + var called bool + fn := func(repo Repository, staged bool) error { + called = true + return errors.New("operation failed") + } + + err := WrapWithStageAndPushIfPossible(context.Background(), nonStageable, StageOptions{}, fn) + require.EqualError(t, err, "operation failed") + require.True(t, called) +} + +func TestWrapWithStageAndPushIfPossible(t *testing.T) { + tests := []struct { + name string + setupMocks func(t *testing.T) *mockStagedRepo + operation func(repo Repository, staged bool) error + expectedError string + }{ + { + name: "successful stage, operation, and push", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + mockStaged := NewMockStagedRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil) + mockStaged.EXPECT().Push(mock.Anything).Return(nil) + mockStaged.EXPECT().Remove(mock.Anything).Return(nil) + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + MockStagedRepository: mockStaged, + } + }, + operation: func(repo Repository, staged bool) error { + require.True(t, staged) + return nil + }, + }, + { + name: "stage failure", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(nil, errors.New("stage failed")) + + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + } + }, + operation: func(repo Repository, staged bool) error { + return nil + }, + expectedError: "stage repository: stage failed", + }, + { + name: "operation failure", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + mockStaged := NewMockStagedRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil) + mockStaged.EXPECT().Remove(mock.Anything).Return(nil) + + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + MockStagedRepository: mockStaged, + } + }, + operation: func(repo Repository, staged bool) error { + return errors.New("operation failed") + }, + expectedError: "operation failed", + }, + { + name: "push failure", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + mockStaged := NewMockStagedRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil) + mockStaged.EXPECT().Push(mock.Anything).Return(errors.New("push failed")) + mockStaged.EXPECT().Remove(mock.Anything).Return(nil) + + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + MockStagedRepository: mockStaged, + } + }, + operation: func(repo Repository, staged bool) error { + return nil + }, + expectedError: "wrapped push error: push failed", + }, + { + name: "remove failure should only log", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + mockStaged := NewMockStagedRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil) + mockStaged.EXPECT().Push(mock.Anything).Return(nil) + mockStaged.EXPECT().Remove(mock.Anything).Return(errors.New("remove failed")) + + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + MockStagedRepository: mockStaged, + } + }, + operation: func(repo Repository, staged bool) error { + return nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := tt.setupMocks(t) + err := WrapWithStageAndPushIfPossible(context.Background(), repo, StageOptions{}, tt.operation) + + if tt.expectedError != "" { + require.EqualError(t, err, tt.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/test.go b/pkg/registry/apis/provisioning/repository/test.go index 0e5778f76b3..f8b72d5b2af 100644 --- a/pkg/registry/apis/provisioning/repository/test.go +++ b/pkg/registry/apis/provisioning/repository/test.go @@ -96,7 +96,7 @@ func ValidateRepository(repo Repository) field.ErrorList { return list } -func fromFieldError(err *field.Error) *provisioning.TestResults { +func FromFieldError(err *field.Error) *provisioning.TestResults { return &provisioning.TestResults{ Code: http.StatusBadRequest, Success: false, diff --git a/pkg/registry/apis/provisioning/repository/test_test.go b/pkg/registry/apis/provisioning/repository/test_test.go index 3121bcc4110..9168d529ea0 100644 --- a/pkg/registry/apis/provisioning/repository/test_test.go +++ b/pkg/registry/apis/provisioning/repository/test_test.go @@ -431,7 +431,7 @@ func TestFromFieldError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := fromFieldError(tt.fieldError) + result := FromFieldError(tt.fieldError) require.NotNil(t, result) require.Equal(t, tt.expectedCode, result.Code) diff --git a/pkg/registry/apis/provisioning/resources/client_factory_mock.go b/pkg/registry/apis/provisioning/resources/client_factory_mock.go index b8aa174e04f..d118582d35c 100644 --- a/pkg/registry/apis/provisioning/resources/client_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/client_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/clients_mock.go b/pkg/registry/apis/provisioning/resources/clients_mock.go index d78496c2183..4af4559943a 100644 --- a/pkg/registry/apis/provisioning/resources/clients_mock.go +++ b/pkg/registry/apis/provisioning/resources/clients_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/dualwriter.go b/pkg/registry/apis/provisioning/resources/dualwriter.go index 7d7b46e0406..2145285d52e 100644 --- a/pkg/registry/apis/provisioning/resources/dualwriter.go +++ b/pkg/registry/apis/provisioning/resources/dualwriter.go @@ -6,6 +6,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" @@ -76,9 +77,8 @@ func (r *DualReadWriter) Delete(ctx context.Context, opts DualWriteOptions) (*Pa return nil, err } - // TODO: implement this if safepath.IsDir(opts.Path) { - return nil, fmt.Errorf("folder delete not supported") + return r.deleteFolder(ctx, opts) } // Read the file from the default branch as it won't exist in the possibly new branch @@ -166,6 +166,12 @@ func (r *DualReadWriter) CreateFolder(ctx context.Context, opts DualWriteOptions }, } + urls, err := getFolderURLs(ctx, opts.Path, opts.Ref, r.repo) + if err != nil { + return nil, err + } + wrap.URLs = urls + if opts.Ref == "" { folderName, err := r.folders.EnsureFolderPathExist(ctx, opts.Path) if err != nil { @@ -317,3 +323,141 @@ func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, _ string) er return apierrors.NewForbidden(FolderResource.GroupResource(), "", fmt.Errorf("must be admin or editor to access folders with provisioning")) } + +func (r *DualReadWriter) deleteFolder(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) { + // if the ref is set, it is not the active branch, so just delete the files from the branch + // and do not delete the items from grafana itself + if opts.Ref != "" { + err := r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message) + if err != nil { + return nil, fmt.Errorf("error deleting folder from repository: %w", err) + } + + return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo) + } + + // before deleting from the repo, first get all children resources to delete from grafana afterwards + treeEntries, err := r.repo.ReadTree(ctx, "") + if err != nil { + return nil, fmt.Errorf("read repository tree: %w", err) + } + // note: parsedFolders will include the folder itself + parsedResources, parsedFolders, err := r.getChildren(ctx, opts.Path, treeEntries) + if err != nil { + return nil, fmt.Errorf("parse resources in folder: %w", err) + } + + // delete from the repo + err = r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message) + if err != nil { + return nil, fmt.Errorf("delete folder from repository: %w", err) + } + + // delete from grafana + ctx, _, err = identity.WithProvisioningIdentity(ctx, r.repo.Config().Namespace) + if err != nil { + return nil, err + } + if err := r.deleteChildren(ctx, parsedResources, parsedFolders); err != nil { + return nil, fmt.Errorf("delete folder from grafana: %w", err) + } + + return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo) +} + +func getFolderURLs(ctx context.Context, path, ref string, repo repository.Repository) (*provisioning.ResourceURLs, error) { + if urlRepo, ok := repo.(repository.RepositoryWithURLs); ok && ref != "" { + urls, err := urlRepo.ResourceURLs(ctx, &repository.FileInfo{Path: path, Ref: ref}) + if err != nil { + return nil, err + } + return urls, nil + } + return nil, nil +} + +func folderDeleteResponse(ctx context.Context, path, ref string, repo repository.Repository) (*ParsedResource, error) { + urls, err := getFolderURLs(ctx, path, ref, repo) + if err != nil { + return nil, err + } + + parsed := &ParsedResource{ + Action: provisioning.ResourceActionDelete, + Info: &repository.FileInfo{ + Path: path, + Ref: ref, + }, + GVK: schema.GroupVersionKind{ + Group: FolderResource.Group, + Version: FolderResource.Version, + Kind: "Folder", + }, + GVR: FolderResource, + Repo: provisioning.ResourceRepositoryInfo{ + Type: repo.Config().Spec.Type, + Namespace: repo.Config().Namespace, + Name: repo.Config().Name, + Title: repo.Config().Spec.Title, + }, + URLs: urls, + } + + return parsed, nil +} + +func (r *DualReadWriter) getChildren(ctx context.Context, folderPath string, treeEntries []repository.FileTreeEntry) ([]*ParsedResource, []Folder, error) { + var resourcesInFolder []repository.FileTreeEntry + var foldersInFolder []Folder + for _, entry := range treeEntries { + // make sure the path is supported (i.e. not ignored by git sync) and that the path is the folder itself or a child of the folder + if IsPathSupported(entry.Path) != nil || !safepath.InDir(entry.Path, folderPath) { + continue + } + // folders cannot be parsed as resources, so handle them separately + if entry.Blob { + resourcesInFolder = append(resourcesInFolder, entry) + } else { + folder := ParseFolder(entry.Path, r.repo.Config().Name) + foldersInFolder = append(foldersInFolder, folder) + } + } + + parsedResources := make([]*ParsedResource, len(resourcesInFolder)) + for i, entry := range resourcesInFolder { + fileInfo, err := r.repo.Read(ctx, entry.Path, "") + if err != nil && !apierrors.IsNotFound(err) { + return nil, nil, fmt.Errorf("could not find resource in repository: %w", err) + } + + parsed, err := r.parser.Parse(ctx, fileInfo) + if err != nil { + return nil, nil, fmt.Errorf("could not parse resource: %w", err) + } + + parsedResources[i] = parsed + } + + return parsedResources, foldersInFolder, nil +} + +func (r *DualReadWriter) deleteChildren(ctx context.Context, childrenResources []*ParsedResource, folders []Folder) error { + for _, parsed := range childrenResources { + err := parsed.Client.Delete(ctx, parsed.Obj.GetName(), metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to delete nested resource from grafana: %w", err) + } + } + + // we need to delete the folders furthest down in the tree first, as folder deletion will fail if there is anything inside of it + safepath.SortByDepth(folders, func(f Folder) string { return f.Path }, false) + + for _, f := range folders { + err := r.folders.Client().Delete(ctx, f.ID, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("failed to delete folder from grafana: %w", err) + } + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/resources/parser_factory_mock.go b/pkg/registry/apis/provisioning/resources/parser_factory_mock.go index 434863be21c..b266809d58e 100644 --- a/pkg/registry/apis/provisioning/resources/parser_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/parser_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/parser_mock.go b/pkg/registry/apis/provisioning/resources/parser_mock.go index c207d0e9095..21e6ab3e78e 100644 --- a/pkg/registry/apis/provisioning/resources/parser_mock.go +++ b/pkg/registry/apis/provisioning/resources/parser_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go b/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go index 3e55762e261..9bf9473d180 100644 --- a/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/repository_resources_mock.go b/pkg/registry/apis/provisioning/resources/repository_resources_mock.go index 6245188528e..7e11d9b9eda 100644 --- a/pkg/registry/apis/provisioning/resources/repository_resources_mock.go +++ b/pkg/registry/apis/provisioning/resources/repository_resources_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources @@ -26,64 +26,6 @@ func (_m *MockRepositoryResources) EXPECT() *MockRepositoryResources_Expecter { return &MockRepositoryResources_Expecter{mock: &_m.Mock} } -// WriteResourceFileFromObject provides a mock function with given fields: ctx, obj, options -func (_m *MockRepositoryResources) WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) { - ret := _m.Called(ctx, obj, options) - - if len(ret) == 0 { - panic("no return value specified for WriteResourceFileFromObject") - } - - var r0 string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)); ok { - return rf(ctx, obj, options) - } - if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, WriteOptions) string); ok { - r0 = rf(ctx, obj, options) - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func(context.Context, *unstructured.Unstructured, WriteOptions) error); ok { - r1 = rf(ctx, obj, options) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockRepositoryResources_WriteResourceFileFromObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteResourceFileFromObject' -type MockRepositoryResources_WriteResourceFileFromObject_Call struct { - *mock.Call -} - -// WriteResourceFileFromObject is a helper method to define mock.On call -// - ctx context.Context -// - obj *unstructured.Unstructured -// - options WriteOptions -func (_e *MockRepositoryResources_Expecter) WriteResourceFileFromObject(ctx interface{}, obj interface{}, options interface{}) *MockRepositoryResources_WriteResourceFileFromObject_Call { - return &MockRepositoryResources_WriteResourceFileFromObject_Call{Call: _e.mock.On("WriteResourceFileFromObject", ctx, obj, options)} -} - -func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Run(run func(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions)) *MockRepositoryResources_WriteResourceFileFromObject_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*unstructured.Unstructured), args[2].(WriteOptions)) - }) - return _c -} - -func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Return(_a0 string, _a1 error) *MockRepositoryResources_WriteResourceFileFromObject_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) RunAndReturn(run func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)) *MockRepositoryResources_WriteResourceFileFromObject_Call { - _c.Call.Return(run) - return _c -} - // EnsureFolderExists provides a mock function with given fields: ctx, folder, parentID func (_m *MockRepositoryResources) EnsureFolderExists(ctx context.Context, folder Folder, parentID string) error { ret := _m.Called(ctx, folder, parentID) @@ -520,6 +462,64 @@ func (_c *MockRepositoryResources_Stats_Call) RunAndReturn(run func(context.Cont return _c } +// WriteResourceFileFromObject provides a mock function with given fields: ctx, obj, options +func (_m *MockRepositoryResources) WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) { + ret := _m.Called(ctx, obj, options) + + if len(ret) == 0 { + panic("no return value specified for WriteResourceFileFromObject") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)); ok { + return rf(ctx, obj, options) + } + if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, WriteOptions) string); ok { + r0 = rf(ctx, obj, options) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, *unstructured.Unstructured, WriteOptions) error); ok { + r1 = rf(ctx, obj, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepositoryResources_WriteResourceFileFromObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteResourceFileFromObject' +type MockRepositoryResources_WriteResourceFileFromObject_Call struct { + *mock.Call +} + +// WriteResourceFileFromObject is a helper method to define mock.On call +// - ctx context.Context +// - obj *unstructured.Unstructured +// - options WriteOptions +func (_e *MockRepositoryResources_Expecter) WriteResourceFileFromObject(ctx interface{}, obj interface{}, options interface{}) *MockRepositoryResources_WriteResourceFileFromObject_Call { + return &MockRepositoryResources_WriteResourceFileFromObject_Call{Call: _e.mock.On("WriteResourceFileFromObject", ctx, obj, options)} +} + +func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Run(run func(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions)) *MockRepositoryResources_WriteResourceFileFromObject_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*unstructured.Unstructured), args[2].(WriteOptions)) + }) + return _c +} + +func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Return(_a0 string, _a1 error) *MockRepositoryResources_WriteResourceFileFromObject_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) RunAndReturn(run func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)) *MockRepositoryResources_WriteResourceFileFromObject_Call { + _c.Call.Return(run) + return _c +} + // WriteResourceFromFile provides a mock function with given fields: ctx, path, ref func (_m *MockRepositoryResources) WriteResourceFromFile(ctx context.Context, path string, ref string) (string, schema.GroupVersionKind, error) { ret := _m.Called(ctx, path, ref) diff --git a/pkg/registry/apis/provisioning/resources/resource_lister_mock.go b/pkg/registry/apis/provisioning/resources/resource_lister_mock.go index fae2e39b221..29fb1712f51 100644 --- a/pkg/registry/apis/provisioning/resources/resource_lister_mock.go +++ b/pkg/registry/apis/provisioning/resources/resource_lister_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/resources.go b/pkg/registry/apis/provisioning/resources/resources.go index 0acbbc4c36a..b1f5cef53c2 100644 --- a/pkg/registry/apis/provisioning/resources/resources.go +++ b/pkg/registry/apis/provisioning/resources/resources.go @@ -7,6 +7,7 @@ import ( "fmt" "slices" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -121,7 +122,7 @@ func (r *ResourcesManager) WriteResourceFileFromObject(ctx context.Context, obj err = r.repo.Write(ctx, fileName, options.Ref, body, commitMessage) if err != nil { - return "", fmt.Errorf("failed to write file: %w", err) + return "", fmt.Errorf("failed to write file: %s, %w", fileName, err) } return fileName, nil @@ -206,6 +207,10 @@ func (r *ResourcesManager) RemoveResourceFromFile(ctx context.Context, path stri err = client.Delete(ctx, objName, metav1.DeleteOptions{}) if err != nil { + if apierrors.IsNotFound(err) { + return objName, schema.GroupVersionKind{}, nil // Already deleted or simply non-existing, nothing to do + } + return "", schema.GroupVersionKind{}, fmt.Errorf("failed to delete: %w", err) } diff --git a/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go b/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go index 6f67bc8ffda..80647e0129c 100644 --- a/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package signature diff --git a/pkg/registry/apis/provisioning/resources/signature/signer_mock.go b/pkg/registry/apis/provisioning/resources/signature/signer_mock.go index 93532369d5c..46cd71dab55 100644 --- a/pkg/registry/apis/provisioning/resources/signature/signer_mock.go +++ b/pkg/registry/apis/provisioning/resources/signature/signer_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package signature diff --git a/pkg/registry/apis/provisioning/resources/tree.go b/pkg/registry/apis/provisioning/resources/tree.go index e18ad25f873..427384e8753 100644 --- a/pkg/registry/apis/provisioning/resources/tree.go +++ b/pkg/registry/apis/provisioning/resources/tree.go @@ -3,7 +3,6 @@ package resources import ( "context" "fmt" - "sort" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -95,10 +94,8 @@ func (t *folderTree) Walk(ctx context.Context, fn WalkFunc) error { toWalk = append(toWalk, folder) } - // sort by depth of the paths - sort.Slice(toWalk, func(i, j int) bool { - return safepath.Depth(toWalk[i].Path) < safepath.Depth(toWalk[j].Path) - }) + // sort by depth (shallowest first) + safepath.SortByDepth(toWalk, func(f Folder) string { return f.Path }, true) for _, folder := range toWalk { if err := fn(ctx, folder, t.tree[folder.ID]); err != nil { diff --git a/pkg/registry/apis/provisioning/resources/tree_mock.go b/pkg/registry/apis/provisioning/resources/tree_mock.go index 86b4038a42e..d07ec5459a7 100644 --- a/pkg/registry/apis/provisioning/resources/tree_mock.go +++ b/pkg/registry/apis/provisioning/resources/tree_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/safepath/walk.go b/pkg/registry/apis/provisioning/safepath/walk.go index 048745da188..a048d239e2d 100644 --- a/pkg/registry/apis/provisioning/safepath/walk.go +++ b/pkg/registry/apis/provisioning/safepath/walk.go @@ -3,6 +3,7 @@ package safepath import ( "context" "path" + "sort" "strings" ) @@ -43,3 +44,22 @@ func Split(p string) []string { } return strings.Split(trimmed, "/") } + +// SortByDepth will sort any resource, by its path depth. You must pass in +// a way to get said path. Ties are alphabetical by default. +func SortByDepth[T any](items []T, pathExtractor func(T) string, asc bool) { + sort.Slice(items, func(i, j int) bool { + pathI, pathJ := pathExtractor(items[i]), pathExtractor(items[j]) + depthI, depthJ := Depth(pathI), Depth(pathJ) + + if depthI == depthJ { + // alphabetical by default if depth is the same + return pathI < pathJ + } + + if asc { + return depthI < depthJ + } + return depthI > depthJ + }) +} diff --git a/pkg/registry/apis/provisioning/safepath/walk_test.go b/pkg/registry/apis/provisioning/safepath/walk_test.go index c1694f2af53..9789e8c0ab0 100644 --- a/pkg/registry/apis/provisioning/safepath/walk_test.go +++ b/pkg/registry/apis/provisioning/safepath/walk_test.go @@ -176,3 +176,56 @@ func TestWalkError(t *testing.T) { require.ErrorIs(t, err, expectedErr) } + +func TestSortByDepth(t *testing.T) { + tests := []struct { + name string + asc bool + paths []string + expected []string + }{ + { + name: "ascending sort (shallowest first)", + paths: []string{"a/b/c", "a", "a/b", "d/e/f/g"}, + asc: true, + expected: []string{"a", "a/b", "a/b/c", "d/e/f/g"}, + }, + { + name: "descending sort with alphabetical tie-break", + paths: []string{"a/b/c", "a", "a/b", "d/e/f/g", "x/y/z"}, + asc: false, + expected: []string{"d/e/f/g", "a/b/c", "x/y/z", "a/b", "a"}, + }, + { + name: "paths with empty string", + paths: []string{"a/b/c", "", "a", "a/b"}, + asc: true, + expected: []string{"", "a", "a/b", "a/b/c"}, + }, + { + name: "paths with trailing slashes", + paths: []string{"a/b/", "a/b/c", "b/", "a/", "a"}, + asc: true, + expected: []string{"a", "a/", "b/", "a/b/", "a/b/c"}, + }, + { + name: "single path", + paths: []string{"a/b/c"}, + expected: []string{"a/b/c"}, + }, + { + name: "empty paths", + paths: []string{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + paths := make([]string, len(tt.paths)) + copy(paths, tt.paths) + SortByDepth(paths, func(s string) string { return s }, tt.asc) + assert.Equal(t, tt.expected, paths) + }) + } +} diff --git a/pkg/registry/apis/provisioning/secrets/secret_mock.go b/pkg/registry/apis/provisioning/secrets/secret_mock.go index d182c819e3c..58505ed8ab7 100644 --- a/pkg/registry/apis/provisioning/secrets/secret_mock.go +++ b/pkg/registry/apis/provisioning/secrets/secret_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package secrets diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/blobstore_client_mock.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/blobstore_client_mock.go index cddf0ada660..c1c09944ea9 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/blobstore_client_mock.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/blobstore_client_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest @@ -9,7 +9,7 @@ import ( mock "github.com/stretchr/testify/mock" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) // MockBlobStoreClient is an autogenerated mock type for the BlobStoreClient type @@ -69,7 +69,7 @@ type MockBlobStoreClient_PutBlob_Call struct { // PutBlob is a helper method to define mock.On call // - ctx context.Context -// - in *resource.PutBlobRequest +// - in *resourcepb.PutBlobRequest // - opts ...grpc.CallOption func (_e *MockBlobStoreClient_Expecter) PutBlob(ctx interface{}, in interface{}, opts ...interface{}) *MockBlobStoreClient_PutBlob_Call { return &MockBlobStoreClient_PutBlob_Call{Call: _e.mock.On("PutBlob", diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_commenter.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_commenter.go index 785e552f68d..299a34b3fe5 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_commenter.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_commenter.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_evaluator.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_evaluator.go index 54673cd92d8..9d328127af4 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_evaluator.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_evaluator.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_pullrequest_repo.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_pullrequest_repo.go index 08a6f3dff61..f8c4b9ba5a9 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_pullrequest_repo.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_pullrequest_repo.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/render_mock.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/render_mock.go index f7aa5bb5039..e2a4ed6db94 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/render_mock.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/render_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/webhooks/register.go b/pkg/registry/apis/provisioning/webhooks/register.go index ff73ad525d4..11a3d9a5a35 100644 --- a/pkg/registry/apis/provisioning/webhooks/register.go +++ b/pkg/registry/apis/provisioning/webhooks/register.go @@ -11,9 +11,8 @@ import ( provisioningapis "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/nanogit" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest" @@ -180,42 +179,41 @@ func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Reposit gvr.Resource, r.GetName(), ) - cloneFn := func(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) { - return gogit.Clone(ctx, e.clonedir, r, opts, e.secrets) - } - - apiRepo, err := repository.NewGitHub(ctx, r, e.ghFactory, e.secrets, cloneFn) - if err != nil { - return nil, fmt.Errorf("create github API repository: %w", err) - } logger := logging.FromContext(ctx).With("url", r.Spec.GitHub.URL, "branch", r.Spec.GitHub.Branch, "path", r.Spec.GitHub.Path) - if !e.features.IsEnabledGlobally(featuremgmt.FlagNanoGit) { - logger.Debug("Instantiating Github repository with go-git and Github API") - return NewGithubWebhookRepository(apiRepo, webhookURL, e.secrets), nil - } - - logger.Info("Instantiating Github repository with nanogit") - + logger.Info("Instantiating Github repository with webhooks") ghCfg := r.Spec.GitHub if ghCfg == nil { return nil, fmt.Errorf("github configuration is required for nano git") } - gitCfg := nanogit.RepositoryConfig{ + // Decrypt GitHub token if needed + ghToken := ghCfg.Token + if ghToken == "" && len(ghCfg.EncryptedToken) > 0 { + decrypted, err := e.secrets.Decrypt(ctx, ghCfg.EncryptedToken) + if err != nil { + return nil, fmt.Errorf("decrypt github token: %w", err) + } + ghToken = string(decrypted) + } + + gitCfg := git.RepositoryConfig{ URL: ghCfg.URL, Branch: ghCfg.Branch, Path: ghCfg.Path, - Token: ghCfg.Token, + Token: ghToken, EncryptedToken: ghCfg.EncryptedToken, } - nanogitRepo, err := nanogit.NewGitRepository(ctx, e.secrets, r, gitCfg) + gitRepo, err := git.NewGitRepository(ctx, r, gitCfg) if err != nil { - return nil, fmt.Errorf("error creating nanogit repository: %w", err) + return nil, fmt.Errorf("error creating git repository: %w", err) } - basicRepo := nanogit.NewGithubRepository(apiRepo, nanogitRepo) + basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken) + if err != nil { + return nil, fmt.Errorf("error creating github repository: %w", err) + } return NewGithubWebhookRepository(basicRepo, webhookURL, e.secrets), nil } diff --git a/pkg/registry/apis/provisioning/webhooks/repository.go b/pkg/registry/apis/provisioning/webhooks/repository.go index c9a619e6c7e..451863fceb8 100644 --- a/pkg/registry/apis/provisioning/webhooks/repository.go +++ b/pkg/registry/apis/provisioning/webhooks/repository.go @@ -25,14 +25,14 @@ type WebhookRepository interface { } type GithubWebhookRepository interface { - repository.GithubRepository + pgh.GithubRepository repository.Hooks WebhookRepository } type githubWebhookRepository struct { - repository.GithubRepository + pgh.GithubRepository config *provisioning.Repository owner string repo string @@ -42,7 +42,7 @@ type githubWebhookRepository struct { } func NewGithubWebhookRepository( - basic repository.GithubRepository, + basic pgh.GithubRepository, webhookURL string, secrets secrets.Service, ) GithubWebhookRepository { diff --git a/pkg/registry/apis/provisioning/webhooks/repository_test.go b/pkg/registry/apis/provisioning/webhooks/repository_test.go index 6c5f55bc35f..1b0eb4b1122 100644 --- a/pkg/registry/apis/provisioning/webhooks/repository_test.go +++ b/pkg/registry/apis/provisioning/webhooks/repository_test.go @@ -15,7 +15,7 @@ import ( "testing" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -983,14 +983,14 @@ func TestGitHubRepository_Webhook(t *testing.T) { func TestGitHubRepository_CommentPullRequest(t *testing.T) { tests := []struct { name string - setupMock func(m *pgh.MockClient) + setupMock func(m *github.MockClient) prNumber int comment string expectedError error }{ { name: "successfully comment on pull request", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { m.On("CreatePullRequestComment", mock.Anything, "grafana", "grafana", 123, "Test comment"). Return(nil) }, @@ -1000,7 +1000,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) { }, { name: "error commenting on pull request", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { m.On("CreatePullRequestComment", mock.Anything, "grafana", "grafana", 456, "Error comment"). Return(fmt.Errorf("failed to create comment")) }, @@ -1013,7 +1013,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) + mockGH := github.NewMockClient(t) tt.setupMock(mockGH) // Create repository with mock @@ -1050,7 +1050,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) { func TestGitHubRepository_OnCreate(t *testing.T) { tests := []struct { name string - setupMock func(m *pgh.MockClient) + setupMock func(m *github.MockClient) config *provisioning.Repository webhookURL string expectedHook *provisioning.WebhookStatus @@ -1058,12 +1058,12 @@ func TestGitHubRepository_OnCreate(t *testing.T) { }{ { name: "successfully create webhook", - setupMock: func(m *pgh.MockClient) { - m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(cfg pgh.WebhookConfig) bool { + setupMock: func(m *github.MockClient) { + m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(cfg github.WebhookConfig) bool { return cfg.URL == "https://example.com/webhook" && cfg.ContentType == "json" && cfg.Active == true - })).Return(pgh.WebhookConfig{ + })).Return(github.WebhookConfig{ ID: 123, URL: "https://example.com/webhook", Secret: "test-secret", @@ -1086,7 +1086,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) { }, { name: "no webhook URL", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // No webhook creation expected }, config: &provisioning.Repository{ @@ -1102,9 +1102,9 @@ func TestGitHubRepository_OnCreate(t *testing.T) { }, { name: "error creating webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything). - Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook")) + Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook")) }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -1122,7 +1122,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) + mockGH := github.NewMockClient(t) tt.setupMock(mockGH) // Create repository with mock @@ -1166,7 +1166,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) { func TestGitHubRepository_OnUpdate(t *testing.T) { tests := []struct { name string - setupMock func(m *pgh.MockClient) + setupMock func(m *github.MockClient) config *provisioning.Repository webhookURL string expectedHook *provisioning.WebhookStatus @@ -1174,17 +1174,17 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }{ { name: "successfully update webhook when webhook exists", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock getting the existing webhook m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 123, URL: "https://example.com/webhook", Events: []string{"push"}, }, nil) // Mock editing the webhook - m.On("EditWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool { + m.On("EditWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool { return hook.ID == 123 && hook.URL == "https://example.com/webhook-updated" && slices.Equal(hook.Events, subscribedEvents) })).Return(nil) @@ -1212,18 +1212,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "create webhook when it doesn't exist", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook not found m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound) + Return(github.WebhookConfig{}, github.ErrResourceNotFound) // Mock creating a new webhook - m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool { + m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool { return hook.URL == "https://example.com/webhook" && hook.ContentType == "json" && slices.Equal(hook.Events, subscribedEvents) && hook.Active == true - })).Return(pgh.WebhookConfig{ + })).Return(github.WebhookConfig{ ID: 456, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1252,7 +1252,7 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "no webhook URL provided", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // No mocks needed }, config: &provisioning.Repository{}, @@ -1262,9 +1262,9 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "error getting webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{}, fmt.Errorf("failed to get webhook")) + Return(github.WebhookConfig{}, fmt.Errorf("failed to get webhook")) }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -1285,10 +1285,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "error editing webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock getting the existing webhook m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 123, URL: "https://example.com/webhook", Events: []string{"push"}, @@ -1317,10 +1317,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "create webhook when webhook status is nil", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock creating a new webhook m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 456, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1348,10 +1348,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "create webhook when webhook ID is zero", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock creating a new webhook m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 789, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1382,10 +1382,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "error when creating webhook fails", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook creation failure m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything). - Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook")) + Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook")) }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -1403,18 +1403,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "creates webhook when ErrResourceNotFound", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook not found m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound) + Return(github.WebhookConfig{}, github.ErrResourceNotFound) // Mock creating a new webhook - m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool { + m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool { return hook.URL == "https://example.com/webhook" && hook.ContentType == "json" && slices.Equal(hook.Events, subscribedEvents) && hook.Active == true - })).Return(pgh.WebhookConfig{ + })).Return(github.WebhookConfig{ ID: 456, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1443,18 +1443,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "error on create when not found", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook not found m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound) + Return(github.WebhookConfig{}, github.ErrResourceNotFound) // Mock error when creating a new webhook - m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool { + m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool { return hook.URL == "https://example.com/webhook" && hook.ContentType == "json" && slices.Equal(hook.Events, subscribedEvents) && hook.Active == true - })).Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook")) + })).Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook")) }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -1475,10 +1475,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "no update needed when URL and events match", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock getting the existing webhook with matching URL and events m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 123, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1514,7 +1514,7 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) + mockGH := github.NewMockClient(t) tt.setupMock(mockGH) // Create repository with mock @@ -1563,14 +1563,14 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { func TestGitHubRepository_OnDelete(t *testing.T) { tests := []struct { name string - setupMock func(m *pgh.MockClient) + setupMock func(m *github.MockClient) config *provisioning.Repository webhookURL string expectedError error }{ { name: "successfully delete webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock deleting the webhook m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)). Return(nil) @@ -1593,7 +1593,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }, { name: "no webhook URL provided", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // No mocks needed }, config: &provisioning.Repository{}, @@ -1602,7 +1602,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }, { name: "webhook not found in status", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // No mocks needed }, config: &provisioning.Repository{ @@ -1620,7 +1620,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }, { name: "error deleting webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook deletion failure m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)). Return(fmt.Errorf("failed to delete webhook")) @@ -1646,7 +1646,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) + mockGH := github.NewMockClient(t) tt.setupMock(mockGH) // Create repository with mock diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index edc3be07ac4..237ad94b085 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -179,6 +179,8 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O return } + logEmptyRefids(raw.Queries, b.log) + for i := range req.Requests { req.Requests[i].Headers = ExtractKnownHeaders(httpreq.Header) } @@ -215,6 +217,20 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O }), nil } +func logEmptyRefids(queries []v0alpha1.DataQuery, logger log.Logger) { + emptyCount := 0 + + for _, q := range queries { + if q.RefID == "" { + emptyCount += 1 + } + } + + if emptyCount > 0 { + logger.Info("empty refid found", "empty_count", emptyCount, "query_count", len(queries)) + } +} + func (b *QueryAPIBuilder) execute(ctx context.Context, req parsedRequestInfo, instanceConfig clientapi.InstanceConfigurationSettings) (qdr *backend.QueryDataResponse, err error) { switch len(req.Requests) { case 0: diff --git a/pkg/registry/apis/secret/contracts/outbox_queue.go b/pkg/registry/apis/secret/contracts/outbox_queue.go deleted file mode 100644 index 767273b1254..00000000000 --- a/pkg/registry/apis/secret/contracts/outbox_queue.go +++ /dev/null @@ -1,64 +0,0 @@ -package contracts - -import ( - "context" -) - -type contextRequestIdKey struct{} - -type OutboxMessageType string - -func GetRequestId(ctx context.Context) string { - v := ctx.Value(contextRequestIdKey{}) - requestId, ok := v.(string) - if !ok { - return "" - } - - return requestId -} - -func ContextWithRequestID(ctx context.Context, requestId string) context.Context { - return context.WithValue(ctx, contextRequestIdKey{}, requestId) -} - -const ( - CreateSecretOutboxMessage OutboxMessageType = "create" - UpdateSecretOutboxMessage OutboxMessageType = "update" - DeleteSecretOutboxMessage OutboxMessageType = "delete" -) - -type AppendOutboxMessage struct { - RequestID string - Type OutboxMessageType - Name string - Namespace string - EncryptedSecret string - KeeperName *string - ExternalID *string -} - -type OutboxMessage struct { - RequestID string - Type OutboxMessageType - MessageID int64 - Name string - Namespace string - EncryptedSecret string - KeeperName *string - ExternalID *string - // How many times this message has been received - ReceiveCount int - Created int64 -} - -type OutboxQueue interface { - // Appends a message to the outbox queue - Append(ctx context.Context, message AppendOutboxMessage) (int64, error) - // Receives at most n messages from the outbox queue - ReceiveN(ctx context.Context, n uint) ([]OutboxMessage, error) - // Deletes a message from the outbox queue - Delete(ctx context.Context, messageID int64) error - // Increments the number of times each message has been received by 1. Must be atomic. - IncrementReceiveCount(ctx context.Context, messageIDs []int64) error -} diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go index ca3f66f991d..94bde58c68e 100644 --- a/pkg/registry/apis/secret/contracts/secure_value.go +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -32,10 +32,9 @@ type ReadOpts struct { type SecureValueMetadataStorage interface { Create(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv0alpha1.SecureValue, error) - Update(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) - Delete(ctx context.Context, namespace xkube.Namespace, name string) error List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.SecureValue, error) - SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error - SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, externalID ExternalID) error + SetVersionToActive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error + SetVersionToInactive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error + SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, version int64, externalID ExternalID) error ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*DecryptSecureValue, error) } diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index e667ad4c4be..01b382edbf7 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -5,10 +5,10 @@ import ( "fmt" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/apimachinery/utils" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/tracectx" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -19,8 +19,8 @@ type SecureValueService struct { accessClient claims.AccessClient database contracts.Database secureValueMetadataStorage contracts.SecureValueMetadataStorage - outboxQueue contracts.OutboxQueue - encryptionManager contracts.EncryptionManager + keeperMetadataStorage contracts.KeeperMetadataStorage + keeperService contracts.KeeperService } func ProvideSecureValueService( @@ -28,16 +28,16 @@ func ProvideSecureValueService( accessClient claims.AccessClient, database contracts.Database, secureValueMetadataStorage contracts.SecureValueMetadataStorage, - outboxQueue contracts.OutboxQueue, - encryptionManager contracts.EncryptionManager, + keeperMetadataStorage contracts.KeeperMetadataStorage, + keeperService contracts.KeeperService, ) *SecureValueService { return &SecureValueService{ tracer: tracer, accessClient: accessClient, database: database, secureValueMetadataStorage: secureValueMetadataStorage, - outboxQueue: outboxQueue, - encryptionManager: encryptionManager, + keeperMetadataStorage: keeperMetadataStorage, + keeperService: keeperService, } } @@ -48,43 +48,91 @@ func (s *SecureValueService) Create(ctx context.Context, sv *secretv0alpha1.Secu attribute.String("actor", actorUID), )) defer span.End() + return s.createNewVersion(ctx, sv, actorUID) +} - sv.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhasePending, Message: "Creating secure value"} +func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, bool, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueService.Update", trace.WithAttributes( + attribute.String("name", newSecureValue.GetName()), + attribute.String("namespace", newSecureValue.GetNamespace()), + attribute.String("actor", actorUID), + )) + defer span.End() - var out *secretv0alpha1.SecureValue - - encryptedSecret, err := s.encryptionManager.Encrypt(ctx, sv.Namespace, []byte(sv.Spec.Value.DangerouslyExposeAndConsumeValue())) - if err != nil { - return nil, fmt.Errorf("encrypting secure value secret: %w", err) - } - - // Specifically here so that the spans from the worker are not inside the transaction. - requestID := tracectx.HexEncodeTraceFromContext(ctx) - - if err := s.database.Transaction(ctx, func(ctx context.Context) error { - createdSecureValue, err := s.secureValueMetadataStorage.Create(ctx, sv, actorUID) + if newSecureValue.Spec.Value == "" { + decrypted, err := s.secureValueMetadataStorage.ReadForDecrypt(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name) if err != nil { - return fmt.Errorf("failed to create securevalue: %w", err) - } - out = createdSecureValue - - if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{ - RequestID: requestID, - Type: contracts.CreateSecretOutboxMessage, - Name: sv.Name, - Namespace: sv.Namespace, - EncryptedSecret: string(encryptedSecret), - KeeperName: sv.Spec.Keeper, - }); err != nil { - return fmt.Errorf("failed to append message to create secure value to outbox queue: %w", err) + return nil, false, fmt.Errorf("reading secure value secret: %+w", err) } - return nil - }); err != nil { - return out, err + // TODO: does this need to be for update? + keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, newSecureValue.Namespace, newSecureValue.Spec.Keeper, contracts.ReadOpts{ForUpdate: true}) + if err != nil { + return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Spec.Keeper, err) + } + + keeper, err := s.keeperService.KeeperForConfig(keeperCfg) + if err != nil { + return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Spec.Keeper, err) + } + logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type()) + + secret, err := keeper.Expose(ctx, keeperCfg, newSecureValue.Namespace, contracts.ExternalID(decrypted.ExternalID)) + if err != nil { + return nil, false, fmt.Errorf("reading secret value from keeper: %w", err) + } + + newSecureValue.Spec.Value = secret } - return out, nil + const updateIsSync = true + createdSv, err := s.createNewVersion(ctx, newSecureValue, actorUID) + return createdSv, updateIsSync, err +} + +func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) { + createdSv, err := s.secureValueMetadataStorage.Create(ctx, sv, actorUID) + if err != nil { + return nil, fmt.Errorf("creating secure value: %w", err) + } + createdSv.Status = secretv0alpha1.SecureValueStatus{ + Version: createdSv.Status.Version, + } + + // TODO: does this need to be for update? + keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, sv.Namespace, sv.Spec.Keeper, contracts.ReadOpts{ForUpdate: true}) + if err != nil { + return nil, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Spec.Keeper, err) + } + + keeper, err := s.keeperService.KeeperForConfig(keeperCfg) + if err != nil { + return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Spec.Keeper, err) + } + logging.FromContext(ctx).Debug("retrieved keeper", "namespace", sv.Namespace, "keeperName", sv.Spec.Keeper, "type", keeperCfg.Type()) + + // TODO: can we stop using external id? + // TODO: store uses only the namespace and returns and id. It could be a kv instead. + // TODO: check that the encrypted store works with multiple versions + externalID, err := keeper.Store(ctx, keeperCfg, sv.Namespace, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) + if err != nil { + return nil, fmt.Errorf("storing secure value in keeper: %w", err) + } + createdSv.Status.ExternalID = string(externalID) + + if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(sv.Namespace), sv.Name, createdSv.Status.Version, externalID); err != nil { + return nil, fmt.Errorf("setting secure value external id: %w", err) + } + + if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(sv.Namespace), sv.Name, createdSv.Status.Version); err != nil { + return nil, fmt.Errorf("marking secure value version as active: %w", err) + } + + // In a single query: + // TODO: set external id + // TODO: set to active + + return createdSv, nil } func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) { @@ -139,84 +187,6 @@ func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace }, nil } -func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, bool, error) { - ctx, span := s.tracer.Start(ctx, "SecureValueService.Create", trace.WithAttributes( - attribute.String("name", newSecureValue.GetName()), - attribute.String("namespace", newSecureValue.GetNamespace()), - attribute.String("actor", actorUID), - )) - defer span.End() - - // True when the effects of an update can be seen immediately. - // Never true in this case since updating a secure value is async. - const updateIsSync = false - - var ( - out *secretv0alpha1.SecureValue - encryptedSecret string - ) - - if newSecureValue.Spec.Value != "" { - buffer, err := s.encryptionManager.Encrypt(ctx, newSecureValue.Namespace, []byte(newSecureValue.Spec.Value.DangerouslyExposeAndConsumeValue())) - if err != nil { - return nil, false, fmt.Errorf("encrypting secure value secret: %w", err) - } - encryptedSecret = string(buffer) - } - - // Especifically here so that the spans from the worker are not inside the transaction. - requestID := tracectx.HexEncodeTraceFromContext(ctx) - - if err := s.database.Transaction(ctx, func(ctx context.Context) error { - sv, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{ForUpdate: true}) - if err != nil { - return fmt.Errorf("fetching secure value: %+w", err) - } - - if sv.Status.Phase == secretv0alpha1.SecureValuePhasePending { - return contracts.ErrSecureValueOperationInProgress - } - - // Succeed immediately if the value is not going to be updated - if encryptedSecret == "" { - newSecureValue.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded} - } else { - newSecureValue.Status = secretv0alpha1.SecureValueStatus{ - Message: "Updating secure value", - Phase: secretv0alpha1.SecureValuePhasePending, - } - } - - // Current implementation replaces everything passed in the spec, so it is not a PATCH. Do we want/need to support that? - updatedSecureValue, err := s.secureValueMetadataStorage.Update(ctx, newSecureValue, actorUID) - if err != nil { - return fmt.Errorf("failed to update secure value: %w", err) - } - out = updatedSecureValue - - // Only the value needs to be updated asynchronously by the outbox worker - if encryptedSecret != "" { - if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{ - RequestID: requestID, - Type: contracts.UpdateSecretOutboxMessage, - Name: newSecureValue.Name, - Namespace: newSecureValue.Namespace, - EncryptedSecret: encryptedSecret, - KeeperName: newSecureValue.Spec.Keeper, - ExternalID: &updatedSecureValue.Status.ExternalID, - }); err != nil { - return fmt.Errorf("failed to append message to update secure value to outbox queue: %w", err) - } - } - - return nil - }); err != nil { - return out, updateIsSync, err - } - - return out, updateIsSync, nil -} - func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) { ctx, span := s.tracer.Start(ctx, "SecureValueService.Delete", trace.WithAttributes( attribute.String("name", name), @@ -224,45 +194,14 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa )) defer span.End() - // Set inside of the transaction callback - var out *secretv0alpha1.SecureValue - - // Especifically here so that the spans from the worker are not inside the transaction. - requestID := tracectx.HexEncodeTraceFromContext(ctx) - - if err := s.database.Transaction(ctx, func(ctx context.Context) error { - sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true}) - if err != nil { - return fmt.Errorf("fetching secure value: %+w", err) - } - - if sv.Status.Phase == secretv0alpha1.SecureValuePhasePending { - return contracts.ErrSecureValueOperationInProgress - } - - sv.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhasePending, Message: "Deleting secure value"} - - if err := s.secureValueMetadataStorage.SetStatus(ctx, namespace, name, sv.Status); err != nil { - return fmt.Errorf("setting secure value status phase: %+w", err) - } - - if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{ - RequestID: requestID, - Type: contracts.DeleteSecretOutboxMessage, - Name: name, - Namespace: namespace.String(), - KeeperName: sv.Spec.Keeper, - ExternalID: &sv.Status.ExternalID, - }); err != nil { - return fmt.Errorf("appending delete secure value message to outbox queue: %+w", err) - } - - out = sv - - return nil - }); err != nil { - return out, err + sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true}) + if err != nil { + return nil, fmt.Errorf("fetching secure value: %+w", err) } - return out, nil + if err := s.secureValueMetadataStorage.SetVersionToInactive(ctx, namespace, name, sv.Status.Version); err != nil { + return nil, fmt.Errorf("setting secure value version to inactive: %+w", err) + } + + return sv, nil } diff --git a/pkg/registry/apis/secret/service/secure_value_test.go b/pkg/registry/apis/secret/service/secure_value_test.go new file mode 100644 index 00000000000..865138d097b --- /dev/null +++ b/pkg/registry/apis/secret/service/secure_value_test.go @@ -0,0 +1,94 @@ +package service_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/stretchr/testify/require" +) + +func TestCrud(t *testing.T) { + t.Parallel() + + t.Run("creating a secure value creates new versions", func(t *testing.T) { + t.Parallel() + sut := testutils.Setup(t) + + sv1, err := sut.CreateSv(t.Context()) + require.NoError(t, err) + + // Create the same secure value twice + input := sv1.DeepCopy() + input.Spec.Description = "d2" + input.Spec.Value = v0alpha1.NewExposedSecureValue("v2") + + sv2, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(input)) + require.NoError(t, err) + require.True(t, sv2.Status.Version > sv1.Status.Version) + + // Read the secure value + sv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv2.Namespace), sv2.Name) + require.NoError(t, err) + + // It should be the latest version + require.Equal(t, sv2.Namespace, sv.Namespace) + require.Equal(t, sv2.Name, sv.Name) + require.Equal(t, "d2", sv.Spec.Description) + require.Equal(t, sv2.Status.Version, sv.Status.Version) + }) + + t.Run("updating a secure value creates new versions", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + // Create a secure value + sv1, err := sut.CreateSv(t.Context()) + require.NoError(t, err) + + ns := sv1.Namespace + name := sv1.Name + + // Update the secure value + input := sv1.DeepCopy() + input.Spec.Description = "d2" + sv2, err := sut.UpdateSv(t.Context(), input) + require.NoError(t, err) + + // Read the secure value + sv3, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(ns), name) + require.NoError(t, err) + + // Nothing has changed except for the updated field. + require.Equal(t, ns, sv2.Namespace) + require.Equal(t, name, sv2.Name) + require.True(t, sv2.Status.Version > sv1.Status.Version) + require.Equal(t, "d2", sv2.Spec.Description) + + require.Equal(t, ns, sv3.Namespace) + require.Equal(t, name, sv3.Name) + require.Equal(t, sv2.Status.Version, sv3.Status.Version) + require.Equal(t, "d2", sv3.Spec.Description) + }) + + t.Run("deleting secure values", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + sv1, err := sut.CreateSv(t.Context()) + require.NoError(t, err) + + sv2, err := sut.DeleteSv(t.Context(), sv1.Namespace, sv1.Name) + require.NoError(t, err) + + require.Equal(t, sv1.Namespace, sv2.Namespace) + require.Equal(t, sv1.Name, sv2.Name) + + _, err = sut.SecureValueMetadataStorage.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name, contracts.ReadOpts{}) + require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) + }) +} diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 40e571aca63..281304320c8 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -3,7 +3,6 @@ package testutils import ( "context" "testing" - "time" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" @@ -12,11 +11,11 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" "github.com/grafana/grafana/pkg/registry/apis/secret/service" - "github.com/grafana/grafana/pkg/registry/apis/secret/worker" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -29,35 +28,28 @@ import ( "github.com/stretchr/testify/require" ) -type setupConfig struct { - workerCfg worker.Config - keeperService contracts.KeeperService +type SetupConfig struct { + KeeperService contracts.KeeperService + AllowList map[string]struct{} } -func defaultSetupCfg() setupConfig { - return setupConfig{ - workerCfg: worker.Config{ - BatchSize: 10, - ReceiveTimeout: 1 * time.Second, - PollingInterval: time.Millisecond, - MaxMessageProcessingAttempts: 5, - }, +func defaultSetupCfg() SetupConfig { + return SetupConfig{} +} + +func WithKeeperService(keeperService contracts.KeeperService) func(*SetupConfig) { + return func(setupCfg *SetupConfig) { + setupCfg.KeeperService = keeperService } } -func WithWorkerConfig(cfg worker.Config) func(*setupConfig) { - return func(setupCfg *setupConfig) { - setupCfg.workerCfg = cfg +func WithMutateCfg(f func(*SetupConfig)) func(*SetupConfig) { + return func(cfg *SetupConfig) { + f(cfg) } } -func WithKeeperService(keeperService contracts.KeeperService) func(*setupConfig) { - return func(setupCfg *setupConfig) { - setupCfg.keeperService = keeperService - } -} - -func Setup(t *testing.T, opts ...func(*setupConfig)) Sut { +func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { setupCfg := defaultSetupCfg() for _, opt := range opts { opt(&setupCfg) @@ -68,8 +60,6 @@ func Setup(t *testing.T, opts ...func(*setupConfig)) Sut { database := database.ProvideDatabase(testDB, tracer) - outboxQueue := metadata.ProvideOutboxQueue(database, tracer, nil) - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(database, tracer, features, nil) @@ -111,34 +101,24 @@ func Setup(t *testing.T, opts ...func(*setupConfig)) Sut { var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper) - if setupCfg.keeperService != nil { - keeperService = setupCfg.keeperService + if setupCfg.KeeperService != nil { + keeperService = setupCfg.KeeperService } - secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, outboxQueue, encryptionManager) + secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, keeperMetadataStorage, keeperService) - worker, err := worker.NewWorker( - setupCfg.workerCfg, - tracer, - database, - outboxQueue, - secureValueMetadataStorage, - keeperMetadataStorage, - keeperService, - encryptionManager, - features, - nil, // metrics - ) + decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, setupCfg.AllowList) + + decryptStorage, err := metadata.ProvideDecryptStorage(features, tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil) require.NoError(t, err) - return Sut{Worker: worker, SecureValueService: secureValueService, SecureValueMetadataStorage: secureValueMetadataStorage, OutboxQueue: outboxQueue, Database: database} + return Sut{SecureValueService: secureValueService, SecureValueMetadataStorage: secureValueMetadataStorage, Database: database, DecryptStorage: decryptStorage} } type Sut struct { - Worker *worker.Worker SecureValueService *service.SecureValueService SecureValueMetadataStorage contracts.SecureValueMetadataStorage - OutboxQueue contracts.OutboxQueue + DecryptStorage contracts.DecryptStorage Database *database.Database } @@ -163,9 +143,7 @@ func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*sec Description: "desc1", Value: secretv0alpha1.NewExposedSecureValue("v1"), }, - Status: secretv0alpha1.SecureValueStatus{ - Phase: secretv0alpha1.SecureValuePhasePending, - }, + Status: secretv0alpha1.SecureValueStatus{}, }, } for _, opt := range opts { diff --git a/pkg/registry/apis/secret/worker/metrics.go b/pkg/registry/apis/secret/worker/metrics.go deleted file mode 100644 index a6f690f7163..00000000000 --- a/pkg/registry/apis/secret/worker/metrics.go +++ /dev/null @@ -1,44 +0,0 @@ -package worker - -import ( - "github.com/prometheus/client_golang/prometheus" -) - -const ( - namespace = "grafana_secrets_manager" - subsystem = "outbox_worker" -) - -// OutboxMetrics is a struct that contains all the metrics for an implementation of the secrets service. -type OutboxMetrics struct { - OutboxMessageProcessingDuration *prometheus.HistogramVec -} - -func newOutboxMetrics() *OutboxMetrics { - return &OutboxMetrics{ - OutboxMessageProcessingDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "message_processing_duration_seconds", - Help: "Duration of outbox message processing", - Buckets: prometheus.DefBuckets, - }, []string{"message_type", "keeper_type"}), - } -} - -// NewOutboxMetrics creates a new SecretsMetrics struct containing registered metrics -func NewOutboxMetrics(reg prometheus.Registerer) *OutboxMetrics { - m := newOutboxMetrics() - - if reg != nil { - reg.MustRegister( - m.OutboxMessageProcessingDuration, - ) - } - - return m -} - -func NewTestMetrics() *OutboxMetrics { - return newOutboxMetrics() -} diff --git a/pkg/registry/apis/secret/worker/worker.go b/pkg/registry/apis/secret/worker/worker.go deleted file mode 100644 index 35474fdae47..00000000000 --- a/pkg/registry/apis/secret/worker/worker.go +++ /dev/null @@ -1,274 +0,0 @@ -package worker - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/grafana/grafana-app-sdk/logging" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/tracectx" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/prometheus/client_golang/prometheus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// Consumes and processes messages from the secure value outbox queue -type Worker struct { - config Config - tracer trace.Tracer - database contracts.Database - outboxQueue contracts.OutboxQueue - secureValueMetadataStorage contracts.SecureValueMetadataStorage - keeperMetadataStorage contracts.KeeperMetadataStorage - keeperService contracts.KeeperService - encryptionManager contracts.EncryptionManager - metrics *OutboxMetrics - enabled bool -} - -// DefaultConfig for the secure value outbox worker. -var DefaultConfig = Config{ - BatchSize: 20, - ReceiveTimeout: 5 * time.Second, - PollingInterval: 100 * time.Millisecond, - MaxMessageProcessingAttempts: 10, -} - -// ProvideWorkerConfig used for wire. -func ProvideWorkerConfig() Config { - return DefaultConfig -} - -type Config struct { - // The max number of messages to fetch from the outbox queue in a batch - BatchSize uint - // How long to wait for a request to fetch messages from the outbox queue - ReceiveTimeout time.Duration - // How often to poll the outbox queue for new messages - PollingInterval time.Duration - // How many tries to try to process a message before marking the operation as failed - MaxMessageProcessingAttempts uint -} - -func NewWorker( - config Config, - tracer trace.Tracer, - database contracts.Database, - outboxQueue contracts.OutboxQueue, - secureValueMetadataStorage contracts.SecureValueMetadataStorage, - keeperMetadataStorage contracts.KeeperMetadataStorage, - keeperService contracts.KeeperService, - encryptionManager contracts.EncryptionManager, - features featuremgmt.FeatureToggles, - reg prometheus.Registerer, -) (*Worker, error) { - if config.BatchSize == 0 { - return nil, fmt.Errorf("config.BatchSize is required") - } - if config.ReceiveTimeout == 0 { - return nil, fmt.Errorf("config.ReceiveTimeout is required") - } - if config.PollingInterval == 0 { - return nil, fmt.Errorf("config.PollingInterval is required") - } - if config.MaxMessageProcessingAttempts == 0 { - return nil, fmt.Errorf("config.MaxMessageProcessingAttempts is required") - } - - // Require both features to be enabled for the worker to run. - enabled := features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) && features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) - - return &Worker{ - config: config, - tracer: tracer, - database: database, - outboxQueue: outboxQueue, - secureValueMetadataStorage: secureValueMetadataStorage, - keeperMetadataStorage: keeperMetadataStorage, - keeperService: keeperService, - encryptionManager: encryptionManager, - metrics: NewOutboxMetrics(reg), - enabled: enabled, - }, nil -} - -// Ensure that Worker implements the BackgroundService interface, so we can start it as a background service. -var _ registry.BackgroundService = (*Worker)(nil) - -// Run is the main method to drive the worker -func (w *Worker) Run(ctx context.Context) error { - if !w.enabled { - return nil - } - - logging.FromContext(ctx).Debug("starting worker control loop") - - t := time.NewTicker(w.config.PollingInterval) - defer t.Stop() - - for { - select { - // If the context was canceled - case <-ctx.Done(): - // return the reason it was canceled - return ctx.Err() - - // Otherwise try to receive messages - case <-t.C: - if ctx.Err() != nil { - return ctx.Err() - } - - if err := w.ReceiveAndProcessMessages(ctx); err != nil { - logging.FromContext(ctx).Error("receiving outbox messages", "err", err.Error()) - } - } - } -} - -// TODO: don't rollback every message when a single error happens -func (w *Worker) ReceiveAndProcessMessages(ctx context.Context) error { - messageIDs := make([]int64, 0) - - txErr := w.database.Transaction(ctx, func(ctx context.Context) error { - timeoutCtx, cancel := context.WithTimeout(ctx, w.config.ReceiveTimeout) - messages, err := w.outboxQueue.ReceiveN(timeoutCtx, w.config.BatchSize) - cancel() - if err != nil { - return err - } - - for _, message := range messages { - messageIDs = append(messageIDs, message.MessageID) - if err := w.processMessage(ctx, message); err != nil { - return fmt.Errorf("processing message: %+v %w", message, err) - } - } - return nil - }) - - // This call is made outside the transaction to make sure the receive count is updated on rollbacks. - incrementErr := w.outboxQueue.IncrementReceiveCount(ctx, messageIDs) - if incrementErr != nil { - incrementErr = fmt.Errorf("incrementing receive count for outbox message: %w", incrementErr) - } - - return errors.Join(txErr, incrementErr) -} - -func (w *Worker) processMessage(ctx context.Context, message contracts.OutboxMessage) error { - start := time.Now() - keeperType := "unknown" - defer func() { - w.metrics.OutboxMessageProcessingDuration.WithLabelValues(string(message.Type), keeperType).Observe(time.Since(start).Seconds()) - }() - logging.FromContext(ctx).Debug("processing message", "type", message.Type, "name", message.Name, "namespace", message.Namespace, "receiveCount", message.ReceiveCount) - - opts := []trace.SpanStartOption{} - // If there's no request ID in the message, start a new root span and log an error. - ctx, err := tracectx.HexDecodeTraceIntoContext(ctx, message.RequestID) - if err != nil { - opts = append(opts, trace.WithNewRoot()) - logging.FromContext(ctx).Error("decoding trace context from message", "err", err.Error(), "message.requestID", message.RequestID) - } - - opts = append(opts, trace.WithAttributes( - attribute.String("message.requestID", message.RequestID), - attribute.Int64("message.id", message.MessageID), - attribute.String("message.type", string(message.Type)), - attribute.String("message.namespace", message.Namespace), - attribute.String("message.secureValue.name", message.Name), - attribute.Int("message.receive.count", message.ReceiveCount), - )) - - ctx, span := w.tracer.Start(ctx, "Worker.ProcessMessage", opts...) - defer span.End() - - if message.ReceiveCount >= int(w.config.MaxMessageProcessingAttempts) { - if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseFailed, Message: fmt.Sprintf("Reached max number of attempts to complete operation: %s", message.Type)}); err != nil { - return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v", message) - } - if err := w.outboxQueue.Delete(ctx, message.MessageID); err != nil { - return fmt.Errorf("deleting message from outbox queue: %w", err) - } - return nil - } - - keeperCfg, err := w.keeperMetadataStorage.GetKeeperConfig(ctx, message.Namespace, message.KeeperName, contracts.ReadOpts{ForUpdate: true}) - if err != nil { - return fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", message.Namespace, message.KeeperName, err) - } - keeperType = string(keeperCfg.Type()) - - keeper, err := w.keeperService.KeeperForConfig(keeperCfg) - if err != nil { - return fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", message.Namespace, message.KeeperName, err) - } - logging.FromContext(ctx).Debug("retrieved keeper", "namespace", message.Namespace, "keeperName", message.KeeperName, "type", keeperCfg.Type()) - - switch message.Type { - case contracts.CreateSecretOutboxMessage: - rawSecret, err := w.encryptionManager.Decrypt(ctx, message.Namespace, []byte(message.EncryptedSecret)) - if err != nil { - return fmt.Errorf("decrypting secure value secret: %w", err) - } - - externalID, err := keeper.Store(ctx, keeperCfg, message.Namespace, string(rawSecret)) - if err != nil { - return fmt.Errorf("storing secret: message=%+v %w", message, err) - } - - if err := w.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(message.Namespace), message.Name, externalID); err != nil { - return fmt.Errorf("setting secret metadata externalID: externalID=%+v message=%+v %w", externalID, message, err) - } - - // Setting the status to Succeeded must be the last action - // since it acts as a fence to clients. - if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded}); err != nil { - return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v %w", message, err) - } - - case contracts.UpdateSecretOutboxMessage: - rawSecret, err := w.encryptionManager.Decrypt(ctx, message.Namespace, []byte(message.EncryptedSecret)) - if err != nil { - return fmt.Errorf("decrypting secure value secret: %w", err) - } - - if err := keeper.Update(ctx, keeperCfg, message.Namespace, contracts.ExternalID(*message.ExternalID), string(rawSecret)); err != nil { - return fmt.Errorf("calling keeper to update secret: %w", err) - } - - // Setting the status to Succeeded must be the last action - // since it acts as a fence to clients. - if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded}); err != nil { - return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v", message) - } - - case contracts.DeleteSecretOutboxMessage: - if err := keeper.Delete(ctx, keeperCfg, message.Namespace, contracts.ExternalID(*message.ExternalID)); err != nil { - return fmt.Errorf("calling keeper to delete secret: %w", err) - } - if err := w.secureValueMetadataStorage.Delete(ctx, xkube.Namespace(message.Namespace), message.Name); err != nil { - return fmt.Errorf("deleting secure value metadata: %+w", err) - } - - default: - return fmt.Errorf("unhandled message type: %s", message.Type) - } - - // Delete the message from the queue after completing all operations because - // if the message is deleted first, the response may be lost, - // resulting in an error, but since the message was actually deleted - // the worker would never retry. - if err := w.outboxQueue.Delete(ctx, message.MessageID); err != nil { - return fmt.Errorf("deleting message from outbox queue: %w", err) - } - - return nil -} diff --git a/pkg/registry/apis/secret/worker/worker_test.go b/pkg/registry/apis/secret/worker/worker_test.go deleted file mode 100644 index 0d20a668e52..00000000000 --- a/pkg/registry/apis/secret/worker/worker_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package worker_test - -import ( - "context" - "fmt" - "testing" - "time" - - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" - "github.com/grafana/grafana/pkg/registry/apis/secret/worker" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" - "github.com/stretchr/testify/require" -) - -type fakeKeeperService struct { - keeperForConfigFunc func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) -} - -func newFakeKeeperService(keeperForConfigFunc func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error)) *fakeKeeperService { - return &fakeKeeperService{keeperForConfigFunc: keeperForConfigFunc} -} - -func (s *fakeKeeperService) KeeperForConfig(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) { - return s.keeperForConfigFunc(cfg) -} - -func TestProcessMessage(t *testing.T) { - t.Parallel() - - t.Run("secure value metadata status is set to Failed when processing a message fails too many times", func(t *testing.T) { - t.Parallel() - - // Given a worker that will attempt to process a message N times - workerCfg := worker.Config{ - BatchSize: 10, - ReceiveTimeout: 1 * time.Second, - PollingInterval: time.Millisecond, - MaxMessageProcessingAttempts: 2, - } - - // And an error that keeps happening - keeperService := newFakeKeeperService(func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) { - return nil, fmt.Errorf("oops") - }) - - sut := testutils.Setup(t, testutils.WithWorkerConfig(workerCfg), testutils.WithKeeperService(keeperService)) - ctx := context.Background() - - // Queue a create secure value operation - sv, err := sut.CreateSv(ctx) - require.NoError(t, err) - - for range workerCfg.MaxMessageProcessingAttempts + 1 { - // The secure value status should be Pending while the worker is trying to process the message - sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{}) - require.NoError(t, err) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase) - - // Worker tries to process messages - _ = sut.Worker.ReceiveAndProcessMessages(ctx) - } - - // After the worker fails to process a message too many times, - // the secure value status is changed to Failed - sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{}) - require.NoError(t, err) - require.Equal(t, secretv0alpha1.SecureValuePhaseFailed, sv.Status.Phase) - - messages, err := sut.OutboxQueue.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Empty(t, messages) - }) - - t.Run("create sv: secure value metadata status is set to Succeeded when message is processed successfully", func(t *testing.T) { - t.Parallel() - - sut := testutils.Setup(t) - ctx := context.Background() - - // Queue a create secure value operation - sv, err := sut.CreateSv(ctx) - require.NoError(t, err) - - // Worker receives and processes the message - require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx)) - - // and sets the secure value status to Succeeded - sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{}) - require.NoError(t, err) - require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase) - - messages, err := sut.OutboxQueue.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Empty(t, messages) - }) - - t.Run("update sv: secure value metadata status is set to Succeeded when message is processed successfully", func(t *testing.T) { - t.Parallel() - - sut := testutils.Setup(t) - ctx := context.Background() - - // Queue a create secure value operation - sv, err := sut.CreateSv(ctx) - require.NoError(t, err) - - // Worker receives and processes the message - require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx)) - - // and sets the secure value status to Succeeded - sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{}) - require.NoError(t, err) - require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase) - - sv.Spec.Description = "desc2" - sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2") - - // Queue an update operation - sv, err = sut.UpdateSv(ctx, sv) - require.NoError(t, err) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase) - - // Worker receives and processes the message - require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx)) - updatedSv, err := sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{}) - require.NoError(t, err) - require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, updatedSv.Status.Phase) - require.Equal(t, sv.Spec.Description, updatedSv.Spec.Description) - - messages, err := sut.OutboxQueue.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Empty(t, messages) - }) - - t.Run("delete sv: secure value metadata is deleted", func(t *testing.T) { - t.Parallel() - - sut := testutils.Setup(t) - ctx := context.Background() - - // Queue a create secure value operation - sv, err := sut.CreateSv(ctx) - require.NoError(t, err) - - // Worker receives and processes the message - require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx)) - - // and sets the secure value status to Succeeded - sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{}) - require.NoError(t, err) - require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase) - - // Queue a delete operation - updatedSv, err := sut.DeleteSv(ctx, sv.Namespace, sv.Name) - require.NoError(t, err) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, updatedSv.Status.Phase) - - // Worker receives and processes the message - require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx)) - - // The secure value has been deleted - _, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{}) - require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) - - messages, err := sut.OutboxQueue.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Empty(t, messages) - }) - - t.Run("when creating a secure value, the secret is encrypted before it is added to the outbox queue", func(t *testing.T) { - t.Parallel() - - sut := testutils.Setup(t) - ctx := context.Background() - - // Queue a create secure value operation - var secret string - _, err := sut.CreateSv(ctx, func(cfg *testutils.CreateSvConfig) { - secret = string(cfg.Sv.Spec.Value) - }) - require.NoError(t, err) - - messages, err := sut.OutboxQueue.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Equal(t, 1, len(messages)) - - encryptedSecret := messages[0].EncryptedSecret - require.NotEmpty(t, secret) - require.NotEmpty(t, encryptedSecret) - require.NotEqual(t, secret, encryptedSecret) - }) - - t.Run("when updating a secure value, the secret is encrypted before it is added to the outbox queue", func(t *testing.T) { - t.Parallel() - - sut := testutils.Setup(t) - ctx := context.Background() - - // Queue a create secure value operation - sv, err := sut.CreateSv(ctx) - require.NoError(t, err) - sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2") - - require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx)) - - newValue := "v2" - sv.Spec.Value = secretv0alpha1.NewExposedSecureValue(newValue) - - // Queue an update secure value operation - _, err = sut.UpdateSv(ctx, sv) - require.NoError(t, err) - - messages, err := sut.OutboxQueue.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Equal(t, 1, len(messages)) - - encryptedSecret := messages[0].EncryptedSecret - require.NotEmpty(t, encryptedSecret) - require.NotEqual(t, newValue, encryptedSecret) - }) - - t.Run("when deleting a secure value, no value is added to the outbox message", func(t *testing.T) { - t.Parallel() - - sut := testutils.Setup(t) - ctx := context.Background() - - // Queue a create secure value operation - sv, err := sut.CreateSv(ctx) - require.NoError(t, err) - sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2") - - require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx)) - - // Queue a delete secure value operation - _, err = sut.DeleteSv(ctx, sv.Namespace, sv.Name) - require.NoError(t, err) - - messages, err := sut.OutboxQueue.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Equal(t, 1, len(messages)) - require.Empty(t, messages[0].EncryptedSecret) - }) -} diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index d8a9010bfd2..f6ae88f9dd1 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/registry" apiregistry "github.com/grafana/grafana/pkg/registry/apis" - secretworker "github.com/grafana/grafana/pkg/registry/apis/secret/worker" appregistry "github.com/grafana/grafana/pkg/registry/apps" "github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite" "github.com/grafana/grafana/pkg/services/anonymous/anonimpl" @@ -71,7 +70,6 @@ func ProvideBackgroundServiceRegistry( appRegistry *appregistry.Service, pluginDashboardUpdater *plugindashboardsservice.DashboardUpdater, dashboardServiceImpl *service.DashboardServiceImpl, - secretManagerWorker *secretworker.Worker, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, _ serviceaccounts.Service, @@ -119,7 +117,6 @@ func ProvideBackgroundServiceRegistry( appRegistry, pluginDashboardUpdater, dashboardServiceImpl, - secretManagerWorker, ) } diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index 979e993eb7a..bca9bf21c6b 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/modules" "github.com/grafana/grafana/pkg/services/authz" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -39,6 +40,7 @@ func NewModule(opts Options, indexMetrics *resource.BleveIndexMetrics, reg prometheus.Registerer, promGatherer prometheus.Gatherer, + tracer tracing.Tracer, // Ensures tracing is initialized license licensing.Licensing, ) (*ModuleServer, error) { s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license) diff --git a/pkg/server/search_server_distributor_test.go b/pkg/server/search_server_distributor_test.go index 3062b1efe8d..32dddffcec8 100644 --- a/pkg/server/search_server_distributor_test.go +++ b/pkg/server/search_server_distributor_test.go @@ -15,6 +15,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/modules" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" @@ -321,7 +322,9 @@ func initModuleServerForTest( opts Options, apiOpts api.ServerOptions, ) testModuleServer { - ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, nil) + tracer := tracing.InitializeTracerForTest() + + ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, nil) require.NoError(t, err) conn, err := grpc.NewClient(cfg.GRPCServer.Address, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index efb82d04bc0..2004a813b11 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -46,7 +46,6 @@ import ( gsmEncryption "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" secretsecurevalueservice "github.com/grafana/grafana/pkg/registry/apis/secret/service" - secretworker "github.com/grafana/grafana/pkg/registry/apis/secret/worker" appregistry "github.com/grafana/grafana/pkg/registry/apps" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -431,15 +430,12 @@ var wireBasicSet = wire.NewSet( secretdecrypt.ProvideDecryptAllowList, secretencryption.ProvideDataKeyStorage, secretencryption.ProvideEncryptedValueStorage, - secretmetadata.ProvideOutboxQueue, secretsecurevalueservice.ProvideSecureValueService, secretmigrator.NewWithEngine, secretdatabase.ProvideDatabase, wire.Bind(new(secretcontracts.Database), new(*secretdatabase.Database)), encryptionManager.ProvideEncryptionManager, gsmEncryption.ProvideThirdPartyProviderMap, - secretworker.ProvideWorkerConfig, - secretworker.NewWorker, // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 166c20a19dc..a1c00aada78 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -61,11 +61,9 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" - encryption2 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + encryption3 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" manager4 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" - "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper" service11 "github.com/grafana/grafana/pkg/registry/apis/secret/service" - "github.com/grafana/grafana/pkg/registry/apis/secret/worker" "github.com/grafana/grafana/pkg/registry/apis/userstorage" "github.com/grafana/grafana/pkg/registry/apps" advisor2 "github.com/grafana/grafana/pkg/registry/apps/advisor" @@ -114,7 +112,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/guardian" service7 "github.com/grafana/grafana/pkg/services/datasources/service" - encryption3 "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption/provider" service2 "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/services/extsvcauth" @@ -236,7 +234,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" database5 "github.com/grafana/grafana/pkg/storage/secret/database" - "github.com/grafana/grafana/pkg/storage/secret/encryption" + encryption2 "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/grafana/grafana/pkg/storage/secret/metadata" migrator2 "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" @@ -650,7 +648,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer) + idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer, tracer) verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationService, idimplService) httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service12, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service13, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationService, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service11, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokenService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) if err != nil { @@ -705,38 +703,6 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser } importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service12, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service12, importDashboardService, service11, pluginService, dashboardService) - config := worker.ProvideWorkerConfig() - databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) - outboxQueue := metadata.ProvideOutboxQueue(databaseDatabase, tracer, registerer) - secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, featureToggles, registerer) - if err != nil { - return nil, err - } - keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(databaseDatabase, tracer, featureToggles, registerer) - if err != nil { - return nil, err - } - encryptedValueStorage, err := encryption.ProvideEncryptedValueStorage(databaseDatabase, tracer, featureToggles) - if err != nil { - return nil, err - } - dataKeyStorage, err := encryption.ProvideDataKeyStorage(databaseDatabase, tracer, featureToggles, registerer) - if err != nil { - return nil, err - } - providerMap := encryption2.ProvideThirdPartyProviderMap() - encryptionManager, err := manager4.ProvideEncryptionManager(tracer, dataKeyStorage, cfg, usageStats, providerMap) - if err != nil { - return nil, err - } - ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) - if err != nil { - return nil, err - } - workerWorker, err := worker.NewWorker(config, tracer, databaseDatabase, outboxQueue, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService, encryptionManager, featureToggles, registerer) - if err != nil { - return nil, err - } sanitizerProvider := sanitizer.ProvideService(renderingService) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { @@ -803,7 +769,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser } ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokenService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationService) - backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, workerWorker, serviceImpl, serviceAccountsProxy, sanitizerProvider, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, serviceImpl, serviceAccountsProxy, sanitizerProvider, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, registerer) if err != nil { @@ -1195,7 +1161,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer) + idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer, tracer) verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationServiceMock, idimplService) httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service12, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service13, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationServiceMock, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service11, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokentestService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) if err != nil { @@ -1250,38 +1216,6 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { } importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service12, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service12, importDashboardService, service11, pluginService, dashboardService) - config := worker.ProvideWorkerConfig() - databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) - outboxQueue := metadata.ProvideOutboxQueue(databaseDatabase, tracer, registerer) - secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, featureToggles, registerer) - if err != nil { - return nil, err - } - keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(databaseDatabase, tracer, featureToggles, registerer) - if err != nil { - return nil, err - } - encryptedValueStorage, err := encryption.ProvideEncryptedValueStorage(databaseDatabase, tracer, featureToggles) - if err != nil { - return nil, err - } - dataKeyStorage, err := encryption.ProvideDataKeyStorage(databaseDatabase, tracer, featureToggles, registerer) - if err != nil { - return nil, err - } - providerMap := encryption2.ProvideThirdPartyProviderMap() - encryptionManager, err := manager4.ProvideEncryptionManager(tracer, dataKeyStorage, cfg, usageStats, providerMap) - if err != nil { - return nil, err - } - ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) - if err != nil { - return nil, err - } - workerWorker, err := worker.NewWorker(config, tracer, databaseDatabase, outboxQueue, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService, encryptionManager, featureToggles, registerer) - if err != nil { - return nil, err - } sanitizerProvider := sanitizer.ProvideService(renderingService) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { @@ -1348,7 +1282,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { } ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokentestService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationServiceMock) - backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, workerWorker, serviceImpl, serviceAccountsProxy, sanitizerProvider, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, serviceImpl, serviceAccountsProxy, sanitizerProvider, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, registerer) if err != nil { @@ -1445,9 +1379,17 @@ func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOp storageMetrics := resource.ProvideStorageMetrics(registerer) bleveIndexMetrics := resource.ProvideIndexMetrics(registerer) gatherer := metrics.ProvideGatherer() + tracingConfig, err := tracing.ProvideTracingConfig(cfg) + if err != nil { + return nil, err + } + tracingService, err := tracing.ProvideService(tracingConfig) + if err != nil { + return nil, err + } hooksService := hooks.ProvideService() ossLicensingService := licensing.ProvideService(cfg, hooksService) - moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, ossLicensingService) + moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, tracingService, ossLicensingService) if err != nil { return nil, err } @@ -1495,7 +1437,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption3.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, featuremgmt.ProvideOpenFeatureService, featuremgmt.ProvideStaticEvaluator, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, encryption.ProvideDataKeyStorage, encryption.ProvideEncryptedValueStorage, metadata.ProvideOutboxQueue, service11.ProvideSecureValueService, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, encryption2.ProvideThirdPartyProviderMap, worker.ProvideWorkerConfig, worker.NewWorker, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, featuremgmt.ProvideOpenFeatureService, featuremgmt.ProvideStaticEvaluator, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, encryption2.ProvideDataKeyStorage, encryption2.ProvideEncryptedValueStorage, service11.ProvideSecureValueService, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, encryption3.ProvideThirdPartyProviderMap, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 0f0dd034983..5a1193c8592 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -8,6 +8,7 @@ import ( "github.com/google/wire" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" @@ -165,6 +166,10 @@ var wireExtsBaseCLISet = wire.NewSet( var wireExtsModuleServerSet = wire.NewSet( NewModule, wireExtsBaseCLISet, + // Tracing + tracing.ProvideTracingConfig, + tracing.ProvideService, + wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, diff --git a/pkg/services/accesscontrol/actest/store_mock.go b/pkg/services/accesscontrol/actest/store_mock.go index ea2a48fe560..84e5bef1eb7 100644 --- a/pkg/services/accesscontrol/actest/store_mock.go +++ b/pkg/services/accesscontrol/actest/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package actest diff --git a/pkg/services/annotations/annotations_repository_mock.go b/pkg/services/annotations/annotations_repository_mock.go index 242834ced2b..f9cab3f896e 100644 --- a/pkg/services/annotations/annotations_repository_mock.go +++ b/pkg/services/annotations/annotations_repository_mock.go @@ -1,10 +1,9 @@ -// Code generated by mockery v2.12.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package annotations import ( context "context" - testing "testing" mock "github.com/stretchr/testify/mock" ) @@ -18,6 +17,10 @@ type FakeAnnotationsRepo struct { func (_m *FakeAnnotationsRepo) Delete(ctx context.Context, params *DeleteParams) error { ret := _m.Called(ctx, params) + if len(ret) == 0 { + panic("no return value specified for Delete") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteParams) error); ok { r0 = rf(ctx, params) @@ -32,7 +35,15 @@ func (_m *FakeAnnotationsRepo) Delete(ctx context.Context, params *DeleteParams) func (_m *FakeAnnotationsRepo) Find(ctx context.Context, query *ItemQuery) ([]*ItemDTO, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for Find") + } + var r0 []*ItemDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *ItemQuery) ([]*ItemDTO, error)); ok { + return rf(ctx, query) + } if rf, ok := ret.Get(0).(func(context.Context, *ItemQuery) []*ItemDTO); ok { r0 = rf(ctx, query) } else { @@ -41,7 +52,6 @@ func (_m *FakeAnnotationsRepo) Find(ctx context.Context, query *ItemQuery) ([]*I } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *ItemQuery) error); ok { r1 = rf(ctx, query) } else { @@ -55,14 +65,21 @@ func (_m *FakeAnnotationsRepo) Find(ctx context.Context, query *ItemQuery) ([]*I func (_m *FakeAnnotationsRepo) FindTags(ctx context.Context, query *TagsQuery) (FindTagsResult, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for FindTags") + } + var r0 FindTagsResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *TagsQuery) (FindTagsResult, error)); ok { + return rf(ctx, query) + } if rf, ok := ret.Get(0).(func(context.Context, *TagsQuery) FindTagsResult); ok { r0 = rf(ctx, query) } else { r0 = ret.Get(0).(FindTagsResult) } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *TagsQuery) error); ok { r1 = rf(ctx, query) } else { @@ -76,6 +93,10 @@ func (_m *FakeAnnotationsRepo) FindTags(ctx context.Context, query *TagsQuery) ( func (_m *FakeAnnotationsRepo) Save(ctx context.Context, item *Item) error { ret := _m.Called(ctx, item) + if len(ret) == 0 { + panic("no return value specified for Save") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *Item) error); ok { r0 = rf(ctx, item) @@ -90,6 +111,10 @@ func (_m *FakeAnnotationsRepo) Save(ctx context.Context, item *Item) error { func (_m *FakeAnnotationsRepo) SaveMany(ctx context.Context, items []Item) error { ret := _m.Called(ctx, items) + if len(ret) == 0 { + panic("no return value specified for SaveMany") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, []Item) error); ok { r0 = rf(ctx, items) @@ -104,6 +129,10 @@ func (_m *FakeAnnotationsRepo) SaveMany(ctx context.Context, items []Item) error func (_m *FakeAnnotationsRepo) Update(ctx context.Context, item *Item) error { ret := _m.Called(ctx, item) + if len(ret) == 0 { + panic("no return value specified for Update") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *Item) error); ok { r0 = rf(ctx, item) @@ -114,8 +143,12 @@ func (_m *FakeAnnotationsRepo) Update(ctx context.Context, item *Item) error { return r0 } -// NewFakeAnnotationsRepo creates a new instance of FakeAnnotationsRepo. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewFakeAnnotationsRepo(t testing.TB) *FakeAnnotationsRepo { +// NewFakeAnnotationsRepo creates a new instance of FakeAnnotationsRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFakeAnnotationsRepo(t interface { + mock.TestingT + Cleanup(func()) +}) *FakeAnnotationsRepo { mock := &FakeAnnotationsRepo{} mock.Mock.Test(t) diff --git a/pkg/services/auth/authimpl/auth_token.go b/pkg/services/auth/authimpl/auth_token.go index bf95502b11e..e8a00ff8c85 100644 --- a/pkg/services/auth/authimpl/auth_token.go +++ b/pkg/services/auth/authimpl/auth_token.go @@ -80,6 +80,9 @@ type UserAuthTokenService struct { } func (s *UserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.CreateToken") + defer span.End() + token, hashedToken, err := generateAndHashToken(s.cfg.SecretKey) if err != nil { return nil, err @@ -136,6 +139,9 @@ func (s *UserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.Create } func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.LookupToken") + defer span.End() + hashedToken := hashToken(s.cfg.SecretKey, unhashedToken) var model userAuthToken var exists bool @@ -234,6 +240,9 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st } func (s *UserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetTokenByExternalSessionID") + defer span.End() + var token userAuthToken err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { exists, err := dbSession.Where("external_session_id = ?", externalSessionID).Get(&token) @@ -258,14 +267,23 @@ func (s *UserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, } func (s *UserAuthTokenService) GetExternalSession(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetExternalSession") + defer span.End() + return s.externalSessionStore.Get(ctx, externalSessionID) } func (s *UserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.FindExternalSessions") + defer span.End() + return s.externalSessionStore.List(ctx, query) } func (s *UserAuthTokenService) UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error { + ctx, span := s.tracer.Start(ctx, "authtoken.UpdateExternalSession") + defer span.End() + return s.externalSessionStore.Update(ctx, externalSessionID, cmd) } @@ -329,6 +347,9 @@ func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateC } func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.rotateToken") + defer span.End() + var clientIPStr string if clientIP != nil { clientIPStr = clientIP.String() @@ -385,6 +406,9 @@ func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.User } func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.UserToken, soft bool) error { + ctx, span := s.tracer.Start(ctx, "authtoken.RevokeToken") + defer span.End() + if token == nil { return auth.ErrUserTokenNotFound } @@ -434,6 +458,9 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.User } func (s *UserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId int64) error { + ctx, span := s.tracer.Start(ctx, "authtoken.RevokeAllUserTokens") + defer span.End() + return s.sqlStore.InTransaction(ctx, func(ctx context.Context) error { ctxLogger := s.log.FromContext(ctx) err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { @@ -466,6 +493,9 @@ func (s *UserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId i } func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, userIds []int64) error { + ctx, span := s.tracer.Start(ctx, "authtoken.BatchRevokeAllUserTokens") + defer span.End() + return s.sqlStore.InTransaction(ctx, func(ctx context.Context) error { ctxLogger := s.log.FromContext(ctx) if len(userIds) == 0 { @@ -507,6 +537,9 @@ func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, use } func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetUserToken") + defer span.End() + var result auth.UserToken err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { var token userAuthToken @@ -526,6 +559,9 @@ func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTok } func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetUserTokens") + defer span.End() + result := []*auth.UserToken{} err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { var tokens []*userAuthToken @@ -554,6 +590,9 @@ func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) // ActiveTokenCount returns the number of active tokens. If userID is nil, the count is for all users. func (s *UserAuthTokenService) ActiveTokenCount(ctx context.Context, userID *int64) (int64, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.ActiveTokenCount") + defer span.End() + if userID != nil && *userID < 1 { return 0, errUserIDInvalid } @@ -574,6 +613,9 @@ func (s *UserAuthTokenService) ActiveTokenCount(ctx context.Context, userID *int } func (s *UserAuthTokenService) DeleteUserRevokedTokens(ctx context.Context, userID int64, window time.Duration) error { + ctx, span := s.tracer.Start(ctx, "authtoken.DeleteUserRevokedTokens") + defer span.End() + return s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { query := "DELETE FROM user_auth_token WHERE user_id = ? AND revoked_at > 0 AND revoked_at <= ?" res, err := sess.Exec(query, userID, time.Now().Add(-window).Unix()) @@ -592,6 +634,9 @@ func (s *UserAuthTokenService) DeleteUserRevokedTokens(ctx context.Context, user } func (s *UserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetUserRevokedTokens") + defer span.End() + result := []*auth.UserToken{} err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { var tokens []*userAuthToken diff --git a/pkg/services/auth/authtest/auth_token_service_mock.go b/pkg/services/auth/authtest/auth_token_service_mock.go index 744ef1fe0b0..11ea10a4d7b 100644 --- a/pkg/services/auth/authtest/auth_token_service_mock.go +++ b/pkg/services/auth/authtest/auth_token_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package authtest diff --git a/pkg/services/auth/authtest/external_session_store_mock.go b/pkg/services/auth/authtest/external_session_store_mock.go index 10894e67444..e4f727e14ec 100644 --- a/pkg/services/auth/authtest/external_session_store_mock.go +++ b/pkg/services/auth/authtest/external_session_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package authtest diff --git a/pkg/services/auth/idimpl/service.go b/pkg/services/auth/idimpl/service.go index 5dec411fdee..cb8e6adc9ce 100644 --- a/pkg/services/auth/idimpl/service.go +++ b/pkg/services/auth/idimpl/service.go @@ -8,6 +8,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/trace" "golang.org/x/sync/singleflight" authnlib "github.com/grafana/authlib/authn" @@ -32,18 +33,18 @@ var _ auth.IDService = (*Service)(nil) func ProvideService( cfg *setting.Cfg, signer auth.IDSigner, - cache remotecache.CacheStorage, - authnService authn.Service, - reg prometheus.Registerer, + cache remotecache.CacheStorage, authnService authn.Service, + reg prometheus.Registerer, tracer trace.Tracer, ) *Service { s := &Service{ cfg: cfg, logger: log.New("id-service"), signer: signer, cache: cache, metrics: newMetrics(reg), nsMapper: request.GetNamespaceMapper(cfg), + tracer: tracer, } - authnService.RegisterPostAuthHook(s.hook, 140) + authnService.RegisterPostAuthHook(s.SyncIDToken, 140) return s } @@ -55,10 +56,14 @@ type Service struct { cache remotecache.CacheStorage si singleflight.Group metrics *metrics + tracer trace.Tracer nsMapper request.NamespaceMapper } func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) { + ctx, span := s.tracer.Start(ctx, "user.sync.SignIdentity") + defer span.End() + defer func(t time.Time) { s.metrics.tokenSigningDurationHistogram.Observe(time.Since(t).Seconds()) }(time.Now()) @@ -140,10 +145,15 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri } func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) error { + ctx, span := s.tracer.Start(ctx, "user.sync.RemoveIDToken") + defer span.End() + return s.cache.Delete(ctx, getCacheKey(id)) } -func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { +func (s *Service) SyncIDToken(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + ctx, span := s.tracer.Start(ctx, "user.sync.SyncIDToken") + defer span.End() // FIXME(kalleep): we should probably lazy load this token, idClaims, err := s.SignIdentity(ctx, identity) if err != nil { diff --git a/pkg/services/auth/idimpl/service_test.go b/pkg/services/auth/idimpl/service_test.go index bb7ee510e55..8690c5c24bc 100644 --- a/pkg/services/auth/idimpl/service_test.go +++ b/pkg/services/auth/idimpl/service_test.go @@ -11,6 +11,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/remotecache" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/idtest" "github.com/grafana/grafana/pkg/services/authn" @@ -29,7 +30,7 @@ func Test_ProvideService(t *testing.T) { }, } - _ = ProvideService(setting.NewCfg(), nil, nil, authnService, nil) + _ = ProvideService(setting.NewCfg(), nil, nil, authnService, nil, tracing.InitializeTracerForTest()) assert.True(t, hookRegistered) }) } @@ -51,7 +52,7 @@ func TestService_SignIdentity(t *testing.T) { t.Run("should sign identity", func(t *testing.T) { s := ProvideService( setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), - &authntest.FakeService{}, nil, + &authntest.FakeService{}, nil, tracing.InitializeTracerForTest(), ) token, _, err := s.SignIdentity(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser}) require.NoError(t, err) @@ -61,7 +62,7 @@ func TestService_SignIdentity(t *testing.T) { t.Run("should sign identity with authenticated by if user is externally authenticated", func(t *testing.T) { s := ProvideService( setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), - &authntest.FakeService{}, nil, + &authntest.FakeService{}, nil, tracing.InitializeTracerForTest(), ) token, _, err := s.SignIdentity(context.Background(), &authn.Identity{ ID: "1", @@ -86,7 +87,7 @@ func TestService_SignIdentity(t *testing.T) { t.Run("should sign identity with authenticated by if user is externally authenticated", func(t *testing.T) { s := ProvideService( setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), - &authntest.FakeService{}, nil, + &authntest.FakeService{}, nil, tracing.InitializeTracerForTest(), ) _, gotClaims, err := s.SignIdentity(context.Background(), &authn.Identity{ ID: "1", @@ -106,7 +107,7 @@ func TestService_SignIdentity(t *testing.T) { t.Run("should sign new token if org role has changed", func(t *testing.T) { s := ProvideService( setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), - &authntest.FakeService{}, nil, + &authntest.FakeService{}, nil, tracing.InitializeTracerForTest(), ) ident := &authn.Identity{ diff --git a/pkg/services/auth/idtest/mock.go b/pkg/services/auth/idtest/mock.go index cb9aeadf316..9837c421cd8 100644 --- a/pkg/services/auth/idtest/mock.go +++ b/pkg/services/auth/idtest/mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package idtest diff --git a/pkg/services/authn/authnimpl/registration.go b/pkg/services/authn/authnimpl/registration.go index 7eadac99083..ce0ed9a9e89 100644 --- a/pkg/services/authn/authnimpl/registration.go +++ b/pkg/services/authn/authnimpl/registration.go @@ -48,10 +48,10 @@ func ProvideRegistration( logger := log.New("authn.registration") authnSvc.RegisterClient(clients.ProvideRender(renderService)) - authnSvc.RegisterClient(clients.ProvideAPIKey(apikeyService)) + authnSvc.RegisterClient(clients.ProvideAPIKey(apikeyService, tracer)) if cfg.LoginCookieName != "" { - authnSvc.RegisterClient(clients.ProvideSession(cfg, sessionService, authInfoService)) + authnSvc.RegisterClient(clients.ProvideSession(cfg, sessionService, authInfoService, tracer)) } var proxyClients []authn.ProxyClient @@ -59,20 +59,20 @@ func ProvideRegistration( // always register LDAP if LDAP is enabled in SSO settings if cfg.LDAPAuthEnabled || features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsLDAP) { - ldap := clients.ProvideLDAP(cfg, ldapService, userService, authInfoService) + ldap := clients.ProvideLDAP(cfg, ldapService, userService, authInfoService, tracer) proxyClients = append(proxyClients, ldap) passwordClients = append(passwordClients, ldap) } if !cfg.DisableLogin { - grafana := clients.ProvideGrafana(cfg, userService) + grafana := clients.ProvideGrafana(cfg, userService, tracer) proxyClients = append(proxyClients, grafana) passwordClients = append(passwordClients, grafana) } // if we have password clients configure check if basic auth or form auth is enabled if len(passwordClients) > 0 { - passwordClient := clients.ProvidePassword(loginAttempts, passwordClients...) + passwordClient := clients.ProvidePassword(loginAttempts, tracer, passwordClients...) if cfg.BasicAuthEnabled { authnSvc.RegisterClient(clients.ProvideBasic(passwordClient)) } @@ -103,7 +103,7 @@ func ProvideRegistration( } if cfg.AuthProxy.Enabled && len(proxyClients) > 0 { - proxy, err := clients.ProvideProxy(cfg, cache, proxyClients...) + proxy, err := clients.ProvideProxy(cfg, cache, tracer, proxyClients...) if err != nil { logger.Error("Failed to configure auth proxy", "err", err) } else { @@ -113,16 +113,16 @@ func ProvideRegistration( if cfg.JWTAuth.Enabled { orgRoleMapper := connectors.ProvideOrgRoleMapper(cfg, orgService) - authnSvc.RegisterClient(clients.ProvideJWT(jwtService, orgRoleMapper, cfg)) + authnSvc.RegisterClient(clients.ProvideJWT(jwtService, orgRoleMapper, cfg, tracer)) } if cfg.ExtJWTAuth.Enabled { - authnSvc.RegisterClient(clients.ProvideExtendedJWT(cfg)) + authnSvc.RegisterClient(clients.ProvideExtendedJWT(cfg, tracer)) } for name := range socialService.GetOAuthProviders() { clientName := authn.ClientWithPrefix(name) - authnSvc.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features)) + authnSvc.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features, tracer)) } if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) { diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index 5bc4aa8fb46..2023cb4e418 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "go.opentelemetry.io/otel/trace" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/components/apikeygen" @@ -35,16 +37,18 @@ const ( metaKeySkipLastUsed = "keySkipLastUsed" ) -func ProvideAPIKey(apiKeyService apikey.Service) *APIKey { +func ProvideAPIKey(apiKeyService apikey.Service, tracer trace.Tracer) *APIKey { return &APIKey{ log: log.New(authn.ClientAPIKey), apiKeyService: apiKeyService, + tracer: tracer, } } type APIKey struct { log log.Logger apiKeyService apikey.Service + tracer trace.Tracer } func (s *APIKey) Name() string { @@ -52,6 +56,8 @@ func (s *APIKey) Name() string { } func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := s.tracer.Start(ctx, "authn.apikey.Authenticate") + defer span.End() key, err := s.getAPIKey(ctx, getTokenFromRequest(r)) if err != nil { if errors.Is(err, apikeygen.ErrInvalidApiKey) { @@ -84,6 +90,8 @@ func (s *APIKey) IsEnabled() bool { } func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) { + ctx, span := s.tracer.Start(ctx, "authn.apikey.getAPIKey") + defer span.End() fn := s.getFromToken if !strings.HasPrefix(token, satokengen.GrafanaPrefix) { fn = s.getFromTokenLegacy @@ -98,6 +106,8 @@ func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, e } func (s *APIKey) getFromToken(ctx context.Context, token string) (*apikey.APIKey, error) { + ctx, span := s.tracer.Start(ctx, "authn.apikey.getFromToken") + defer span.End() decoded, err := satokengen.Decode(token) if err != nil { return nil, err @@ -112,6 +122,8 @@ func (s *APIKey) getFromToken(ctx context.Context, token string) (*apikey.APIKey } func (s *APIKey) getFromTokenLegacy(ctx context.Context, token string) (*apikey.APIKey, error) { + ctx, span := s.tracer.Start(ctx, "authn.apikey.getFromTokenLegacy") + defer span.End() decoded, err := apikeygen.Decode(token) if err != nil { return nil, err @@ -144,6 +156,9 @@ func (s *APIKey) Priority() uint { } func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error { + ctx, span := s.tracer.Start(ctx, "authn.apikey.Hook") //nolint:ineffassign,staticcheck + defer span.End() + if r.GetMeta(metaKeySkipLastUsed) != "" { return nil } diff --git a/pkg/services/authn/clients/api_key_test.go b/pkg/services/authn/clients/api_key_test.go index 889f38cd4d0..62fc83bc7f0 100644 --- a/pkg/services/authn/clients/api_key_test.go +++ b/pkg/services/authn/clients/api_key_test.go @@ -12,6 +12,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/components/satokengen" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/authn" @@ -106,7 +107,7 @@ func TestAPIKey_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideAPIKey(&apikeytest.Service{ExpectedAPIKey: tt.expectedKey}) + c := ProvideAPIKey(&apikeytest.Service{ExpectedAPIKey: tt.expectedKey}, tracing.InitializeTracerForTest()) identity, err := c.Authenticate(context.Background(), tt.req) if tt.expectedErr != nil { @@ -173,7 +174,7 @@ func TestAPIKey_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideAPIKey(&apikeytest.Service{}) + c := ProvideAPIKey(&apikeytest.Service{}, tracing.InitializeTracerForTest()) assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) }) } diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 74754d85681..5baafdf0c6b 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/go-jose/go-jose/v3/jwt" + "go.opentelemetry.io/otel/trace" authlib "github.com/grafana/authlib/authn" claims "github.com/grafana/authlib/types" @@ -41,7 +42,7 @@ var ( ) ) -func ProvideExtendedJWT(cfg *setting.Cfg) *ExtendedJWT { +func ProvideExtendedJWT(cfg *setting.Cfg, tracer trace.Tracer) *ExtendedJWT { keys := authlib.NewKeyRetriever(authlib.KeyRetrieverConfig{ SigningKeysURL: cfg.ExtJWTAuth.JWKSUrl, }) @@ -60,6 +61,7 @@ func ProvideExtendedJWT(cfg *setting.Cfg) *ExtendedJWT { namespaceMapper: request.GetNamespaceMapper(cfg), accessTokenVerifier: accessTokenVerifier, idTokenVerifier: idTokenVerifier, + tracer: tracer, } } @@ -69,9 +71,13 @@ type ExtendedJWT struct { accessTokenVerifier authlib.Verifier[authlib.AccessTokenClaims] idTokenVerifier authlib.Verifier[authlib.IDTokenClaims] namespaceMapper request.NamespaceMapper + tracer trace.Tracer } func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := s.tracer.Start(ctx, "authn.extjwt.Authenticate") + defer span.End() + jwtToken := s.retrieveAuthenticationToken(r.HTTPRequest) accessTokenClaims, err := s.accessTokenVerifier.Verify(ctx, jwtToken) diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 13970e681be..b72b6303154 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -17,6 +17,7 @@ import ( authnlib "github.com/grafana/authlib/authn" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/setting" ) @@ -699,7 +700,7 @@ func setupTestCtx(cfg *setting.Cfg) *testEnv { } } - extJwtClient := ProvideExtendedJWT(cfg) + extJwtClient := ProvideExtendedJWT(cfg, tracing.InitializeTracerForTest()) return &testEnv{ s: extJwtClient, diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index ddabb405443..379c19ffca3 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -7,6 +7,8 @@ import ( "net/mail" "strconv" + "go.opentelemetry.io/otel/trace" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" @@ -19,13 +21,14 @@ import ( var _ authn.ProxyClient = new(Grafana) var _ authn.PasswordClient = new(Grafana) -func ProvideGrafana(cfg *setting.Cfg, userService user.Service) *Grafana { - return &Grafana{cfg, userService} +func ProvideGrafana(cfg *setting.Cfg, userService user.Service, tracer trace.Tracer) *Grafana { + return &Grafana{cfg, userService, tracer} } type Grafana struct { cfg *setting.Cfg userService user.Service + tracer trace.Tracer } func (c *Grafana) String() string { @@ -33,6 +36,9 @@ func (c *Grafana) String() string { } func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.grafana.AuthenticateProxy") //nolint:ineffassign,staticcheck + defer span.End() + identity := &authn.Identity{ AuthenticatedBy: login.AuthProxyAuthModule, AuthID: username, @@ -91,6 +97,9 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern } func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.grafana.AuthenticatePassword") + defer span.End() + usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username}) if err != nil { if errors.Is(err, user.ErrUserNotFound) { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index fd93cf2faa7..fd40757e319 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -97,7 +98,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { cfg := setting.NewCfg() cfg.AuthProxy.AutoSignUp = true cfg.AuthProxy.HeaderProperty = tt.proxyProperty - c := ProvideGrafana(cfg, usertest.NewUserServiceFake()) + c := ProvideGrafana(cfg, usertest.NewUserServiceFake(), tracing.InitializeTracerForTest()) identity, err := c.AuthenticateProxy(context.Background(), tt.req, tt.username, tt.additional) assert.ErrorIs(t, err, tt.expectedErr) @@ -175,7 +176,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { userService.ExpectedError = user.ErrUserNotFound } - c := ProvideGrafana(setting.NewCfg(), userService) + c := ProvideGrafana(setting.NewCfg(), userService, tracing.InitializeTracerForTest()) identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password) assert.ErrorIs(t, err, tt.expectedErr) assert.EqualValues(t, tt.expectedIdentity, identity) diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 82323e21bfe..60d7ed21406 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -5,6 +5,8 @@ import ( "net/http" "strings" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social/connectors" @@ -30,13 +32,14 @@ var ( "jwt.invalid_role", errutil.WithPublicMessage("Invalid Role in claim")) ) -func ProvideJWT(jwtService auth.JWTVerifierService, orgRoleMapper *connectors.OrgRoleMapper, cfg *setting.Cfg) *JWT { +func ProvideJWT(jwtService auth.JWTVerifierService, orgRoleMapper *connectors.OrgRoleMapper, cfg *setting.Cfg, tracer trace.Tracer) *JWT { return &JWT{ cfg: cfg, log: log.New(authn.ClientJWT), jwtService: jwtService, orgRoleMapper: orgRoleMapper, orgMappingCfg: orgRoleMapper.ParseOrgMappingSettings(context.Background(), cfg.JWTAuth.OrgMapping, cfg.JWTAuth.RoleAttributeStrict), + tracer: tracer, } } @@ -46,6 +49,7 @@ type JWT struct { orgMappingCfg connectors.MappingConfiguration log log.Logger jwtService auth.JWTVerifierService + tracer trace.Tracer } func (s *JWT) Name() string { @@ -53,6 +57,9 @@ func (s *JWT) Name() string { } func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := s.tracer.Start(ctx, "authn.jwt.Authenticate") + defer span.End() + jwtToken := s.retrieveToken(r.HTTPRequest) s.stripSensitiveParam(r.HTTPRequest) @@ -114,9 +121,6 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi if !s.cfg.JWTAuth.SkipOrgRoleSync { role, grafanaAdmin := s.extractRoleAndAdmin(claims) - if err != nil { - s.log.Warn("Failed to extract role", "err", err) - } if s.cfg.JWTAuth.AllowAssignGrafanaAdmin { id.IsGrafanaAdmin = &grafanaAdmin diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 7fccffc6b17..2de491935b7 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social/connectors" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" @@ -262,7 +263,7 @@ func TestAuthenticateJWT(t *testing.T) { jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(tc.cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - tc.cfg) + tc.cfg, tracing.InitializeTracerForTest()) validHTTPReq := &http.Request{ Header: map[string][]string{ jwtHeaderName: {"sample-token"}}, @@ -380,7 +381,7 @@ func TestJWTClaimConfig(t *testing.T) { } jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - cfg) + cfg, tracing.InitializeTracerForTest()) _, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, @@ -493,7 +494,7 @@ func TestJWTTest(t *testing.T) { jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - cfg) + cfg, tracing.InitializeTracerForTest()) httpReq := &http.Request{ URL: &url.URL{RawQuery: "auth_token=" + tc.token}, Header: map[string][]string{ @@ -549,7 +550,7 @@ func TestJWTStripParam(t *testing.T) { jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - cfg) + cfg, tracing.InitializeTracerForTest()) _, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, @@ -608,7 +609,7 @@ func TestJWTSubClaimsConfig(t *testing.T) { jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - cfg) + cfg, tracing.InitializeTracerForTest()) identity, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index e78f0ff119c..4f9cbccf066 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -4,6 +4,8 @@ import ( "context" "errors" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/ldap/multildap" @@ -20,8 +22,8 @@ type ldapService interface { User(username string) (*login.ExternalUserInfo, error) } -func ProvideLDAP(cfg *setting.Cfg, ldapService ldapService, userService user.Service, authInfoService login.AuthInfoService) *LDAP { - return &LDAP{cfg, log.New("authn.ldap"), ldapService, userService, authInfoService} +func ProvideLDAP(cfg *setting.Cfg, ldapService ldapService, userService user.Service, authInfoService login.AuthInfoService, tracer trace.Tracer) *LDAP { + return &LDAP{cfg, log.New("authn.ldap"), ldapService, userService, authInfoService, tracer} } type LDAP struct { @@ -30,6 +32,7 @@ type LDAP struct { service ldapService userService user.Service authInfoService login.AuthInfoService + tracer trace.Tracer } func (c *LDAP) String() string { @@ -37,6 +40,8 @@ func (c *LDAP) String() string { } func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, _ map[string]string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.ldap.AuthenticateProxy") + defer span.End() info, err := c.service.User(username) if errors.Is(err, multildap.ErrDidNotFindUser) { return c.disableUser(ctx, username) @@ -50,6 +55,8 @@ func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username } func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.ldap.AuthenticatePassword") + defer span.End() info, err := c.service.Login(&login.LoginUserQuery{ Username: username, Password: password, @@ -75,6 +82,8 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern // disableUser will disable users if they logged in via LDAP previously func (c *LDAP) disableUser(ctx context.Context, username string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.ldap.disableUser") + defer span.End() c.logger.Debug("User was not found in the LDAP directory tree", "username", username) retErr := errIdentityNotFound.Errorf("no user found: %w", multildap.ErrDidNotFindUser) diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index cd08fa2ac01..2d09ef5c933 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/ldap/multildap" @@ -197,13 +197,13 @@ func setupLDAPTestCase(tt *ldapTestCase) *LDAP { ExpectedError: tt.expectedAuthInfoErr, } - c := &LDAP{ - cfg: setting.NewCfg(), - logger: log.New("authn.ldap.test"), - service: &service.LDAPFakeService{ExpectedUser: tt.expectedLDAPInfo, ExpectedError: tt.expectedLDAPErr}, - userService: userService, - authInfoService: authInfoService, - } + c := ProvideLDAP( + setting.NewCfg(), + &service.LDAPFakeService{ExpectedUser: tt.expectedLDAPInfo, ExpectedError: tt.expectedLDAPErr}, + userService, + authInfoService, + tracing.InitializeTracerForTest(), + ) return c } diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 1c6429c3cc4..0e8053a39b7 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -12,6 +12,7 @@ import ( "os" "strings" + "go.opentelemetry.io/otel/trace" "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/apimachinery/errutil" @@ -70,12 +71,14 @@ var ( func ProvideOAuth( name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService, - socialService social.Service, settingsProviderService setting.Provider, features featuremgmt.FeatureToggles, + socialService social.Service, settingsProviderService setting.Provider, + features featuremgmt.FeatureToggles, tracer trace.Tracer, ) *OAuth { providerName := strings.TrimPrefix(name, "auth.client.") return &OAuth{ name, fmt.Sprintf("oauth_%s", providerName), providerName, - log.New(name), cfg, settingsProviderService, oauthService, socialService, features, + log.New(name), cfg, tracer, settingsProviderService, oauthService, + socialService, features, } } @@ -85,6 +88,7 @@ type OAuth struct { providerName string log log.Logger cfg *setting.Cfg + tracer trace.Tracer settingsProviderSvc setting.Provider oauthService oauthtoken.OAuthTokenService @@ -97,6 +101,9 @@ func (c *OAuth) Name() string { } func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.oauth.Authenticate") + defer span.End() + r.SetMeta(authn.MetaKeyAuthModule, c.moduleName) oauthCfg := c.socialService.GetOAuthInfoProvider(c.providerName) @@ -232,6 +239,9 @@ func (c *OAuth) GetConfig() authn.SSOClientConfig { } func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) { + ctx, span := c.tracer.Start(ctx, "authn.oauth.RedirectURL") //nolint:ineffassign,staticcheck + defer span.End() + var opts []oauth2.AuthCodeOption oauthCfg := c.socialService.GetOAuthInfoProvider(c.providerName) @@ -274,6 +284,9 @@ func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redir } func (c *OAuth) Logout(ctx context.Context, user identity.Requester, sessionToken *auth.UserToken) (*authn.Redirect, bool) { + ctx, span := c.tracer.Start(ctx, "authn.oauth.Logout") + defer span.End() + token := c.oauthService.GetCurrentOAuthToken(ctx, user, sessionToken) userID, err := identity.UserIdentifier(user.GetID()) diff --git a/pkg/services/authn/clients/oauth_test.go b/pkg/services/authn/clients/oauth_test.go index 7a9222154f7..2bd536d6544 100644 --- a/pkg/services/authn/clients/oauth_test.go +++ b/pkg/services/authn/clients/oauth_test.go @@ -16,6 +16,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/login/social/socialtest" "github.com/grafana/grafana/pkg/services/auth" @@ -296,7 +297,7 @@ func TestOAuth_Authenticate(t *testing.T) { }, } - c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, settingsProvider, featuremgmt.WithFeatures(tt.features...)) + c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, settingsProvider, featuremgmt.WithFeatures(tt.features...), tracing.InitializeTracerForTest()) identity, err := c.Authenticate(context.Background(), tt.req) assert.ErrorIs(t, err, tt.expectedErr) @@ -376,7 +377,7 @@ func TestOAuth_RedirectURL(t *testing.T) { cfg := setting.NewCfg() - c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, &setting.OSSImpl{Cfg: cfg}, featuremgmt.WithFeatures()) + c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, &setting.OSSImpl{Cfg: cfg}, featuremgmt.WithFeatures(), tracing.InitializeTracerForTest()) redirect, err := c.RedirectURL(context.Background(), nil) assert.ErrorIs(t, err, tt.expectedErr) @@ -489,7 +490,7 @@ func TestOAuth_Logout(t *testing.T) { fakeSocialSvc := &socialtest.FakeSocialService{ ExpectedAuthInfoProvider: tt.oauthCfg, } - c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}, featuremgmt.WithFeatures()) + c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}, featuremgmt.WithFeatures(), tracing.InitializeTracerForTest()) redirect, ok := c.Logout(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser}, nil) @@ -549,7 +550,8 @@ func TestIsEnabled(t *testing.T) { nil, fakeSocialSvc, &setting.OSSImpl{Cfg: cfg}, - featuremgmt.WithFeatures()) + featuremgmt.WithFeatures(), + tracing.InitializeTracerForTest()) assert.Equal(t, tt.expected, c.IsEnabled()) }) } diff --git a/pkg/services/authn/clients/password.go b/pkg/services/authn/clients/password.go index 76e82d4fc59..f9b2483d2c5 100644 --- a/pkg/services/authn/clients/password.go +++ b/pkg/services/authn/clients/password.go @@ -4,6 +4,8 @@ import ( "context" "errors" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" @@ -18,17 +20,20 @@ var ( var _ authn.PasswordClient = new(Password) -func ProvidePassword(loginAttempts loginattempt.Service, clients ...authn.PasswordClient) *Password { - return &Password{loginAttempts, clients, log.New("authn.password")} +func ProvidePassword(loginAttempts loginattempt.Service, tracer trace.Tracer, clients ...authn.PasswordClient) *Password { + return &Password{loginAttempts, clients, log.New("authn.password"), tracer} } type Password struct { loginAttempts loginattempt.Service clients []authn.PasswordClient log log.Logger + tracer trace.Tracer } func (c *Password) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.password.AuthenticatePassword") + defer span.End() r.SetMeta(authn.MetaKeyUsername, username) ok, err := c.loginAttempts.Validate(ctx, username) diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go index 6f7619ce1b7..fd7933a37e7 100644 --- a/pkg/services/authn/clients/password_test.go +++ b/pkg/services/authn/clients/password_test.go @@ -10,6 +10,7 @@ import ( claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" @@ -65,7 +66,7 @@ func TestPassword_AuthenticatePassword(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvidePassword(loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, tt.clients...) + c := ProvidePassword(loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, tracing.InitializeTracerForTest(), tt.clients...) r := &authn.Request{ OrgID: 12345, HTTPRequest: &http.Request{ diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index 237d02f13cc..f20b1534a02 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -13,6 +13,7 @@ import ( "time" claims "github.com/grafana/authlib/types" + "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" @@ -45,12 +46,12 @@ var ( _ authn.ContextAwareClient = new(Proxy) ) -func ProvideProxy(cfg *setting.Cfg, cache proxyCache, clients ...authn.ProxyClient) (*Proxy, error) { +func ProvideProxy(cfg *setting.Cfg, cache proxyCache, tracer trace.Tracer, clients ...authn.ProxyClient) (*Proxy, error) { list, err := parseAcceptList(cfg.AuthProxy.Whitelist) if err != nil { return nil, err } - return &Proxy{log.New(authn.ClientProxy), cfg, cache, clients, list}, nil + return &Proxy{log.New(authn.ClientProxy), cfg, cache, clients, list, tracer}, nil } type proxyCache interface { @@ -65,6 +66,7 @@ type Proxy struct { cache proxyCache clients []authn.ProxyClient acceptedIPs []*net.IPNet + tracer trace.Tracer } func (c *Proxy) Name() string { @@ -72,6 +74,8 @@ func (c *Proxy) Name() string { } func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.proxy.Authenticate") + defer span.End() if !c.isAllowedIP(r) { return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list") } @@ -115,6 +119,8 @@ func (c *Proxy) IsEnabled() bool { // See if we have cached the user id, in that case we can fetch the signed-in user and skip sync. // Error here means that we could not find anything in cache, so we can proceed as usual func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *authn.Request) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.proxy.retrieveIDFromCache") + defer span.End() entry, err := c.cache.Get(ctx, cacheKey) if err != nil { return nil, err @@ -148,6 +154,8 @@ func (c *Proxy) Priority() uint { } func (c *Proxy) Hook(ctx context.Context, id *authn.Identity, r *authn.Request) error { + ctx, span := c.tracer.Start(ctx, "authn.proxy.Hook") + defer span.End() if id.ClientParams.CacheAuthProxyKey == "" { return nil } diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index b0cefbd6d6c..7b1faaa9efa 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/setting" @@ -113,7 +114,7 @@ func TestProxy_Authenticate(t *testing.T) { calledAdditional = additional return nil, nil }} - c, err := ProvideProxy(cfg, &fakeCache{expectedErr: errors.New("")}, proxyClient) + c, err := ProvideProxy(cfg, &fakeCache{expectedErr: errors.New("")}, tracing.InitializeTracerForTest(), proxyClient) require.NoError(t, err) _, err = c.Authenticate(context.Background(), tt.req) @@ -169,7 +170,7 @@ func TestProxy_Test(t *testing.T) { cfg := setting.NewCfg() cfg.AuthProxy.HeaderName = "Proxy-Header" - c, _ := ProvideProxy(cfg, nil, nil, nil) + c, _ := ProvideProxy(cfg, nil, tracing.InitializeTracerForTest(), nil) assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req)) }) } @@ -208,7 +209,7 @@ func TestProxy_Hook(t *testing.T) { withRole := func(role string) func(t *testing.T) { cacheKey := fmt.Sprintf("users:johndoe-%s", role) return func(t *testing.T) { - c, err := ProvideProxy(cfg, cache, authntest.MockProxyClient{}) + c, err := ProvideProxy(cfg, cache, tracing.InitializeTracerForTest(), authntest.MockProxyClient{}) require.NoError(t, err) userIdentity := &authn.Identity{ ID: "1", diff --git a/pkg/services/authn/clients/session.go b/pkg/services/authn/clients/session.go index fa0f60b8e12..a644c4771af 100644 --- a/pkg/services/authn/clients/session.go +++ b/pkg/services/authn/clients/session.go @@ -7,6 +7,8 @@ import ( "strconv" "time" + "go.opentelemetry.io/otel/trace" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/auth" @@ -18,12 +20,14 @@ import ( var _ authn.ContextAwareClient = new(Session) -func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService, authInfoService login.AuthInfoService) *Session { +func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService, + authInfoService login.AuthInfoService, tracer trace.Tracer) *Session { return &Session{ cfg: cfg, log: log.New(authn.ClientSession), sessionService: sessionService, authInfoService: authInfoService, + tracer: tracer, } } @@ -32,6 +36,7 @@ type Session struct { log log.Logger sessionService auth.UserTokenService authInfoService login.AuthInfoService + tracer trace.Tracer } func (s *Session) Name() string { diff --git a/pkg/services/authn/clients/session_test.go b/pkg/services/authn/clients/session_test.go index 946168f9f14..13195ba2b9b 100644 --- a/pkg/services/authn/clients/session_test.go +++ b/pkg/services/authn/clients/session_test.go @@ -11,6 +11,7 @@ import ( claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" @@ -31,7 +32,7 @@ func TestSession_Test(t *testing.T) { cfg := setting.NewCfg() cfg.LoginCookieName = "" cfg.LoginMaxLifetime = 20 * time.Second - s := ProvideSession(cfg, &authtest.FakeUserAuthTokenService{}, &authinfotest.FakeService{}) + s := ProvideSession(cfg, &authtest.FakeUserAuthTokenService{}, &authinfotest.FakeService{}, tracing.InitializeTracerForTest()) disabled := s.Test(context.Background(), &authn.Request{HTTPRequest: validHTTPReq}) assert.False(t, disabled) @@ -194,7 +195,7 @@ func TestSession_Authenticate(t *testing.T) { cfg.LoginCookieName = cookieName cfg.TokenRotationIntervalMinutes = 10 cfg.LoginMaxLifetime = 20 * time.Second - s := ProvideSession(cfg, tt.fields.sessionService, tt.fields.authInfoService) + s := ProvideSession(cfg, tt.fields.sessionService, tt.fields.authInfoService, tracing.InitializeTracerForTest()) got, err := s.Authenticate(context.Background(), tt.args.r) require.True(t, (err != nil) == tt.wantErr, err) diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go index a9fe7ce855b..801480b910b 100644 --- a/pkg/services/cloudmigration/api/dtos.go +++ b/pkg/services/cloudmigration/api/dtos.go @@ -150,17 +150,19 @@ const ( type ItemErrorCode string const ( - ErrDatasourceNameConflict ItemErrorCode = "DATASOURCE_NAME_CONFLICT" - ErrDatasourceInvalidURL ItemErrorCode = "DATASOURCE_INVALID_URL" - ErrDatasourceAlreadyManaged ItemErrorCode = "DATASOURCE_ALREADY_MANAGED" - ErrFolderNameConflict ItemErrorCode = "FOLDER_NAME_CONFLICT" - ErrDashboardAlreadyManaged ItemErrorCode = "DASHBOARD_ALREADY_MANAGED" - ErrLibraryElementNameConflict ItemErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" - ErrUnsupportedDataType ItemErrorCode = "UNSUPPORTED_DATA_TYPE" - ErrResourceConflict ItemErrorCode = "RESOURCE_CONFLICT" - ErrUnexpectedStatus ItemErrorCode = "UNEXPECTED_STATUS_CODE" - ErrInternalServiceError ItemErrorCode = "INTERNAL_SERVICE_ERROR" - ErrGeneric ItemErrorCode = "GENERIC_ERROR" + ErrAlertRulesQuotaReached ItemErrorCode = "ALERT_RULES_QUOTA_REACHED" + ErrAlertRulesGroupQuotaReached ItemErrorCode = "ALERT_RULES_GROUP_QUOTA_REACHED" + ErrDatasourceNameConflict ItemErrorCode = "DATASOURCE_NAME_CONFLICT" + ErrDatasourceInvalidURL ItemErrorCode = "DATASOURCE_INVALID_URL" + ErrDatasourceAlreadyManaged ItemErrorCode = "DATASOURCE_ALREADY_MANAGED" + ErrFolderNameConflict ItemErrorCode = "FOLDER_NAME_CONFLICT" + ErrDashboardAlreadyManaged ItemErrorCode = "DASHBOARD_ALREADY_MANAGED" + ErrLibraryElementNameConflict ItemErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" + ErrUnsupportedDataType ItemErrorCode = "UNSUPPORTED_DATA_TYPE" + ErrResourceConflict ItemErrorCode = "RESOURCE_CONFLICT" + ErrUnexpectedStatus ItemErrorCode = "UNEXPECTED_STATUS_CODE" + ErrInternalServiceError ItemErrorCode = "INTERNAL_SERVICE_ERROR" + ErrGeneric ItemErrorCode = "GENERIC_ERROR" ) // swagger:parameters getCloudMigrationRun diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 41d27e02f94..a01a33c62d4 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -110,17 +110,19 @@ const ( type ResourceErrorCode string const ( - ErrDatasourceNameConflict ResourceErrorCode = "DATASOURCE_NAME_CONFLICT" - ErrDatasourceInvalidURL ResourceErrorCode = "DATASOURCE_INVALID_URL" - ErrDatasourceAlreadyManaged ResourceErrorCode = "DATASOURCE_ALREADY_MANAGED" - ErrFolderNameConflict ResourceErrorCode = "FOLDER_NAME_CONFLICT" - ErrDashboardAlreadyManaged ResourceErrorCode = "DASHBOARD_ALREADY_MANAGED" - ErrLibraryElementNameConflict ResourceErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" - ErrUnsupportedDataType ResourceErrorCode = "UNSUPPORTED_DATA_TYPE" - ErrResourceConflict ResourceErrorCode = "RESOURCE_CONFLICT" - ErrUnexpectedStatus ResourceErrorCode = "UNEXPECTED_STATUS_CODE" - ErrInternalServiceError ResourceErrorCode = "INTERNAL_SERVICE_ERROR" - ErrGeneric ResourceErrorCode = "GENERIC_ERROR" + ErrAlertRulesQuotaReached ResourceErrorCode = "ALERT_RULES_QUOTA_REACHED" + ErrAlertRulesGroupQuotaReached ResourceErrorCode = "ALERT_RULES_GROUP_QUOTA_REACHED" + ErrDatasourceNameConflict ResourceErrorCode = "DATASOURCE_NAME_CONFLICT" + ErrDatasourceInvalidURL ResourceErrorCode = "DATASOURCE_INVALID_URL" + ErrDatasourceAlreadyManaged ResourceErrorCode = "DATASOURCE_ALREADY_MANAGED" + ErrFolderNameConflict ResourceErrorCode = "FOLDER_NAME_CONFLICT" + ErrDashboardAlreadyManaged ResourceErrorCode = "DASHBOARD_ALREADY_MANAGED" + ErrLibraryElementNameConflict ResourceErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" + ErrUnsupportedDataType ResourceErrorCode = "UNSUPPORTED_DATA_TYPE" + ErrResourceConflict ResourceErrorCode = "RESOURCE_CONFLICT" + ErrUnexpectedStatus ResourceErrorCode = "UNEXPECTED_STATUS_CODE" + ErrInternalServiceError ResourceErrorCode = "INTERNAL_SERVICE_ERROR" + ErrGeneric ResourceErrorCode = "GENERIC_ERROR" ) type SnapshotResourceStats struct { diff --git a/pkg/services/dashboards/dashboard_provisioning_mock.go b/pkg/services/dashboards/dashboard_provisioning_mock.go index 4b7863b27e6..016a20528f3 100644 --- a/pkg/services/dashboards/dashboard_provisioning_mock.go +++ b/pkg/services/dashboards/dashboard_provisioning_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dashboards diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index cba4d23820d..d20a9525622 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dashboards @@ -94,36 +94,6 @@ func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID return r0, r1 } -// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID -func (_m *FakeDashboardService) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { - ret := _m.Called(ctx, libraryPanelUID, orgID) - - if len(ret) == 0 { - panic("no return value specified for GetDashboardsByLibraryPanelUID") - } - - var r0 []*DashboardRef - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok { - return rf(ctx, libraryPanelUID, orgID) - } - if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok { - r0 = rf(ctx, libraryPanelUID, orgID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*DashboardRef) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { - r1 = rf(ctx, libraryPanelUID, orgID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // CountInFolders provides a mock function with given fields: ctx, orgID, folderUIDs, user func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) { ret := _m.Called(ctx, orgID, folderUIDs, user) @@ -368,6 +338,36 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDas return r0, r1 } +// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID +func (_m *FakeDashboardService) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { + ret := _m.Called(ctx, libraryPanelUID, orgID) + + if len(ret) == 0 { + panic("no return value specified for GetDashboardsByLibraryPanelUID") + } + + var r0 []*DashboardRef + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok { + return rf(ctx, libraryPanelUID, orgID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok { + r0 = rf(ctx, libraryPanelUID, orgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardRef) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, libraryPanelUID, orgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ImportDashboard provides a mock function with given fields: ctx, dto func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) { ret := _m.Called(ctx, dto) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 1a0b46c82cd..5b208c34b7f 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dashboards @@ -370,6 +370,36 @@ func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashb return r0, r1 } +// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID +func (_m *FakeDashboardStore) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { + ret := _m.Called(ctx, libraryPanelUID, orgID) + + if len(ret) == 0 { + panic("no return value specified for GetDashboardsByLibraryPanelUID") + } + + var r0 []*DashboardRef + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok { + return rf(ctx, libraryPanelUID, orgID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok { + r0 = rf(ctx, libraryPanelUID, orgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardRef) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, libraryPanelUID, orgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetDashboardsByPluginID provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) { ret := _m.Called(ctx, query) @@ -628,36 +658,6 @@ func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64 return r0 } -// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID -func (_m *FakeDashboardStore) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { - ret := _m.Called(ctx, libraryPanelUID, orgID) - - if len(ret) == 0 { - panic("no return value specified for GetDashboardsByLibraryPanelUID") - } - - var r0 []*DashboardRef - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok { - return rf(ctx, libraryPanelUID, orgID) - } - if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok { - r0 = rf(ctx, libraryPanelUID, orgID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*DashboardRef) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { - r1 = rf(ctx, libraryPanelUID, orgID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // ValidateDashboardBeforeSave provides a mock function with given fields: ctx, dashboard, overwrite func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) { ret := _m.Called(ctx, dashboard, overwrite) diff --git a/pkg/services/dashboardsnapshots/service_mock.go b/pkg/services/dashboardsnapshots/service_mock.go index 4d3ef270349..a323aa5d7b2 100644 --- a/pkg/services/dashboardsnapshots/service_mock.go +++ b/pkg/services/dashboardsnapshots/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.34.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dashboardsnapshots @@ -17,6 +17,10 @@ type MockService struct { func (_m *MockService) CreateDashboardSnapshot(_a0 context.Context, _a1 *CreateDashboardSnapshotCommand) (*DashboardSnapshot, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for CreateDashboardSnapshot") + } + var r0 *DashboardSnapshot var r1 error if rf, ok := ret.Get(0).(func(context.Context, *CreateDashboardSnapshotCommand) (*DashboardSnapshot, error)); ok { @@ -43,6 +47,10 @@ func (_m *MockService) CreateDashboardSnapshot(_a0 context.Context, _a1 *CreateD func (_m *MockService) DeleteDashboardSnapshot(_a0 context.Context, _a1 *DeleteDashboardSnapshotCommand) error { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for DeleteDashboardSnapshot") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteDashboardSnapshotCommand) error); ok { r0 = rf(_a0, _a1) @@ -57,6 +65,10 @@ func (_m *MockService) DeleteDashboardSnapshot(_a0 context.Context, _a1 *DeleteD func (_m *MockService) DeleteExpiredSnapshots(_a0 context.Context, _a1 *DeleteExpiredSnapshotsCommand) error { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for DeleteExpiredSnapshots") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteExpiredSnapshotsCommand) error); ok { r0 = rf(_a0, _a1) @@ -71,6 +83,10 @@ func (_m *MockService) DeleteExpiredSnapshots(_a0 context.Context, _a1 *DeleteEx func (_m *MockService) GetDashboardSnapshot(_a0 context.Context, _a1 *GetDashboardSnapshotQuery) (*DashboardSnapshot, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for GetDashboardSnapshot") + } + var r0 *DashboardSnapshot var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotQuery) (*DashboardSnapshot, error)); ok { @@ -97,6 +113,10 @@ func (_m *MockService) GetDashboardSnapshot(_a0 context.Context, _a1 *GetDashboa func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for SearchDashboardSnapshots") + } + var r0 DashboardSnapshotsList var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error)); ok { @@ -123,6 +143,10 @@ func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDas func (_m *MockService) ValidateDashboardExists(_a0 context.Context, _a1 int64, _a2 string) error { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for ValidateDashboardExists") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok { r0 = rf(_a0, _a1, _a2) diff --git a/pkg/services/extsvcauth/tests/extsvcregmock.go b/pkg/services/extsvcauth/tests/extsvcregmock.go index 82b0a278108..7ef96c6c36e 100644 --- a/pkg/services/extsvcauth/tests/extsvcregmock.go +++ b/pkg/services/extsvcauth/tests/extsvcregmock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.35.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package tests @@ -18,6 +18,10 @@ type ExternalServiceRegistryMock struct { func (_m *ExternalServiceRegistryMock) GetExternalServiceNames(ctx context.Context) ([]string, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for GetExternalServiceNames") + } + var r0 []string var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { @@ -44,6 +48,10 @@ func (_m *ExternalServiceRegistryMock) GetExternalServiceNames(ctx context.Conte func (_m *ExternalServiceRegistryMock) HasExternalService(ctx context.Context, name string) (bool, error) { ret := _m.Called(ctx, name) + if len(ret) == 0 { + panic("no return value specified for HasExternalService") + } + var r0 bool var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { @@ -68,6 +76,10 @@ func (_m *ExternalServiceRegistryMock) HasExternalService(ctx context.Context, n func (_m *ExternalServiceRegistryMock) RemoveExternalService(ctx context.Context, name string) error { ret := _m.Called(ctx, name) + if len(ret) == 0 { + panic("no return value specified for RemoveExternalService") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, name) @@ -82,6 +94,10 @@ func (_m *ExternalServiceRegistryMock) RemoveExternalService(ctx context.Context func (_m *ExternalServiceRegistryMock) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { ret := _m.Called(ctx, cmd) + if len(ret) == 0 { + panic("no return value specified for SaveExternalService") + } + var r0 *extsvcauth.ExternalService var r1 error if rf, ok := ret.Get(0).(func(context.Context, *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error)); ok { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index cb275abd33a..c6a388c9660 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -337,13 +337,6 @@ var ( RequiresRestart: true, Owner: grafanaAppPlatformSquad, }, - { - Name: "nanoGit", - Description: "Use experimental git library for provisioning", - Stage: FeatureStageExperimental, - RequiresRestart: true, - Owner: grafanaAppPlatformSquad, - }, { Name: "grafanaAPIServerEnsureKubectlAccess", Description: "Start an additional https handler and write kubectl options", @@ -1261,13 +1254,6 @@ var ( Owner: grafanaObservabilityTracesAndProfilingSquad, FrontendOnly: true, }, - { - Name: "jaegerBackendMigration", - Description: "Enables querying the Jaeger data source without the proxy", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOSSBigTent, - Expression: "true", - }, { Name: "alertingUIOptimizeReducer", Description: "Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query", @@ -1320,6 +1306,8 @@ var ( FrontendOnly: true, }, { + // Remove this flag once Loki v4 is released and the min supported version is v3.0+, + // since users on v2.9 need it to disable the feature, as it doesn't work for them. Name: "lokiLabelNamesQueryApi", Description: "Defaults to using the Loki `/labels` API instead of `/series`", Stage: FeatureStageGeneralAvailability, @@ -1714,7 +1702,7 @@ var ( Description: "Use fixed-width numbers globally in the UI", Stage: FeatureStageGeneralAvailability, Owner: grafanaFrontendPlatformSquad, - Expression: "true", + Expression: "false", }, { Name: "newInfluxDSConfigPageDesign", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index dffcd8bcad4..d071dbd8d91 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -43,7 +43,6 @@ mlExpressions,experimental,@grafana/alerting-squad,false,false,false datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false -nanoGit,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false @@ -165,7 +164,6 @@ prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,fal enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false enableSCIM,preview,@grafana/identity-access-team,false,false,false crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true -jaegerBackendMigration,GA,@grafana/oss-big-tent,false,false,false alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e105464df6a..9fbc4ee06fb 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -183,10 +183,6 @@ const ( // Next generation provisioning... and git FlagProvisioning = "provisioning" - // FlagNanoGit - // Use experimental git library for provisioning - FlagNanoGit = "nanoGit" - // FlagGrafanaAPIServerEnsureKubectlAccess // Start an additional https handler and write kubectl options FlagGrafanaAPIServerEnsureKubectlAccess = "grafanaAPIServerEnsureKubectlAccess" @@ -671,10 +667,6 @@ const ( // Enables browser crash detection reporting to Faro. FlagCrashDetection = "crashDetection" - // FlagJaegerBackendMigration - // Enables querying the Jaeger data source without the proxy - FlagJaegerBackendMigration = "jaegerBackendMigration" - // FlagAlertingUIOptimizeReducer // Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query FlagAlertingUIOptimizeReducer = "alertingUIOptimizeReducer" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 004b9c5e01b..e63024c3a52 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1542,6 +1542,7 @@ "name": "jaegerBackendMigration", "resourceVersion": "1751465665226", "creationTimestamp": "2024-11-15T14:40:20Z", + "deletionTimestamp": "2025-07-07T14:12:35Z", "annotations": { "grafana.app/updatedTimestamp": "2025-07-02 14:14:25.226989 +0000 UTC" } @@ -2048,7 +2049,8 @@ "metadata": { "name": "nanoGit", "resourceVersion": "1750434297879", - "creationTimestamp": "2025-06-17T17:07:30Z" + "creationTimestamp": "2025-06-17T17:07:30Z", + "deletionTimestamp": "2025-07-09T11:53:17Z" }, "spec": { "description": "Use experimental git library for provisioning", @@ -2943,17 +2945,17 @@ { "metadata": { "name": "tabularNumbers", - "resourceVersion": "1750754828442", + "resourceVersion": "1752078306230", "creationTimestamp": "2025-06-24T11:52:03Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-06-24 08:47:08.442537461 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-07-09 16:25:06.230922635 +0000 UTC" } }, "spec": { "description": "Use fixed-width numbers globally in the UI", "stage": "GA", "codeowner": "@grafana/grafana-frontend-platform", - "expression": "true" + "expression": "false" } }, { diff --git a/pkg/services/folder/foldertest/folder_store_mock.go b/pkg/services/folder/foldertest/folder_store_mock.go index 816dda5a4bc..1a88e59c4e0 100644 --- a/pkg/services/folder/foldertest/folder_store_mock.go +++ b/pkg/services/folder/foldertest/folder_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package foldertest @@ -14,10 +14,44 @@ type FakeFolderStore struct { mock.Mock } +// Get provides a mock function with given fields: ctx, q +func (_m *FakeFolderStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) { + ret := _m.Called(ctx, q) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *folder.Folder + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) (*folder.Folder, error)); ok { + return rf(ctx, q) + } + if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) *folder.Folder); ok { + r0 = rf(ctx, q) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*folder.Folder) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, folder.GetFolderQuery) error); ok { + r1 = rf(ctx, q) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetFolderByID provides a mock function with given fields: ctx, orgID, id func (_m *FakeFolderStore) GetFolderByID(ctx context.Context, orgID int64, id int64) (*folder.Folder, error) { ret := _m.Called(ctx, orgID, id) + if len(ret) == 0 { + panic("no return value specified for GetFolderByID") + } + var r0 *folder.Folder var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*folder.Folder, error)); ok { @@ -40,36 +74,14 @@ func (_m *FakeFolderStore) GetFolderByID(ctx context.Context, orgID int64, id in return r0, r1 } -// Get provides a mock function with given fields: ctx, query -func (_m *FakeFolderStore) Get(ctx context.Context, query folder.GetFolderQuery) (*folder.Folder, error) { - ret := _m.Called(ctx, query) - - var r0 *folder.Folder - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) (*folder.Folder, error)); ok { - return rf(ctx, query) - } - if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) *folder.Folder); ok { - r0 = rf(ctx, query) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*folder.Folder) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, folder.GetFolderQuery) error); ok { - r1 = rf(ctx, query) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetFolderByUID provides a mock function with given fields: ctx, orgID, uid func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid string) (*folder.Folder, error) { ret := _m.Called(ctx, orgID, uid) + if len(ret) == 0 { + panic("no return value specified for GetFolderByUID") + } + var r0 *folder.Folder var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*folder.Folder, error)); ok { @@ -96,6 +108,10 @@ func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid func (_m *FakeFolderStore) GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*folder.Folder, error) { ret := _m.Called(ctx, orgID, uids) + if len(ret) == 0 { + panic("no return value specified for GetFolders") + } + var r0 map[string]*folder.Folder var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, []string) (map[string]*folder.Folder, error)); ok { @@ -118,29 +134,6 @@ func (_m *FakeFolderStore) GetFolders(ctx context.Context, orgID int64, uids []s return r0, r1 } -func (_m *FakeFolderStore) CountInOrg(ctx context.Context, orgID int64) (int64, error) { - ret := _m.Called(ctx, orgID) - - var r0 int64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok { - return rf(ctx, orgID) - } - if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok { - r0 = rf(ctx, orgID) - } else { - r0 = ret.Get(0).(int64) - } - - if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { - r1 = rf(ctx, orgID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // NewFakeFolderStore creates a new instance of FakeFolderStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFakeFolderStore(t interface { diff --git a/pkg/services/frontend/frontend_service.go b/pkg/services/frontend/frontend_service.go index 81b67859ec2..b79876dd4c5 100644 --- a/pkg/services/frontend/frontend_service.go +++ b/pkg/services/frontend/frontend_service.go @@ -8,6 +8,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel" "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/log" @@ -15,6 +16,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/frontend") + type frontendService struct { *services.BasicService cfg *setting.Cfg diff --git a/pkg/services/frontend/index.go b/pkg/services/frontend/index.go index f7fdf617b35..ff18e6e5b7d 100644 --- a/pkg/services/frontend/index.go +++ b/pkg/services/frontend/index.go @@ -79,6 +79,9 @@ func NewIndexProvider(cfg *setting.Cfg, license licensing.Licensing) (*IndexProv } func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.Request) { + _, span := tracer.Start(request.Context(), "frontend.index.HandleRequest") + defer span.End() + if request.Method != "GET" { writer.WriteHeader(http.StatusMethodNotAllowed) return diff --git a/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go b/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go index 4400a37dfae..1a345e7be84 100644 --- a/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go +++ b/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go @@ -1,15 +1,17 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package eval_mocks import ( context "context" - time "time" backend "github.com/grafana/grafana-plugin-sdk-go/backend" - mock "github.com/stretchr/testify/mock" eval "github.com/grafana/grafana/pkg/services/ngalert/eval" + + mock "github.com/stretchr/testify/mock" + + time "time" ) // ConditionEvaluatorMock is an autogenerated mock type for the ConditionEvaluator type @@ -29,7 +31,15 @@ func (_m *ConditionEvaluatorMock) EXPECT() *ConditionEvaluatorMock_Expecter { func (_m *ConditionEvaluatorMock) Evaluate(ctx context.Context, now time.Time) (eval.Results, error) { ret := _m.Called(ctx, now) + if len(ret) == 0 { + panic("no return value specified for Evaluate") + } + var r0 eval.Results + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, time.Time) (eval.Results, error)); ok { + return rf(ctx, now) + } if rf, ok := ret.Get(0).(func(context.Context, time.Time) eval.Results); ok { r0 = rf(ctx, now) } else { @@ -38,7 +48,6 @@ func (_m *ConditionEvaluatorMock) Evaluate(ctx context.Context, now time.Time) ( } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, time.Time) error); ok { r1 = rf(ctx, now) } else { @@ -56,7 +65,7 @@ type ConditionEvaluatorMock_Evaluate_Call struct { // Evaluate is a helper method to define mock.On call // - ctx context.Context // - now time.Time -func (_e *ConditionEvaluatorMock_Expecter) Evaluate(ctx any, now any) *ConditionEvaluatorMock_Evaluate_Call { +func (_e *ConditionEvaluatorMock_Expecter) Evaluate(ctx interface{}, now interface{}) *ConditionEvaluatorMock_Evaluate_Call { return &ConditionEvaluatorMock_Evaluate_Call{Call: _e.mock.On("Evaluate", ctx, now)} } @@ -72,11 +81,24 @@ func (_c *ConditionEvaluatorMock_Evaluate_Call) Return(_a0 eval.Results, _a1 err return _c } +func (_c *ConditionEvaluatorMock_Evaluate_Call) RunAndReturn(run func(context.Context, time.Time) (eval.Results, error)) *ConditionEvaluatorMock_Evaluate_Call { + _c.Call.Return(run) + return _c +} + // EvaluateRaw provides a mock function with given fields: ctx, now func (_m *ConditionEvaluatorMock) EvaluateRaw(ctx context.Context, now time.Time) (*backend.QueryDataResponse, error) { ret := _m.Called(ctx, now) + if len(ret) == 0 { + panic("no return value specified for EvaluateRaw") + } + var r0 *backend.QueryDataResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, time.Time) (*backend.QueryDataResponse, error)); ok { + return rf(ctx, now) + } if rf, ok := ret.Get(0).(func(context.Context, time.Time) *backend.QueryDataResponse); ok { r0 = rf(ctx, now) } else { @@ -85,7 +107,6 @@ func (_m *ConditionEvaluatorMock) EvaluateRaw(ctx context.Context, now time.Time } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, time.Time) error); ok { r1 = rf(ctx, now) } else { @@ -103,7 +124,7 @@ type ConditionEvaluatorMock_EvaluateRaw_Call struct { // EvaluateRaw is a helper method to define mock.On call // - ctx context.Context // - now time.Time -func (_e *ConditionEvaluatorMock_Expecter) EvaluateRaw(ctx any, now any) *ConditionEvaluatorMock_EvaluateRaw_Call { +func (_e *ConditionEvaluatorMock_Expecter) EvaluateRaw(ctx interface{}, now interface{}) *ConditionEvaluatorMock_EvaluateRaw_Call { return &ConditionEvaluatorMock_EvaluateRaw_Call{Call: _e.mock.On("EvaluateRaw", ctx, now)} } @@ -119,13 +140,17 @@ func (_c *ConditionEvaluatorMock_EvaluateRaw_Call) Return(resp *backend.QueryDat return _c } -type mockConstructorTestingTNewConditionEvaluatorMock interface { - mock.TestingT - Cleanup(func()) +func (_c *ConditionEvaluatorMock_EvaluateRaw_Call) RunAndReturn(run func(context.Context, time.Time) (*backend.QueryDataResponse, error)) *ConditionEvaluatorMock_EvaluateRaw_Call { + _c.Call.Return(run) + return _c } // NewConditionEvaluatorMock creates a new instance of ConditionEvaluatorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewConditionEvaluatorMock(t mockConstructorTestingTNewConditionEvaluatorMock) *ConditionEvaluatorMock { +// The first argument is typically a *testing.T value. +func NewConditionEvaluatorMock(t interface { + mock.TestingT + Cleanup(func()) +}) *ConditionEvaluatorMock { mock := &ConditionEvaluatorMock{} mock.Mock.Test(t) diff --git a/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go b/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go index a3419fdc508..20f4e14d0ab 100644 --- a/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.44.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package alertmanager_mock @@ -585,7 +585,7 @@ func (_c *AlertmanagerMock_PutAlerts_Call) RunAndReturn(run func(context.Context return _c } -// Ready provides a mock function with given fields: +// Ready provides a mock function with no fields func (_m *AlertmanagerMock) Ready() bool { ret := _m.Called() @@ -781,7 +781,7 @@ func (_c *AlertmanagerMock_SilenceState_Call) RunAndReturn(run func(context.Cont return _c } -// StopAndWait provides a mock function with given fields: +// StopAndWait provides a mock function with no fields func (_m *AlertmanagerMock) StopAndWait() { _m.Called() } @@ -809,7 +809,7 @@ func (_c *AlertmanagerMock_StopAndWait_Call) Return() *AlertmanagerMock_StopAndW } func (_c *AlertmanagerMock_StopAndWait_Call) RunAndReturn(run func()) *AlertmanagerMock_StopAndWait_Call { - _c.Call.Return(run) + _c.Run(run) return _c } diff --git a/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go b/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go index 03b5f825775..03b7b893757 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go +++ b/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.44.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package legacy_storage diff --git a/pkg/services/ngalert/provisioning/provisioning_store_mock.go b/pkg/services/ngalert/provisioning/provisioning_store_mock.go index 65df0062a8a..31cc77e26a6 100644 --- a/pkg/services/ngalert/provisioning/provisioning_store_mock.go +++ b/pkg/services/ngalert/provisioning/provisioning_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.34.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package provisioning @@ -26,6 +26,10 @@ func (_m *MockProvisioningStore) EXPECT() *MockProvisioningStore_Expecter { func (_m *MockProvisioningStore) DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error { ret := _m.Called(ctx, o, org) + if len(ret) == 0 { + panic("no return value specified for DeleteProvenance") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64) error); ok { r0 = rf(ctx, o, org) @@ -70,6 +74,10 @@ func (_c *MockProvisioningStore_DeleteProvenance_Call) RunAndReturn(run func(con func (_m *MockProvisioningStore) GetProvenance(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) { ret := _m.Called(ctx, o, org) + if len(ret) == 0 { + panic("no return value specified for GetProvenance") + } + var r0 models.Provenance var r1 error if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64) (models.Provenance, error)); ok { @@ -124,6 +132,10 @@ func (_c *MockProvisioningStore_GetProvenance_Call) RunAndReturn(run func(contex func (_m *MockProvisioningStore) GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error) { ret := _m.Called(ctx, org, resourceType) + if len(ret) == 0 { + panic("no return value specified for GetProvenances") + } + var r0 map[string]models.Provenance var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) (map[string]models.Provenance, error)); ok { @@ -180,6 +192,10 @@ func (_c *MockProvisioningStore_GetProvenances_Call) RunAndReturn(run func(conte func (_m *MockProvisioningStore) SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error { ret := _m.Called(ctx, o, org, p) + if len(ret) == 0 { + panic("no return value specified for SetProvenance") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64, models.Provenance) error); ok { r0 = rf(ctx, o, org, p) diff --git a/pkg/services/ngalert/provisioning/quota_checker_mock.go b/pkg/services/ngalert/provisioning/quota_checker_mock.go index 596e6c92d61..0639fccf984 100644 --- a/pkg/services/ngalert/provisioning/quota_checker_mock.go +++ b/pkg/services/ngalert/provisioning/quota_checker_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.34.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package provisioning @@ -26,6 +26,10 @@ func (_m *MockQuotaChecker) EXPECT() *MockQuotaChecker_Expecter { func (_m *MockQuotaChecker) CheckQuotaReached(ctx context.Context, target quota.TargetSrv, scopeParams *quota.ScopeParameters) (bool, error) { ret := _m.Called(ctx, target, scopeParams) + if len(ret) == 0 { + panic("no return value specified for CheckQuotaReached") + } + var r0 bool var r1 error if rf, ok := ret.Get(0).(func(context.Context, quota.TargetSrv, *quota.ScopeParameters) (bool, error)); ok { diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 56cf9d8ec26..a5850cb4b0c 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -293,18 +293,20 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config if err := am.mergeExtraConfigs(ctx, decryptedCfg); err != nil { return fmt.Errorf("unable to merge extra configurations: %w", err) } - rawDecrypted, err := json.Marshal(decryptedCfg) + payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg) + rawPayload, err := json.Marshal(payload) if err != nil { return fmt.Errorf("unable to marshal decrypted configuration: %w", err) } - configHash := md5.Sum(rawDecrypted) + + configHash := md5.Sum(rawPayload) // Send the configuration only if we need to. if !am.shouldSendConfig(ctx, configHash) { return nil } - return am.sendConfiguration(ctx, decryptedCfg, fmt.Sprintf("%x", configHash), config.CreatedAt, am.isDefaultConfiguration(configHash)) + return am.sendConfiguration(ctx, payload, fmt.Sprintf("%x", configHash), config.CreatedAt, am.isDefaultConfiguration(configHash)) } func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) bool { @@ -370,11 +372,11 @@ func (am *Alertmanager) mergeExtraConfigs(ctx context.Context, config *apimodels return nil } -func (am *Alertmanager) sendConfiguration(ctx context.Context, decrypted *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error { +func (am *Alertmanager) sendConfiguration(ctx context.Context, cfg *remoteClient.GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error { am.metrics.ConfigSyncsTotal.Inc() if err := am.mimirClient.CreateGrafanaAlertmanagerConfig( ctx, - decrypted, + cfg, hash, createdAt, isDefault, @@ -420,14 +422,14 @@ func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P if err := am.mergeExtraConfigs(ctx, decryptedCfg); err != nil { return fmt.Errorf("unable to merge extra configurations: %w", err) } - - rawCfg, err := json.Marshal(decryptedCfg) + payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg) + rawCfg, err := json.Marshal(payload) if err != nil { return err } hash := fmt.Sprintf("%x", md5.Sum(rawCfg)) - return am.sendConfiguration(ctx, decryptedCfg, hash, time.Now().Unix(), false) + return am.sendConfiguration(ctx, payload, hash, time.Now().Unix(), false) } // SaveAndApplyDefaultConfig sends the default Grafana Alertmanager configuration to the remote Alertmanager. @@ -446,10 +448,17 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error { return err } + payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg) + rawCfg, err := json.Marshal(payload) + if err != nil { + return err + } + hash := fmt.Sprintf("%x", md5.Sum(rawCfg)) + return am.sendConfiguration( ctx, - decryptedCfg, - am.defaultConfigHash, + payload, + hash, time.Now().Unix(), true, ) diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 3bbcb03de6b..27ec3f5a83d 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -3,6 +3,7 @@ package remote import ( "context" "crypto/md5" + "embed" "encoding/base64" "encoding/json" "errors" @@ -11,6 +12,7 @@ import ( "net/http" "net/http/httptest" "os" + "path" "slices" "strings" "testing" @@ -47,6 +49,9 @@ import ( "github.com/grafana/grafana/pkg/util" ) +//go:embed test-data/*.* +var testData embed.FS + var ( defaultGrafanaConfig = setting.GetAlertmanagerDefaultConfiguration() errTest = errors.New("test") @@ -357,12 +362,14 @@ func TestCompareAndSendConfiguration(t *testing.T) { testGrafanaConfigWithBadEncryption, err := json.Marshal(inputCfg) require.NoError(t, err) - cfgWithDecryptedSecret, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) + test, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) require.NoError(t, err) + cfgWithDecryptedSecret := PostableUserConfigToGrafanaAlertmanagerConfig(test) - cfgWithAutogenRoutes, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) + testAutogenRoutes, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) require.NoError(t, err) - require.NoError(t, testAutogenFn(nil, nil, 0, &cfgWithAutogenRoutes.AlertmanagerConfig, false)) + require.NoError(t, testAutogenFn(nil, nil, 0, &testAutogenRoutes.AlertmanagerConfig, false)) + cfgWithAutogenRoutes := PostableUserConfigToGrafanaAlertmanagerConfig(testAutogenRoutes) // Calculate hashes for expected configurations cfgWithDecryptedSecretBytes, err := json.Marshal(cfgWithDecryptedSecret) @@ -373,6 +380,20 @@ func TestCompareAndSendConfiguration(t *testing.T) { require.NoError(t, err) cfgWithAutogenRoutesHash := fmt.Sprintf("%x", md5.Sum(cfgWithAutogenRoutesBytes)) + cfgWithExtraUnmergedBytes, err := testData.ReadFile(path.Join("test-data", "config-with-extra.json")) + require.NoError(t, err) + cfgWithExtraUnmerged, err := notifier.Load(cfgWithExtraUnmergedBytes) + require.NoError(t, err) + r, err := cfgWithExtraUnmerged.GetMergedAlertmanagerConfig() + require.NoError(t, err) + cfgWithExtraMerged := &client.GrafanaAlertmanagerConfig{ + TemplateFiles: cfgWithExtraUnmerged.TemplateFiles, + AlertmanagerConfig: r.Config, + } + cfgWithExtraMergedBytes, err := json.Marshal(cfgWithExtraMerged) + require.NoError(t, err) + cfgWithExtraMergedHash := fmt.Sprintf("%x", md5.Sum(cfgWithExtraMergedBytes)) + tests := []struct { name string config string @@ -428,6 +449,15 @@ func TestCompareAndSendConfiguration(t *testing.T) { }, nil, }, + { + name: "no error, with extra configurations", + config: string(cfgWithExtraUnmergedBytes), + autogenFn: NoopAutogenFn, + expCfg: &client.UserGrafanaConfig{ + GrafanaAlertmanagerConfig: cfgWithExtraMerged, + Hash: cfgWithExtraMergedHash, + }, + }, } for _, test := range tests { @@ -677,7 +707,7 @@ func TestCompareAndSendConfigurationWithExtraConfigs(t *testing.T) { // Return an empty config to ensure it gets replaced w.Header().Add("content-type", "application/json") require.NoError(t, json.NewEncoder(w).Encode(client.UserGrafanaConfig{ - GrafanaAlertmanagerConfig: &apimodels.PostableUserConfig{}, + GrafanaAlertmanagerConfig: &client.GrafanaAlertmanagerConfig{}, })) return } diff --git a/pkg/services/ngalert/remote/client/alertmanager_configuration.go b/pkg/services/ngalert/remote/client/alertmanager_configuration.go index 164a3949011..35239700ea7 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_configuration.go +++ b/pkg/services/ngalert/remote/client/alertmanager_configuration.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/grafana/alerting/definition" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -15,14 +16,25 @@ const ( grafanaAlertmanagerReceiversPath = "/api/v1/grafana/receivers" ) +type GrafanaAlertmanagerConfig struct { + TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` + AlertmanagerConfig definition.PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"` +} + +func (u *GrafanaAlertmanagerConfig) MarshalJSON() ([]byte, error) { + // this is special marshaling that makes sure that secrets are not masked + type cfg GrafanaAlertmanagerConfig + return definition.MarshalJSONWithSecrets((*cfg)(u)) +} + type UserGrafanaConfig struct { - GrafanaAlertmanagerConfig *apimodels.PostableUserConfig `json:"configuration"` - Hash string `json:"configuration_hash"` - CreatedAt int64 `json:"created"` - Default bool `json:"default"` - Promoted bool `json:"promoted"` - ExternalURL string `json:"external_url"` - SmtpConfig SmtpConfig `json:"smtp_config"` + GrafanaAlertmanagerConfig *GrafanaAlertmanagerConfig `json:"configuration"` + Hash string `json:"configuration_hash"` + CreatedAt int64 `json:"created"` + Default bool `json:"default"` + Promoted bool `json:"promoted"` + ExternalURL string `json:"external_url"` + SmtpConfig SmtpConfig `json:"smtp_config"` // TODO: Remove once everything can be sent in the 'SmtpConfig' field. SmtpFrom string `json:"smtp_from"` @@ -52,7 +64,7 @@ func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafana return gc, nil } -func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error { +func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error { payload, err := definition.MarshalJSONWithSecrets(&UserGrafanaConfig{ GrafanaAlertmanagerConfig: cfg, Hash: hash, diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index 931e2f7aa3d..61075dc1df4 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -29,7 +29,7 @@ type MimirClient interface { DeleteGrafanaAlertmanagerState(ctx context.Context) error GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) - CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error + CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error DeleteGrafanaAlertmanagerConfig(ctx context.Context) error TestTemplate(ctx context.Context, c alertingNotify.TestTemplatesConfigBodyParams) (*alertingNotify.TestTemplatesResults, error) diff --git a/pkg/services/ngalert/remote/compat.go b/pkg/services/ngalert/remote/compat.go new file mode 100644 index 00000000000..8914bc96728 --- /dev/null +++ b/pkg/services/ngalert/remote/compat.go @@ -0,0 +1,13 @@ +package remote + +import ( + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/remote/client" +) + +func PostableUserConfigToGrafanaAlertmanagerConfig(config *definitions.PostableUserConfig) *client.GrafanaAlertmanagerConfig { + return &client.GrafanaAlertmanagerConfig{ + TemplateFiles: config.TemplateFiles, + AlertmanagerConfig: config.AlertmanagerConfig, + } +} diff --git a/pkg/services/ngalert/remote/mock/remoteAlertmanager.go b/pkg/services/ngalert/remote/mock/remoteAlertmanager.go index 09cfb04bc67..33a77c10f1c 100644 --- a/pkg/services/ngalert/remote/mock/remoteAlertmanager.go +++ b/pkg/services/ngalert/remote/mock/remoteAlertmanager.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.44.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package alertmanager_mock @@ -125,52 +125,6 @@ func (_c *RemoteAlertmanagerMock_CompareAndSendConfiguration_Call) RunAndReturn( return _c } -// SendState provides a mock function with given fields: _a0 -func (_m *RemoteAlertmanagerMock) SendState(_a0 context.Context) error { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for SendState") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// RemoteAlertmanagerMock_SendState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendState' -type RemoteAlertmanagerMock_SendState_Call struct { - *mock.Call -} - -// SendState is a helper method to define mock.On call -// - _a0 context.Context -func (_e *RemoteAlertmanagerMock_Expecter) SendState(_a0 interface{}) *RemoteAlertmanagerMock_SendState_Call { - return &RemoteAlertmanagerMock_SendState_Call{Call: _e.mock.On("SendState", _a0)} -} - -func (_c *RemoteAlertmanagerMock_SendState_Call) Run(run func(_a0 context.Context)) *RemoteAlertmanagerMock_SendState_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *RemoteAlertmanagerMock_SendState_Call) Return(_a0 error) *RemoteAlertmanagerMock_SendState_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *RemoteAlertmanagerMock_SendState_Call) RunAndReturn(run func(context.Context) error) *RemoteAlertmanagerMock_SendState_Call { - _c.Call.Return(run) - return _c -} - // CreateSilence provides a mock function with given fields: _a0, _a1 func (_m *RemoteAlertmanagerMock) CreateSilence(_a0 context.Context, _a1 *v2models.PostableSilence) (string, error) { ret := _m.Called(_a0, _a1) @@ -678,7 +632,7 @@ func (_c *RemoteAlertmanagerMock_PutAlerts_Call) RunAndReturn(run func(context.C return _c } -// Ready provides a mock function with given fields: +// Ready provides a mock function with no fields func (_m *RemoteAlertmanagerMock) Ready() bool { ret := _m.Called() @@ -816,6 +770,52 @@ func (_c *RemoteAlertmanagerMock_SaveAndApplyDefaultConfig_Call) RunAndReturn(ru return _c } +// SendState provides a mock function with given fields: _a0 +func (_m *RemoteAlertmanagerMock) SendState(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for SendState") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoteAlertmanagerMock_SendState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendState' +type RemoteAlertmanagerMock_SendState_Call struct { + *mock.Call +} + +// SendState is a helper method to define mock.On call +// - _a0 context.Context +func (_e *RemoteAlertmanagerMock_Expecter) SendState(_a0 interface{}) *RemoteAlertmanagerMock_SendState_Call { + return &RemoteAlertmanagerMock_SendState_Call{Call: _e.mock.On("SendState", _a0)} +} + +func (_c *RemoteAlertmanagerMock_SendState_Call) Run(run func(_a0 context.Context)) *RemoteAlertmanagerMock_SendState_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *RemoteAlertmanagerMock_SendState_Call) Return(_a0 error) *RemoteAlertmanagerMock_SendState_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RemoteAlertmanagerMock_SendState_Call) RunAndReturn(run func(context.Context) error) *RemoteAlertmanagerMock_SendState_Call { + _c.Call.Return(run) + return _c +} + // SilenceState provides a mock function with given fields: _a0 func (_m *RemoteAlertmanagerMock) SilenceState(_a0 context.Context) (notify.SilenceState, error) { ret := _m.Called(_a0) @@ -874,7 +874,7 @@ func (_c *RemoteAlertmanagerMock_SilenceState_Call) RunAndReturn(run func(contex return _c } -// StopAndWait provides a mock function with given fields: +// StopAndWait provides a mock function with no fields func (_m *RemoteAlertmanagerMock) StopAndWait() { _m.Called() } @@ -902,7 +902,7 @@ func (_c *RemoteAlertmanagerMock_StopAndWait_Call) Return() *RemoteAlertmanagerM } func (_c *RemoteAlertmanagerMock_StopAndWait_Call) RunAndReturn(run func()) *RemoteAlertmanagerMock_StopAndWait_Call { - _c.Call.Return(run) + _c.Run(run) return _c } diff --git a/pkg/services/ngalert/remote/test-data/config-with-extra.json b/pkg/services/ngalert/remote/test-data/config-with-extra.json new file mode 100644 index 00000000000..4f15ddfc590 --- /dev/null +++ b/pkg/services/ngalert/remote/test-data/config-with-extra.json @@ -0,0 +1,41 @@ +{ + "template_files": { + "test": "{{ define \"my_templ\" }}TEST{{ end }}" + }, + "alertmanager_config": { + "route": { + "receiver": "grafana-default-email", + "group_by": [ + "grafana_folder", + "alertname" + ] + }, + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "", + "name": "some other name", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "" + } + } + ] + } + ] + }, + "extra_config": [ + { + "identifier": "imported", + "merge_matchers": ["imported=\"true\""], + "template_files": + { + "extra_template": "{{ define \"my_message\" }}TEST{{ end }}" + }, + "alertmanager_config": "{\"receivers\":[{\"webhook_configs\":[{\"url\":\"http://localhost\"}],\"name\":\"webhook\"}],\"route\":{\"receiver\":\"webhook\"}}" + } + ] +} \ No newline at end of file diff --git a/pkg/services/ngalert/schedule/alerts_sender_mock.go b/pkg/services/ngalert/schedule/alerts_sender_mock.go index 75a9e2ecc5e..6ba9367193e 100644 --- a/pkg/services/ngalert/schedule/alerts_sender_mock.go +++ b/pkg/services/ngalert/schedule/alerts_sender_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.34.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package schedule @@ -55,7 +55,7 @@ func (_c *AlertsSenderMock_Send_Call) Return() *AlertsSenderMock_Send_Call { } func (_c *AlertsSenderMock_Send_Call) RunAndReturn(run func(context.Context, models.AlertRuleKey, definitions.PostableAlerts)) *AlertsSenderMock_Send_Call { - _c.Call.Return(run) + _c.Run(run) return _c } diff --git a/pkg/services/ngalert/store/admin_configuration_store_mock.go b/pkg/services/ngalert/store/admin_configuration_store_mock.go index 4dfc88ff5cc..080a185ef5b 100644 --- a/pkg/services/ngalert/store/admin_configuration_store_mock.go +++ b/pkg/services/ngalert/store/admin_configuration_store_mock.go @@ -1,11 +1,10 @@ -// Code generated by mockery v2.10.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package store import ( - mock "github.com/stretchr/testify/mock" - models "github.com/grafana/grafana/pkg/services/ngalert/models" + mock "github.com/stretchr/testify/mock" ) // AdminConfigurationStoreMock is an autogenerated mock type for the AdminConfigurationStore type @@ -25,6 +24,10 @@ func (_m *AdminConfigurationStoreMock) EXPECT() *AdminConfigurationStoreMock_Exp func (_m *AdminConfigurationStoreMock) DeleteAdminConfiguration(orgID int64) error { ret := _m.Called(orgID) + if len(ret) == 0 { + panic("no return value specified for DeleteAdminConfiguration") + } + var r0 error if rf, ok := ret.Get(0).(func(int64) error); ok { r0 = rf(orgID) @@ -42,7 +45,7 @@ type AdminConfigurationStoreMock_DeleteAdminConfiguration_Call struct { // DeleteAdminConfiguration is a helper method to define mock.On call // - orgID int64 -func (_e *AdminConfigurationStoreMock_Expecter) DeleteAdminConfiguration(orgID any) *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call { +func (_e *AdminConfigurationStoreMock_Expecter) DeleteAdminConfiguration(orgID interface{}) *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call { return &AdminConfigurationStoreMock_DeleteAdminConfiguration_Call{Call: _e.mock.On("DeleteAdminConfiguration", orgID)} } @@ -58,11 +61,24 @@ func (_c *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call) Return(_a0 return _c } +func (_c *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call) RunAndReturn(run func(int64) error) *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call { + _c.Call.Return(run) + return _c +} + // GetAdminConfiguration provides a mock function with given fields: orgID func (_m *AdminConfigurationStoreMock) GetAdminConfiguration(orgID int64) (*models.AdminConfiguration, error) { ret := _m.Called(orgID) + if len(ret) == 0 { + panic("no return value specified for GetAdminConfiguration") + } + var r0 *models.AdminConfiguration + var r1 error + if rf, ok := ret.Get(0).(func(int64) (*models.AdminConfiguration, error)); ok { + return rf(orgID) + } if rf, ok := ret.Get(0).(func(int64) *models.AdminConfiguration); ok { r0 = rf(orgID) } else { @@ -71,7 +87,6 @@ func (_m *AdminConfigurationStoreMock) GetAdminConfiguration(orgID int64) (*mode } } - var r1 error if rf, ok := ret.Get(1).(func(int64) error); ok { r1 = rf(orgID) } else { @@ -88,7 +103,7 @@ type AdminConfigurationStoreMock_GetAdminConfiguration_Call struct { // GetAdminConfiguration is a helper method to define mock.On call // - orgID int64 -func (_e *AdminConfigurationStoreMock_Expecter) GetAdminConfiguration(orgID any) *AdminConfigurationStoreMock_GetAdminConfiguration_Call { +func (_e *AdminConfigurationStoreMock_Expecter) GetAdminConfiguration(orgID interface{}) *AdminConfigurationStoreMock_GetAdminConfiguration_Call { return &AdminConfigurationStoreMock_GetAdminConfiguration_Call{Call: _e.mock.On("GetAdminConfiguration", orgID)} } @@ -104,11 +119,24 @@ func (_c *AdminConfigurationStoreMock_GetAdminConfiguration_Call) Return(_a0 *mo return _c } -// GetAdminConfigurations provides a mock function with given fields: +func (_c *AdminConfigurationStoreMock_GetAdminConfiguration_Call) RunAndReturn(run func(int64) (*models.AdminConfiguration, error)) *AdminConfigurationStoreMock_GetAdminConfiguration_Call { + _c.Call.Return(run) + return _c +} + +// GetAdminConfigurations provides a mock function with no fields func (_m *AdminConfigurationStoreMock) GetAdminConfigurations() ([]*models.AdminConfiguration, error) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for GetAdminConfigurations") + } + var r0 []*models.AdminConfiguration + var r1 error + if rf, ok := ret.Get(0).(func() ([]*models.AdminConfiguration, error)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() []*models.AdminConfiguration); ok { r0 = rf() } else { @@ -117,7 +145,6 @@ func (_m *AdminConfigurationStoreMock) GetAdminConfigurations() ([]*models.Admin } } - var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { @@ -149,10 +176,19 @@ func (_c *AdminConfigurationStoreMock_GetAdminConfigurations_Call) Return(_a0 [] return _c } +func (_c *AdminConfigurationStoreMock_GetAdminConfigurations_Call) RunAndReturn(run func() ([]*models.AdminConfiguration, error)) *AdminConfigurationStoreMock_GetAdminConfigurations_Call { + _c.Call.Return(run) + return _c +} + // UpdateAdminConfiguration provides a mock function with given fields: _a0 func (_m *AdminConfigurationStoreMock) UpdateAdminConfiguration(_a0 UpdateAdminConfigurationCmd) error { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for UpdateAdminConfiguration") + } + var r0 error if rf, ok := ret.Get(0).(func(UpdateAdminConfigurationCmd) error); ok { r0 = rf(_a0) @@ -170,7 +206,7 @@ type AdminConfigurationStoreMock_UpdateAdminConfiguration_Call struct { // UpdateAdminConfiguration is a helper method to define mock.On call // - _a0 UpdateAdminConfigurationCmd -func (_e *AdminConfigurationStoreMock_Expecter) UpdateAdminConfiguration(_a0 any) *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call { +func (_e *AdminConfigurationStoreMock_Expecter) UpdateAdminConfiguration(_a0 interface{}) *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call { return &AdminConfigurationStoreMock_UpdateAdminConfiguration_Call{Call: _e.mock.On("UpdateAdminConfiguration", _a0)} } @@ -185,3 +221,22 @@ func (_c *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call) Return(_a0 _c.Call.Return(_a0) return _c } + +func (_c *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call) RunAndReturn(run func(UpdateAdminConfigurationCmd) error) *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call { + _c.Call.Return(run) + return _c +} + +// NewAdminConfigurationStoreMock creates a new instance of AdminConfigurationStoreMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAdminConfigurationStoreMock(t interface { + mock.TestingT + Cleanup(func()) +}) *AdminConfigurationStoreMock { + mock := &AdminConfigurationStoreMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/oauthtoken/oauthtokentest/service_mock.go b/pkg/services/oauthtoken/oauthtokentest/service_mock.go index 925432a6190..e1643e0e848 100644 --- a/pkg/services/oauthtoken/oauthtokentest/service_mock.go +++ b/pkg/services/oauthtoken/oauthtokentest/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package oauthtokentest diff --git a/pkg/services/org/orgtest/mock.go b/pkg/services/org/orgtest/mock.go index 7dc2b16d34d..ea598a2be0c 100644 --- a/pkg/services/org/orgtest/mock.go +++ b/pkg/services/org/orgtest/mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package orgtest @@ -62,24 +62,6 @@ func (_m *MockService) CreateWithMember(_a0 context.Context, _a1 *org.CreateOrgC return r0, r1 } -// Delete provides a mock function with given fields: _a0, _a1 -func (_m *MockService) Delete(_a0 context.Context, _a1 *org.DeleteOrgCommand) error { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *org.DeleteOrgCommand) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // DeleteUserFromAll provides a mock function with given fields: _a0, _a1 func (_m *MockService) DeleteUserFromAll(_a0 context.Context, _a1 int64) error { ret := _m.Called(_a0, _a1) diff --git a/pkg/services/pluginsintegration/advisor/advisor.go b/pkg/services/pluginsintegration/advisor/advisor.go index c9782de28bf..98989679aa8 100644 --- a/pkg/services/pluginsintegration/advisor/advisor.go +++ b/pkg/services/pluginsintegration/advisor/advisor.go @@ -86,7 +86,7 @@ func (s *Service) ReportSummary(ctx context.Context) (*ReportInfo, error) { latestDatasourceCheck := findLatestCheck(checkList.GetItems(), datasourcecheck.CheckID) reportInfo := &ReportInfo{} if latestPluginCheck != nil { - for _, failure := range latestPluginCheck.CheckStatus.Report.Failures { + for _, failure := range latestPluginCheck.Status.Report.Failures { switch failure.StepID { case plugincheck.UpdateStepID: reportInfo.PluginsOutdated++ @@ -96,7 +96,7 @@ func (s *Service) ReportSummary(ctx context.Context) (*ReportInfo, error) { } } if latestDatasourceCheck != nil { - for _, failure := range latestDatasourceCheck.CheckStatus.Report.Failures { + for _, failure := range latestDatasourceCheck.Status.Report.Failures { if failure.StepID == datasourcecheck.HealthCheckStepID { reportInfo.DatasourcesUnhealthy++ } diff --git a/pkg/services/pluginsintegration/advisor/advisor_test.go b/pkg/services/pluginsintegration/advisor/advisor_test.go index c9c9b8d1609..5fce5f23f35 100644 --- a/pkg/services/pluginsintegration/advisor/advisor_test.go +++ b/pkg/services/pluginsintegration/advisor/advisor_test.go @@ -41,8 +41,8 @@ func TestService_ReportSummary(t *testing.T) { checks.TypeLabel: plugincheck.CheckID, }, }, - CheckStatus: advisorv0alpha1.CheckStatus{ - Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Status: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckReport{ Failures: []advisorv0alpha1.CheckReportFailure{ {StepID: plugincheck.UpdateStepID}, }, @@ -56,8 +56,8 @@ func TestService_ReportSummary(t *testing.T) { checks.TypeLabel: plugincheck.CheckID, }, }, - CheckStatus: advisorv0alpha1.CheckStatus{ - Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Status: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckReport{ Failures: []advisorv0alpha1.CheckReportFailure{ {StepID: plugincheck.UpdateStepID}, {StepID: plugincheck.DeprecationStepID}, @@ -72,8 +72,8 @@ func TestService_ReportSummary(t *testing.T) { checks.TypeLabel: datasourcecheck.CheckID, }, }, - CheckStatus: advisorv0alpha1.CheckStatus{ - Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Status: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckReport{ Failures: []advisorv0alpha1.CheckReportFailure{ {StepID: datasourcecheck.HealthCheckStepID}, {StepID: datasourcecheck.HealthCheckStepID}, diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index bca3a42339b..727eb8d2952 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -83,8 +83,8 @@ func TestLoader_Load(t *testing.T) { Description: "Data source for Amazon AWS monitoring service", Keywords: []string{"aws", "amazon"}, Logos: plugins.Logos{ - Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Small: "public/plugins/cloudwatch/img/amazon-web-services.png", + Large: "public/plugins/cloudwatch/img/amazon-web-services.png", }, Links: []plugins.InfoLink{ {Name: "Raise issue", URL: "https://github.com/grafana/grafana/issues/new"}, @@ -121,7 +121,7 @@ func TestLoader_Load(t *testing.T) { QueryOptions: map[string]bool{"minInterval": true}, }, Module: "core:plugin/cloudwatch", - BaseURL: "public/app/plugins/datasource/cloudwatch", + BaseURL: "public/plugins/cloudwatch", FS: mustNewStaticFSForTests(t, filepath.Join(corePluginDir(t), "app/plugins/datasource/cloudwatch")), Signature: plugins.SignatureStatusInternal, Class: plugins.ClassCore, @@ -1325,8 +1325,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Module: "public/plugins/test-datasource/nested/module.js", - BaseURL: "public/plugins/test-datasource/nested", + Module: "public/plugins/test-panel/module.js", + BaseURL: "public/plugins/test-panel", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, @@ -1497,8 +1497,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { {Name: "License", URL: "https://github.com/grafana/grafana-starter-panel/blob/master/LICENSE"}, }, Logos: plugins.Logos{ - Small: "public/plugins/myorgid-simple-app/child/img/logo.svg", - Large: "public/plugins/myorgid-simple-app/child/img/logo.svg", + Small: "public/plugins/myorgid-simple-panel/img/logo.svg", + Large: "public/plugins/myorgid-simple-panel/img/logo.svg", }, Screenshots: []plugins.Screenshots{}, Description: "Grafana Panel Plugin Template", @@ -1523,8 +1523,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Module: "public/plugins/myorgid-simple-app/child/module.js", - BaseURL: "public/plugins/myorgid-simple-app/child", + Module: "public/plugins/myorgid-simple-panel/module.js", + BaseURL: "public/plugins/myorgid-simple-panel", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")), IncludedInAppID: parent.ID, Signature: plugins.SignatureStatusValid, diff --git a/pkg/services/publicdashboards/public_dashboard_middleware_mock.go b/pkg/services/publicdashboards/public_dashboard_middleware_mock.go index f47e99dc0e2..79ebc20e6ce 100644 --- a/pkg/services/publicdashboards/public_dashboard_middleware_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_middleware_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.36.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package publicdashboards diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index 8e07012e294..b54ef3ec6db 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package publicdashboards diff --git a/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go b/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go index 646a6a033e2..e4c828a4faf 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package publicdashboards diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index f485ed445e6..32902d98df2 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package publicdashboards @@ -264,36 +264,6 @@ func (_m *FakePublicDashboardStore) FindByDashboardUid(ctx context.Context, orgI return r0, r1 } -// FindByFolder provides a mock function with given fields: ctx, orgId, folderUid -func (_m *FakePublicDashboardStore) FindByFolder(ctx context.Context, orgId int64, folderUid string) ([]*models.PublicDashboard, error) { - ret := _m.Called(ctx, orgId, folderUid) - - if len(ret) == 0 { - panic("no return value specified for FindByFolder") - } - - var r0 []*models.PublicDashboard - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int64, string) ([]*models.PublicDashboard, error)); ok { - return rf(ctx, orgId, folderUid) - } - if rf, ok := ret.Get(0).(func(context.Context, int64, string) []*models.PublicDashboard); ok { - r0 = rf(ctx, orgId, folderUid) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*models.PublicDashboard) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { - r1 = rf(ctx, orgId, folderUid) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetMetrics provides a mock function with given fields: ctx func (_m *FakePublicDashboardStore) GetMetrics(ctx context.Context) (*models.Metrics, error) { ret := _m.Called(ctx) diff --git a/pkg/services/query/query_service_mock.go b/pkg/services/query/query_service_mock.go index 5c5b4b69d37..800c822290d 100644 --- a/pkg/services/query/query_service_mock.go +++ b/pkg/services/query/query_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package query @@ -9,9 +9,9 @@ import ( dtos "github.com/grafana/grafana/pkg/api/dtos" - mock "github.com/stretchr/testify/mock" - identity "github.com/grafana/grafana/pkg/apimachinery/identity" + + mock "github.com/stretchr/testify/mock" ) // FakeQueryService is an autogenerated mock type for the Service type @@ -19,22 +19,29 @@ type FakeQueryService struct { mock.Mock } -// QueryData provides a mock function with given fields: ctx, _a1, skipDSCache, reqDTO -func (_m *FakeQueryService) QueryData(ctx context.Context, _a1 identity.Requester, skipDSCache bool, reqDTO dtos.MetricRequest) (*backend.QueryDataResponse, error) { - ret := _m.Called(ctx, _a1, skipDSCache, reqDTO) +// QueryData provides a mock function with given fields: ctx, user, skipDSCache, reqDTO +func (_m *FakeQueryService) QueryData(ctx context.Context, user identity.Requester, skipDSCache bool, reqDTO dtos.MetricRequest) (*backend.QueryDataResponse, error) { + ret := _m.Called(ctx, user, skipDSCache, reqDTO) + + if len(ret) == 0 { + panic("no return value specified for QueryData") + } var r0 *backend.QueryDataResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, bool, dtos.MetricRequest) (*backend.QueryDataResponse, error)); ok { + return rf(ctx, user, skipDSCache, reqDTO) + } if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, bool, dtos.MetricRequest) *backend.QueryDataResponse); ok { - r0 = rf(ctx, _a1, skipDSCache, reqDTO) + r0 = rf(ctx, user, skipDSCache, reqDTO) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*backend.QueryDataResponse) } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, identity.Requester, bool, dtos.MetricRequest) error); ok { - r1 = rf(ctx, _a1, skipDSCache, reqDTO) + r1 = rf(ctx, user, skipDSCache, reqDTO) } else { r1 = ret.Error(1) } @@ -46,6 +53,10 @@ func (_m *FakeQueryService) QueryData(ctx context.Context, _a1 identity.Requeste func (_m *FakeQueryService) Run(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Run") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -56,13 +67,12 @@ func (_m *FakeQueryService) Run(ctx context.Context) error { return r0 } -type mockConstructorTestingTNewFakeQueryService interface { +// NewFakeQueryService creates a new instance of FakeQueryService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFakeQueryService(t interface { mock.TestingT Cleanup(func()) -} - -// NewFakeQueryService creates a new instance of FakeQueryService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewFakeQueryService(t mockConstructorTestingTNewFakeQueryService) *FakeQueryService { +}) *FakeQueryService { mock := &FakeQueryService{} mock.Mock.Test(t) diff --git a/pkg/services/scimutil/scim_util.go b/pkg/services/scimutil/scim_util.go index 022277ba994..92d0ac74270 100644 --- a/pkg/services/scimutil/scim_util.go +++ b/pkg/services/scimutil/scim_util.go @@ -83,11 +83,7 @@ func (s *SCIMUtil) fetchDynamicSCIMSetting(ctx context.Context, orgID int64, set case "group": enabled = scimConfig.EnableGroupSync case "allowNonProvisionedUsers": - if scimConfig.AllowNonProvisionedUsers != nil { - enabled = *scimConfig.AllowNonProvisionedUsers - } else { - enabled = false - } + enabled = scimConfig.AllowNonProvisionedUsers default: s.logger.Error("Invalid setting type provided to fetchDynamicSCIMSetting", "settingType", settingType) return false, false @@ -112,9 +108,9 @@ func (s *SCIMUtil) getOrgSCIMConfig(ctx context.Context, orgID int64) (*SCIMConf // SCIMConfigSpec represents the spec part of a SCIMConfig resource type SCIMConfigSpec struct { - EnableUserSync bool `json:"enableUserSync"` - EnableGroupSync bool `json:"enableGroupSync"` - AllowNonProvisionedUsers *bool `json:"allowNonProvisionedUsers,omitempty"` + EnableUserSync bool `json:"enableUserSync"` + EnableGroupSync bool `json:"enableGroupSync"` + AllowNonProvisionedUsers bool `json:"allowNonProvisionedUsers"` } // unstructuredToSCIMConfig converts an unstructured object to a SCIMConfigSpec @@ -139,6 +135,6 @@ func (s *SCIMUtil) unstructuredToSCIMConfig(obj *unstructured.Unstructured) (*SC return &SCIMConfigSpec{ EnableUserSync: enableUserSync, EnableGroupSync: enableGroupSync, - AllowNonProvisionedUsers: &allowNonProvisionedUsers, + AllowNonProvisionedUsers: allowNonProvisionedUsers, }, nil } diff --git a/pkg/services/scimutil/scim_util_test.go b/pkg/services/scimutil/scim_util_test.go index 2e371d32fd8..0e9d3b39ee5 100644 --- a/pkg/services/scimutil/scim_util_test.go +++ b/pkg/services/scimutil/scim_util_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/util" ) // MockK8sHandler is a mock implementation of client.K8sHandler for testing @@ -573,7 +572,7 @@ func TestSCIMUtil_unstructuredToSCIMConfig(t *testing.T) { expectedSpec: SCIMConfigSpec{ EnableUserSync: true, EnableGroupSync: true, - AllowNonProvisionedUsers: util.Pointer(false), + AllowNonProvisionedUsers: false, }, }, { @@ -582,7 +581,7 @@ func TestSCIMUtil_unstructuredToSCIMConfig(t *testing.T) { expectedSpec: SCIMConfigSpec{ EnableUserSync: false, EnableGroupSync: false, - AllowNonProvisionedUsers: util.Pointer(false), + AllowNonProvisionedUsers: false, }, }, { @@ -591,7 +590,7 @@ func TestSCIMUtil_unstructuredToSCIMConfig(t *testing.T) { expectedSpec: SCIMConfigSpec{ EnableUserSync: true, EnableGroupSync: false, - AllowNonProvisionedUsers: util.Pointer(false), + AllowNonProvisionedUsers: false, }, }, { @@ -600,7 +599,7 @@ func TestSCIMUtil_unstructuredToSCIMConfig(t *testing.T) { expectedSpec: SCIMConfigSpec{ EnableUserSync: false, EnableGroupSync: false, - AllowNonProvisionedUsers: util.Pointer(true), + AllowNonProvisionedUsers: true, }, }, { diff --git a/pkg/services/searchV2/search_service_mock.go b/pkg/services/searchV2/search_service_mock.go index 63f7214c067..920b0621291 100644 --- a/pkg/services/searchV2/search_service_mock.go +++ b/pkg/services/searchV2/search_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.10.6. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package searchV2 @@ -6,6 +6,7 @@ import ( context "context" backend "github.com/grafana/grafana-plugin-sdk-go/backend" + mock "github.com/stretchr/testify/mock" user "github.com/grafana/grafana/pkg/services/user" @@ -20,6 +21,10 @@ type MockSearchService struct { func (_m *MockSearchService) DoDashboardQuery(ctx context.Context, _a1 *backend.User, orgId int64, query DashboardQuery) *backend.DataResponse { ret := _m.Called(ctx, _a1, orgId, query) + if len(ret) == 0 { + panic("no return value specified for DoDashboardQuery") + } + var r0 *backend.DataResponse if rf, ok := ret.Get(0).(func(context.Context, *backend.User, int64, DashboardQuery) *backend.DataResponse); ok { r0 = rf(ctx, _a1, orgId, query) @@ -32,10 +37,14 @@ func (_m *MockSearchService) DoDashboardQuery(ctx context.Context, _a1 *backend. return r0 } -// IsDisabled provides a mock function with given fields: +// IsDisabled provides a mock function with no fields func (_m *MockSearchService) IsDisabled() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for IsDisabled") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() @@ -50,6 +59,10 @@ func (_m *MockSearchService) IsDisabled() bool { func (_m *MockSearchService) IsReady(ctx context.Context, orgId int64) IsSearchReadyResponse { ret := _m.Called(ctx, orgId) + if len(ret) == 0 { + panic("no return value specified for IsReady") + } + var r0 IsSearchReadyResponse if rf, ok := ret.Get(0).(func(context.Context, int64) IsSearchReadyResponse); ok { r0 = rf(ctx, orgId) @@ -69,6 +82,10 @@ func (_m *MockSearchService) RegisterDashboardIndexExtender(ext DashboardIndexEx func (_m *MockSearchService) Run(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Run") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -79,7 +96,7 @@ func (_m *MockSearchService) Run(ctx context.Context) error { return r0 } -// TriggerReIndex provides a mock function with given fields: +// TriggerReIndex provides a mock function with no fields func (_m *MockSearchService) TriggerReIndex() { _m.Called() } @@ -88,6 +105,10 @@ func (_m *MockSearchService) TriggerReIndex() { func (_m *MockSearchService) doDashboardQuery(ctx context.Context, _a1 *user.SignedInUser, orgId int64, query DashboardQuery) *backend.DataResponse { ret := _m.Called(ctx, _a1, orgId, query) + if len(ret) == 0 { + panic("no return value specified for doDashboardQuery") + } + var r0 *backend.DataResponse if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, DashboardQuery) *backend.DataResponse); ok { r0 = rf(ctx, _a1, orgId, query) @@ -99,3 +120,17 @@ func (_m *MockSearchService) doDashboardQuery(ctx context.Context, _a1 *user.Sig return r0 } + +// NewMockSearchService creates a new instance of MockSearchService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockSearchService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockSearchService { + mock := &MockSearchService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/secrets/fakes/mock_service.go b/pkg/services/secrets/fakes/mock_service.go index 30ac1628653..a138659559c 100644 --- a/pkg/services/secrets/fakes/mock_service.go +++ b/pkg/services/secrets/fakes/mock_service.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package fakes @@ -18,6 +18,10 @@ type MockService struct { func (_m *MockService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) { ret := _m.Called(ctx, payload) + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { @@ -44,6 +48,10 @@ func (_m *MockService) Decrypt(ctx context.Context, payload []byte) ([]byte, err func (_m *MockService) DecryptJsonData(ctx context.Context, sjd map[string][]byte) (map[string]string, error) { ret := _m.Called(ctx, sjd) + if len(ret) == 0 { + panic("no return value specified for DecryptJsonData") + } + var r0 map[string]string var r1 error if rf, ok := ret.Get(0).(func(context.Context, map[string][]byte) (map[string]string, error)); ok { @@ -70,6 +78,10 @@ func (_m *MockService) DecryptJsonData(ctx context.Context, sjd map[string][]byt func (_m *MockService) Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error) { ret := _m.Called(ctx, payload, opt) + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, []byte, secrets.EncryptionOptions) ([]byte, error)); ok { @@ -96,6 +108,10 @@ func (_m *MockService) Encrypt(ctx context.Context, payload []byte, opt secrets. func (_m *MockService) EncryptJsonData(ctx context.Context, kv map[string]string, opt secrets.EncryptionOptions) (map[string][]byte, error) { ret := _m.Called(ctx, kv, opt) + if len(ret) == 0 { + panic("no return value specified for EncryptJsonData") + } + var r0 map[string][]byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, map[string]string, secrets.EncryptionOptions) (map[string][]byte, error)); ok { @@ -122,6 +138,10 @@ func (_m *MockService) EncryptJsonData(ctx context.Context, kv map[string]string func (_m *MockService) GetDecryptedValue(ctx context.Context, sjd map[string][]byte, key string, fallback string) string { ret := _m.Called(ctx, sjd, key, fallback) + if len(ret) == 0 { + panic("no return value specified for GetDecryptedValue") + } + var r0 string if rf, ok := ret.Get(0).(func(context.Context, map[string][]byte, string, string) string); ok { r0 = rf(ctx, sjd, key, fallback) @@ -136,6 +156,10 @@ func (_m *MockService) GetDecryptedValue(ctx context.Context, sjd map[string][]b func (_m *MockService) ReEncryptDataKeys(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for ReEncryptDataKeys") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -150,6 +174,10 @@ func (_m *MockService) ReEncryptDataKeys(ctx context.Context) error { func (_m *MockService) RotateDataKeys(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for RotateDataKeys") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) diff --git a/pkg/services/serviceaccounts/tests/extsvcaccmock.go b/pkg/services/serviceaccounts/tests/extsvcaccmock.go index a81afeb50a8..3f1fdf31152 100644 --- a/pkg/services/serviceaccounts/tests/extsvcaccmock.go +++ b/pkg/services/serviceaccounts/tests/extsvcaccmock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.35.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package tests @@ -18,6 +18,10 @@ type MockExtSvcAccountsService struct { func (_m *MockExtSvcAccountsService) EnableExtSvcAccount(ctx context.Context, cmd *serviceaccounts.EnableExtSvcAccountCmd) error { ret := _m.Called(ctx, cmd) + if len(ret) == 0 { + panic("no return value specified for EnableExtSvcAccount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.EnableExtSvcAccountCmd) error); ok { r0 = rf(ctx, cmd) @@ -32,6 +36,10 @@ func (_m *MockExtSvcAccountsService) EnableExtSvcAccount(ctx context.Context, cm func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *serviceaccounts.ManageExtSvcAccountCmd) (int64, error) { ret := _m.Called(ctx, cmd) + if len(ret) == 0 { + panic("no return value specified for ManageExtSvcAccount") + } + var r0 int64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.ManageExtSvcAccountCmd) (int64, error)); ok { @@ -56,6 +64,10 @@ func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cm func (_m *MockExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, orgID int64, extSvcSlug string) error { ret := _m.Called(ctx, orgID, extSvcSlug) + if len(ret) == 0 { + panic("no return value specified for RemoveExtSvcAccount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok { r0 = rf(ctx, orgID, extSvcSlug) @@ -70,6 +82,10 @@ func (_m *MockExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, or func (_m *MockExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, orgID int64, saID int64) (*serviceaccounts.ExtSvcAccount, error) { ret := _m.Called(ctx, orgID, saID) + if len(ret) == 0 { + panic("no return value specified for RetrieveExtSvcAccount") + } + var r0 *serviceaccounts.ExtSvcAccount var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*serviceaccounts.ExtSvcAccount, error)); ok { diff --git a/pkg/services/serviceaccounts/tests/mocks.go b/pkg/services/serviceaccounts/tests/mocks.go index a33fc5ae5b4..28df9752b24 100644 --- a/pkg/services/serviceaccounts/tests/mocks.go +++ b/pkg/services/serviceaccounts/tests/mocks.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.35.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package tests @@ -21,6 +21,10 @@ type MockServiceAccountService struct { func (_m *MockServiceAccountService) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { ret := _m.Called(ctx, serviceAccountID, cmd) + if len(ret) == 0 { + panic("no return value specified for AddServiceAccountToken") + } + var r0 *apikey.APIKey var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error)); ok { @@ -47,6 +51,10 @@ func (_m *MockServiceAccountService) AddServiceAccountToken(ctx context.Context, func (_m *MockServiceAccountService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { ret := _m.Called(ctx, orgID, saForm) + if len(ret) == 0 { + panic("no return value specified for CreateServiceAccount") + } + var r0 *serviceaccounts.ServiceAccountDTO var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error)); ok { @@ -73,6 +81,10 @@ func (_m *MockServiceAccountService) CreateServiceAccount(ctx context.Context, o func (_m *MockServiceAccountService) DeleteServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) error { ret := _m.Called(ctx, orgID, serviceAccountID) + if len(ret) == 0 { + panic("no return value specified for DeleteServiceAccount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { r0 = rf(ctx, orgID, serviceAccountID) @@ -87,6 +99,10 @@ func (_m *MockServiceAccountService) DeleteServiceAccount(ctx context.Context, o func (_m *MockServiceAccountService) DeleteServiceAccountToken(ctx context.Context, orgID int64, serviceAccountID int64, tokenID int64) error { ret := _m.Called(ctx, orgID, serviceAccountID, tokenID) + if len(ret) == 0 { + panic("no return value specified for DeleteServiceAccountToken") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int64) error); ok { r0 = rf(ctx, orgID, serviceAccountID, tokenID) @@ -101,6 +117,10 @@ func (_m *MockServiceAccountService) DeleteServiceAccountToken(ctx context.Conte func (_m *MockServiceAccountService) EnableServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, enable bool) error { ret := _m.Called(ctx, orgID, serviceAccountID, enable) + if len(ret) == 0 { + panic("no return value specified for EnableServiceAccount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64, bool) error); ok { r0 = rf(ctx, orgID, serviceAccountID, enable) @@ -115,6 +135,10 @@ func (_m *MockServiceAccountService) EnableServiceAccount(ctx context.Context, o func (_m *MockServiceAccountService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for ListTokens") + } + var r0 []apikey.APIKey var r1 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error)); ok { @@ -141,6 +165,10 @@ func (_m *MockServiceAccountService) ListTokens(ctx context.Context, query *serv func (_m *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { ret := _m.Called(ctx, orgID) + if len(ret) == 0 { + panic("no return value specified for MigrateApiKeysToServiceAccounts") + } + var r0 *serviceaccounts.MigrationResult var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64) (*serviceaccounts.MigrationResult, error)); ok { @@ -163,10 +191,14 @@ func (_m *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context return r0, r1 } -// RetrieveServiceAccount provides a mock function with given fields: ctx, orgID, serviceAccountID +// RetrieveServiceAccount provides a mock function with given fields: ctx, query func (_m *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for RetrieveServiceAccount") + } + var r0 *serviceaccounts.ServiceAccountProfileDTO var r1 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error)); ok { @@ -193,6 +225,10 @@ func (_m *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, func (_m *MockServiceAccountService) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { ret := _m.Called(ctx, orgID, name) + if len(ret) == 0 { + panic("no return value specified for RetrieveServiceAccountIdByName") + } + var r0 int64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) (int64, error)); ok { @@ -217,6 +253,10 @@ func (_m *MockServiceAccountService) RetrieveServiceAccountIdByName(ctx context. func (_m *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for SearchOrgServiceAccounts") + } + var r0 *serviceaccounts.SearchOrgServiceAccountsResult var r1 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error)); ok { @@ -243,6 +283,10 @@ func (_m *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Contex func (_m *MockServiceAccountService) UpdateServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { ret := _m.Called(ctx, orgID, serviceAccountID, saForm) + if len(ret) == 0 { + panic("no return value specified for UpdateServiceAccount") + } + var r0 *serviceaccounts.ServiceAccountProfileDTO var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64, *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error)); ok { diff --git a/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go b/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go index b02bc013056..069f5d2ea7b 100644 --- a/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go +++ b/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package ssosettingstests diff --git a/pkg/services/ssosettings/ssosettingstests/service_mock.go b/pkg/services/ssosettings/ssosettingstests/service_mock.go index 45bd2ab9dc9..528e3e5a059 100644 --- a/pkg/services/ssosettings/ssosettingstests/service_mock.go +++ b/pkg/services/ssosettings/ssosettingstests/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.40.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package ssosettingstests diff --git a/pkg/services/ssosettings/ssosettingstests/store_mock.go b/pkg/services/ssosettings/ssosettingstests/store_mock.go index 009660cd3e6..3ac1d6c1096 100644 --- a/pkg/services/ssosettings/ssosettingstests/store_mock.go +++ b/pkg/services/ssosettings/ssosettingstests/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.40.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package ssosettingstests diff --git a/pkg/services/store/entity_events_mock.go b/pkg/services/store/entity_events_mock.go index 52dc2bdff45..460f81a75c4 100644 --- a/pkg/services/store/entity_events_mock.go +++ b/pkg/services/store/entity_events_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.12.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package store @@ -18,7 +18,15 @@ type MockEntityEventsService struct { func (_m *MockEntityEventsService) GetAllEventsAfter(ctx context.Context, id int64) ([]*EntityEvent, error) { ret := _m.Called(ctx, id) + if len(ret) == 0 { + panic("no return value specified for GetAllEventsAfter") + } + var r0 []*EntityEvent + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) ([]*EntityEvent, error)); ok { + return rf(ctx, id) + } if rf, ok := ret.Get(0).(func(context.Context, int64) []*EntityEvent); ok { r0 = rf(ctx, id) } else { @@ -27,7 +35,6 @@ func (_m *MockEntityEventsService) GetAllEventsAfter(ctx context.Context, id int } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { r1 = rf(ctx, id) } else { @@ -41,7 +48,15 @@ func (_m *MockEntityEventsService) GetAllEventsAfter(ctx context.Context, id int func (_m *MockEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEvent, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for GetLastEvent") + } + var r0 *EntityEvent + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*EntityEvent, error)); ok { + return rf(ctx) + } if rf, ok := ret.Get(0).(func(context.Context) *EntityEvent); ok { r0 = rf(ctx) } else { @@ -50,7 +65,6 @@ func (_m *MockEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEve } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { @@ -60,10 +74,14 @@ func (_m *MockEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEve return r0, r1 } -// IsDisabled provides a mock function with given fields: +// IsDisabled provides a mock function with no fields func (_m *MockEntityEventsService) IsDisabled() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for IsDisabled") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() @@ -74,15 +92,14 @@ func (_m *MockEntityEventsService) IsDisabled() bool { return r0 } -// OnEvent provides a mock function with given fields: handler -func (_m *MockEntityEventsService) OnEvent(handler EventHandler) { - _m.Called(handler) -} - // Run provides a mock function with given fields: ctx func (_m *MockEntityEventsService) Run(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Run") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -93,24 +110,14 @@ func (_m *MockEntityEventsService) Run(ctx context.Context) error { return r0 } -// SaveEvent provides a mock function with given fields: ctx, cmd -func (_m *MockEntityEventsService) SaveEvent(ctx context.Context, cmd SaveEventCmd) error { - ret := _m.Called(ctx, cmd) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, SaveEventCmd) error); ok { - r0 = rf(ctx, cmd) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // deleteEventsOlderThan provides a mock function with given fields: ctx, duration func (_m *MockEntityEventsService) deleteEventsOlderThan(ctx context.Context, duration time.Duration) error { ret := _m.Called(ctx, duration) + if len(ret) == 0 { + panic("no return value specified for deleteEventsOlderThan") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, time.Duration) error); ok { r0 = rf(ctx, duration) @@ -121,13 +128,12 @@ func (_m *MockEntityEventsService) deleteEventsOlderThan(ctx context.Context, du return r0 } -type NewMockEntityEventsServiceT interface { +// NewMockEntityEventsService creates a new instance of MockEntityEventsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockEntityEventsService(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockEntityEventsService creates a new instance of MockEntityEventsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockEntityEventsService(t NewMockEntityEventsServiceT) *MockEntityEventsService { +}) *MockEntityEventsService { mock := &MockEntityEventsService{} mock.Mock.Test(t) diff --git a/pkg/services/store/system_users_mock.go b/pkg/services/store/system_users_mock.go index 525a807dd69..d733148bb35 100644 --- a/pkg/services/store/system_users_mock.go +++ b/pkg/services/store/system_users_mock.go @@ -1,11 +1,11 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package store import ( + filestorage "github.com/grafana/grafana/pkg/infra/filestorage" mock "github.com/stretchr/testify/mock" - filestorage "github.com/grafana/grafana/pkg/infra/filestorage" user "github.com/grafana/grafana/pkg/services/user" ) @@ -18,7 +18,15 @@ type FakeSystemUsers struct { func (_m *FakeSystemUsers) GetFilter(_a0 *user.SignedInUser) (map[string]filestorage.PathFilter, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetFilter") + } + var r0 map[string]filestorage.PathFilter + var r1 error + if rf, ok := ret.Get(0).(func(*user.SignedInUser) (map[string]filestorage.PathFilter, error)); ok { + return rf(_a0) + } if rf, ok := ret.Get(0).(func(*user.SignedInUser) map[string]filestorage.PathFilter); ok { r0 = rf(_a0) } else { @@ -27,7 +35,6 @@ func (_m *FakeSystemUsers) GetFilter(_a0 *user.SignedInUser) (map[string]filesto } } - var r1 error if rf, ok := ret.Get(1).(func(*user.SignedInUser) error); ok { r1 = rf(_a0) } else { @@ -41,7 +48,15 @@ func (_m *FakeSystemUsers) GetFilter(_a0 *user.SignedInUser) (map[string]filesto func (_m *FakeSystemUsers) GetUser(userType SystemUserType, orgID int64) (*user.SignedInUser, error) { ret := _m.Called(userType, orgID) + if len(ret) == 0 { + panic("no return value specified for GetUser") + } + var r0 *user.SignedInUser + var r1 error + if rf, ok := ret.Get(0).(func(SystemUserType, int64) (*user.SignedInUser, error)); ok { + return rf(userType, orgID) + } if rf, ok := ret.Get(0).(func(SystemUserType, int64) *user.SignedInUser); ok { r0 = rf(userType, orgID) } else { @@ -50,7 +65,6 @@ func (_m *FakeSystemUsers) GetUser(userType SystemUserType, orgID int64) (*user. } } - var r1 error if rf, ok := ret.Get(1).(func(SystemUserType, int64) error); ok { r1 = rf(userType, orgID) } else { @@ -65,13 +79,12 @@ func (_m *FakeSystemUsers) RegisterUser(userType SystemUserType, filterFn func() _m.Called(userType, filterFn) } -type mockConstructorTestingTNewFakeSystemUsers interface { +// NewFakeSystemUsers creates a new instance of FakeSystemUsers. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFakeSystemUsers(t interface { mock.TestingT Cleanup(func()) -} - -// NewFakeSystemUsers creates a new instance of FakeSystemUsers. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewFakeSystemUsers(t mockConstructorTestingTNewFakeSystemUsers) *FakeSystemUsers { +}) *FakeSystemUsers { mock := &FakeSystemUsers{} mock.Mock.Test(t) diff --git a/pkg/services/user/usertest/mock.go b/pkg/services/user/usertest/mock.go index 943b1520fb1..36da416d1ef 100644 --- a/pkg/services/user/usertest/mock.go +++ b/pkg/services/user/usertest/mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package usertest @@ -230,35 +230,6 @@ func (_m *MockService) GetByUID(_a0 context.Context, _a1 *user.GetUserByUIDQuery return r0, r1 } -func (_m *MockService) ListByIdOrUID(_a0 context.Context, _a1 []string, _a2 []int64) ([]*user.User, error) { - ret := _m.Called(_a0, _a1, _a2) - - if len(ret) == 0 { - panic("no return value specified for ListByIdOrUID") - } - - var r0 []*user.User - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []string, []int64) ([]*user.User, error)); ok { - return rf(_a0, _a1, _a2) - } - if rf, ok := ret.Get(0).(func(context.Context, []string, []int64) []*user.User); ok { - r0 = rf(_a0, _a1, _a2) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*user.User) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, []string, []int64) error); ok { - r1 = rf(_a0, _a1, _a2) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetProfile provides a mock function with given fields: _a0, _a1 func (_m *MockService) GetProfile(_a0 context.Context, _a1 *user.GetUserProfileQuery) (*user.UserProfileDTO, error) { ret := _m.Called(_a0, _a1) @@ -339,6 +310,36 @@ func (_m *MockService) GetUsageStats(ctx context.Context) map[string]interface{} return r0 } +// ListByIdOrUID provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MockService) ListByIdOrUID(_a0 context.Context, _a1 []string, _a2 []int64) ([]*user.User, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for ListByIdOrUID") + } + + var r0 []*user.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string, []int64) ([]*user.User, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, []string, []int64) []*user.User); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*user.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string, []int64) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Search provides a mock function with given fields: _a0, _a1 func (_m *MockService) Search(_a0 context.Context, _a1 *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) { ret := _m.Called(_a0, _a1) diff --git a/pkg/setting/settingtest/provider_mock.go b/pkg/setting/settingtest/provider_mock.go index 9cccb382d72..bf0bb232d86 100644 --- a/pkg/setting/settingtest/provider_mock.go +++ b/pkg/setting/settingtest/provider_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package settingtest @@ -12,7 +12,7 @@ type MockProvider struct { mock.Mock } -// Current provides a mock function with given fields: +// Current provides a mock function with no fields func (_m *MockProvider) Current() setting.SettingsBag { ret := _m.Called() @@ -32,7 +32,7 @@ func (_m *MockProvider) Current() setting.SettingsBag { return r0 } -// CurrentVerbose provides a mock function with given fields: +// CurrentVerbose provides a mock function with no fields func (_m *MockProvider) CurrentVerbose() setting.VerboseSettingsBag { ret := _m.Called() diff --git a/pkg/storage/legacysql/dualwrite/service_mock.go b/pkg/storage/legacysql/dualwrite/service_mock.go index 264c19d7047..d7524082652 100644 --- a/pkg/storage/legacysql/dualwrite/service_mock.go +++ b/pkg/storage/legacysql/dualwrite/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dualwrite diff --git a/pkg/storage/secret/metadata/data/secure_value_create.sql b/pkg/storage/secret/metadata/data/secure_value_create.sql index ae03dd2405e..a120feaa5e5 100644 --- a/pkg/storage/secret/metadata/data/secure_value_create.sql +++ b/pkg/storage/secret/metadata/data/secure_value_create.sql @@ -8,10 +8,8 @@ INSERT INTO {{ .Ident "secret_secure_value" }} ( {{ .Ident "created_by" }}, {{ .Ident "updated" }}, {{ .Ident "updated_by" }}, - {{ .Ident "status_phase" }}, - {{ if .Row.Message.Valid }} - {{ .Ident "status_message" }}, - {{ end }} + {{ .Ident "active" }}, + {{ .Ident "version" }}, {{ .Ident "description" }}, {{ if .Row.Keeper.Valid }} {{ .Ident "keeper" }}, @@ -33,10 +31,8 @@ INSERT INTO {{ .Ident "secret_secure_value" }} ( {{ .Arg .Row.CreatedBy }}, {{ .Arg .Row.Updated }}, {{ .Arg .Row.UpdatedBy }}, - {{ .Arg .Row.Phase }}, - {{ if .Row.Message.Valid }} - {{ .Arg .Row.Message.String }}, - {{ end }} + {{ .Arg .Row.Active }}, + {{ .Arg .Row.Version }}, {{ .Arg .Row.Description }}, {{ if .Row.Keeper.Valid }} {{ .Arg .Row.Keeper.String }}, @@ -48,4 +44,4 @@ INSERT INTO {{ .Ident "secret_secure_value" }} ( {{ .Arg .Row.Ref.String }}, {{ end }} {{ .Arg .Row.ExternalID }} -); +); \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_delete.sql b/pkg/storage/secret/metadata/data/secure_value_delete.sql deleted file mode 100644 index 2714510c291..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql b/pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql new file mode 100644 index 00000000000..90319d469eb --- /dev/null +++ b/pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql @@ -0,0 +1,10 @@ +SELECT + {{ .Ident "version" }} +FROM + {{ .Ident "secret_secure_value" }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} +ORDER BY {{ .Ident "version" }} DESC +LIMIT 1 +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_list.sql b/pkg/storage/secret/metadata/data/secure_value_list.sql index db19a729ebf..9c0c604b72d 100644 --- a/pkg/storage/secret/metadata/data/secure_value_list.sql +++ b/pkg/storage/secret/metadata/data/secure_value_list.sql @@ -8,15 +8,17 @@ SELECT {{ .Ident "created_by" }}, {{ .Ident "updated" }}, {{ .Ident "updated_by" }}, - {{ .Ident "status_phase" }}, - {{ .Ident "status_message" }}, {{ .Ident "description" }}, {{ .Ident "keeper" }}, {{ .Ident "decrypters" }}, {{ .Ident "ref" }}, - {{ .Ident "external_id" }} + {{ .Ident "external_id" }}, + {{ .Ident "version" }}, + {{ .Ident "active" }} FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "active" }} = true ORDER BY {{ .Ident "updated" }} DESC -; +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_listByName.sql b/pkg/storage/secret/metadata/data/secure_value_listByName.sql index b8158d3c0ff..7a5945e4fde 100644 --- a/pkg/storage/secret/metadata/data/secure_value_listByName.sql +++ b/pkg/storage/secret/metadata/data/secure_value_listByName.sql @@ -5,7 +5,9 @@ SELECT {{ .Ident "keeper" }} FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} IN ({{ .ArgList .UsedSecureValues }}) +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} IN ({{ .ArgList .UsedSecureValues }}) AND + {{ .Ident "active" }} = true {{ .SelectFor "UPDATE" }} ; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_append.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_append.sql deleted file mode 100644 index e541cec3140..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_append.sql +++ /dev/null @@ -1,33 +0,0 @@ -INSERT INTO {{ .Ident "secret_secure_value_outbox" }} ( - {{ .Ident "request_id" }}, - {{ .Ident "message_type" }}, - {{ .Ident "name" }}, - {{ .Ident "namespace" }}, -{{ if .Row.EncryptedSecret.Valid }} - {{ .Ident "encrypted_secret" }}, -{{ end }} -{{ if .Row.KeeperName.Valid }} - {{ .Ident "keeper_name" }}, -{{ end }} -{{ if .Row.ExternalID.Valid }} - {{ .Ident "external_id" }}, -{{ end }} - {{ .Ident "receive_count" }}, - {{ .Ident "created" }} -) VALUES ( - {{ .Arg .Row.RequestID }}, - {{ .Arg .Row.MessageType }}, - {{ .Arg .Row.Name }}, - {{ .Arg .Row.Namespace }}, -{{ if .Row.EncryptedSecret.Valid }} - {{ .Arg .Row.EncryptedSecret.String }}, -{{ end }} -{{ if .Row.KeeperName.Valid }} - {{ .Arg .Row.KeeperName.String }}, -{{ end }} -{{ if .Row.ExternalID.Valid }} - {{ .Arg .Row.ExternalID.String }}, -{{ end }} - {{ .Arg .Row.ReceiveCount }}, - {{ .Arg .Row.Created }} -); diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_delete.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_delete.sql deleted file mode 100644 index d23f3808f9c..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_delete.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - {{ .Ident "secret_secure_value_outbox" }} -WHERE - {{ .Ident "id" }} = {{ .Arg .MessageID }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_fetch_message_ids.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_fetch_message_ids.sql deleted file mode 100644 index 2c53c2223fd..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_fetch_message_ids.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT - {{ .Ident "id" }} -FROM {{ .Ident "secret_secure_value_outbox" }} -ORDER BY id ASC -LIMIT {{ .Arg .ReceiveLimit }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_query_timestamp.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_query_timestamp.sql deleted file mode 100644 index 8e525ca9009..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_query_timestamp.sql +++ /dev/null @@ -1,8 +0,0 @@ -SELECT - {{ .Ident "created" }}, - {{ .Ident "message_type" }} -FROM - {{ .Ident "secret_secure_value_outbox" }} -WHERE - {{ .Ident "id" }} = {{ .Arg .MessageID }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_receiveN.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_receiveN.sql deleted file mode 100644 index 254cb3d3f3d..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_receiveN.sql +++ /dev/null @@ -1,19 +0,0 @@ -SELECT - {{ .Ident "request_id" }}, - {{ .Ident "id" }}, - {{ .Ident "message_type" }}, - {{ .Ident "name" }}, - {{ .Ident "namespace" }}, - {{ .Ident "encrypted_secret" }}, - {{ .Ident "keeper_name" }}, - {{ .Ident "external_id" }}, - {{ .Ident "receive_count" }}, - {{ .Ident "created" }} -FROM - {{ .Ident "secret_secure_value_outbox" }} -WHERE - {{ .Ident "id" }} IN ({{ .ArgList .MessageIDs }}) -ORDER BY - {{ .Ident "id" }} ASC -{{ .SelectFor "UPDATE SKIP LOCKED" }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_update_receive_count.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_update_receive_count.sql deleted file mode 100644 index 610f29695a8..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_update_receive_count.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE - {{ .Ident "secret_secure_value_outbox" }} -SET - {{ .Ident "receive_count" }} = {{ .Ident "receive_count" }} + 1 -WHERE - {{ .Ident "id" }} IN ({{ .ArgList .MessageIDs }}) -; diff --git a/pkg/storage/secret/metadata/data/secure_value_read.sql b/pkg/storage/secret/metadata/data/secure_value_read.sql index 3453abb08e4..b90d54b4a5f 100644 --- a/pkg/storage/secret/metadata/data/secure_value_read.sql +++ b/pkg/storage/secret/metadata/data/secure_value_read.sql @@ -8,18 +8,20 @@ SELECT {{ .Ident "created_by" }}, {{ .Ident "updated" }}, {{ .Ident "updated_by" }}, - {{ .Ident "status_phase" }}, - {{ .Ident "status_message" }}, {{ .Ident "description" }}, {{ .Ident "keeper" }}, {{ .Ident "decrypters" }}, {{ .Ident "ref" }}, - {{ .Ident "external_id" }} + {{ .Ident "external_id" }}, + {{ .Ident "active" }}, + {{ .Ident "version" }} FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "active" }} = true {{ if .IsForUpdate }} {{ .SelectFor "UPDATE" }} {{ end }} -; +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql b/pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql index 14080fa0507..975c00d9f3b 100644 --- a/pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql +++ b/pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql @@ -2,9 +2,12 @@ SELECT {{ .Ident "keeper" }}, {{ .Ident "decrypters" }}, {{ .Ident "ref" }}, - {{ .Ident "external_id" }} + {{ .Ident "external_id" }}, + {{ .Ident "active" }} FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} -; +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "active" }} = true +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_set_version_to_active.sql b/pkg/storage/secret/metadata/data/secure_value_set_version_to_active.sql new file mode 100644 index 00000000000..bf069441ac3 --- /dev/null +++ b/pkg/storage/secret/metadata/data/secure_value_set_version_to_active.sql @@ -0,0 +1,8 @@ +UPDATE + {{ .Ident "secret_secure_value" }} +SET + {{ .Ident "active" }} = ({{ .Ident "version" }} = {{ .Arg .Version}}) +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_set_version_to_inactive.sql b/pkg/storage/secret/metadata/data/secure_value_set_version_to_inactive.sql new file mode 100644 index 00000000000..608bb813c4f --- /dev/null +++ b/pkg/storage/secret/metadata/data/secure_value_set_version_to_inactive.sql @@ -0,0 +1,9 @@ +UPDATE + {{ .Ident "secret_secure_value" }} +SET + {{ .Ident "active" }} = false +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "version" }} = {{ .Arg .Version }} +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_update.sql b/pkg/storage/secret/metadata/data/secure_value_update.sql deleted file mode 100644 index 90fc4946611..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_update.sql +++ /dev/null @@ -1,30 +0,0 @@ -UPDATE - {{ .Ident "secret_secure_value" }} -SET - {{ .Ident "guid" }} = {{ .Arg .Row.GUID }}, - {{ .Ident "name" }} = {{ .Arg .Row.Name }}, - {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }}, - {{ .Ident "annotations" }} = {{ .Arg .Row.Annotations }}, - {{ .Ident "labels" }} = {{ .Arg .Row.Labels }}, - {{ .Ident "created" }} = {{ .Arg .Row.Created }}, - {{ .Ident "created_by" }} = {{ .Arg .Row.CreatedBy }}, - {{ .Ident "updated" }} = {{ .Arg .Row.Updated }}, - {{ .Ident "updated_by" }} = {{ .Arg .Row.UpdatedBy }}, - {{ .Ident "status_phase" }} = {{ .Arg .Row.Phase }}, - {{ if .Row.Message.Valid }} - {{ .Ident "status_message" }} = {{ .Arg .Row.Message.String }}, - {{ end }} - {{ .Ident "description" }} = {{ .Arg .Row.Description }}, - {{ if .Row.Keeper.Valid }} - {{ .Ident "keeper" }} = {{ .Arg .Row.Keeper.String }}, - {{ end }} - {{ if .Row.Decrypters.Valid }} - {{ .Ident "decrypters" }} = {{ .Arg .Row.Decrypters.String }}, - {{ end }} - {{ if .Row.Ref.Valid }} - {{ .Ident "ref" }} = {{ .Arg .Row.Ref.String }}, - {{ end }} - {{ .Ident "external_id" }} = {{ .Arg .Row.ExternalID }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Row.Name }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql b/pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql index 9b2927802e6..fb891badaf5 100644 --- a/pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql +++ b/pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql @@ -2,6 +2,8 @@ UPDATE {{ .Ident "secret_secure_value" }} SET {{ .Ident "external_id" }} = {{ .Arg .ExternalID }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} -; +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "version" }} = {{ .Arg .Version }} +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_updateStatus.sql b/pkg/storage/secret/metadata/data/secure_value_updateStatus.sql deleted file mode 100644 index dfba1960ce0..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_updateStatus.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE - {{ .Ident "secret_secure_value" }} -SET - {{ .Ident "status_phase" }} = {{ .Arg .Phase }}, - {{ .Ident "status_message" }} = {{ .Arg .Message }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} -; diff --git a/pkg/storage/secret/metadata/decrypt_store_test.go b/pkg/storage/secret/metadata/decrypt_store_test.go index 69a178c01cf..5ebbb2f9466 100644 --- a/pkg/storage/secret/metadata/decrypt_store_test.go +++ b/pkg/storage/secret/metadata/decrypt_store_test.go @@ -1,4 +1,4 @@ -package metadata +package metadata_test import ( "context" @@ -7,23 +7,11 @@ import ( "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace/noop" "github.com/grafana/grafana/pkg/apimachinery/identity" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" - encryptionmanager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" - "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/secret/database" - encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" - "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" ) func TestIntegrationDecrypt(t *testing.T) { @@ -39,9 +27,9 @@ func TestIntegrationDecrypt(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - decryptSvc, _, _, _ := setupDecryptTestService(t, nil) + sut := testutils.Setup(t) - exposed, err := decryptSvc.Decrypt(ctx, "default", "name") + exposed, err := sut.DecryptStorage.Decrypt(ctx, "default", "name") require.Error(t, err) require.Empty(t, exposed) }) @@ -55,9 +43,11 @@ func TestIntegrationDecrypt(t *testing.T) { // Create auth context with proper permissions authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/group1:decrypt"}, "svc", types.TypeUser) - decryptSvc, _, _, _ := setupDecryptTestService(t, map[string]struct{}{"group1": {}}) + sut := testutils.Setup(t, testutils.WithMutateCfg(func(sc *testutils.SetupConfig) { + sc.AllowList = map[string]struct{}{"group1": {}} + })) - exposed, err := decryptSvc.Decrypt(authCtx, "default", "non-existent-value") + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "non-existent-value") require.ErrorIs(t, err, contracts.ErrDecryptNotFound) require.Empty(t, exposed) }) @@ -78,7 +68,9 @@ func TestIntegrationDecrypt(t *testing.T) { allowList := map[string]struct{}{"allowed-group": {}} // Setup service - decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList) + sut := testutils.Setup(t, testutils.WithMutateCfg(func(sc *testutils.SetupConfig) { + sc.AllowList = allowList + })) // Create a secure value that is not in the allowlist spec := secretv0alpha1.SecureValueSpec{ @@ -90,9 +82,10 @@ func TestIntegrationDecrypt(t *testing.T) { sv.Name = svName sv.Namespace = "default" - newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) - exposed, err := decryptSvc.Decrypt(authCtx, "default", svName) + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName) require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.Empty(t, exposed) }) @@ -112,7 +105,9 @@ func TestIntegrationDecrypt(t *testing.T) { allowList := map[string]struct{}{svcIdentity: {}} // Setup service - decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList) + sut := testutils.Setup(t, testutils.WithMutateCfg(func(sc *testutils.SetupConfig) { + sc.AllowList = allowList + })) // Create a secure value that is in the allowlist spec := secretv0alpha1.SecureValueSpec{ @@ -124,9 +119,10 @@ func TestIntegrationDecrypt(t *testing.T) { sv.Name = "sv-test" sv.Namespace = "default" - newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) - exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test") + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test") require.NoError(t, err) require.NotEmpty(t, exposed) require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue()) @@ -148,7 +144,9 @@ func TestIntegrationDecrypt(t *testing.T) { allowList := map[string]struct{}{svcIdentity: {}} // Setup service - decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList) + sut := testutils.Setup(t, testutils.WithMutateCfg(func(sc *testutils.SetupConfig) { + sc.AllowList = allowList + })) // Create a secure value that is in the allowlist spec := secretv0alpha1.SecureValueSpec{ @@ -160,9 +158,10 @@ func TestIntegrationDecrypt(t *testing.T) { sv.Name = svName sv.Namespace = "default" - newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) - exposed, err := decryptSvc.Decrypt(authCtx, "default", svName) + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName) require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.Empty(t, exposed) }) @@ -179,7 +178,7 @@ func TestIntegrationDecrypt(t *testing.T) { authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues"}, svcIdentity, types.TypeUser) // Setup service - decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}}) + sut := testutils.Setup(t) // Create a secure value spec := secretv0alpha1.SecureValueSpec{ @@ -191,9 +190,10 @@ func TestIntegrationDecrypt(t *testing.T) { sv.Name = "sv-test" sv.Namespace = "default" - newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) - exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test") + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test") require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.Empty(t, exposed) }) @@ -211,7 +211,7 @@ func TestIntegrationDecrypt(t *testing.T) { authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/" + svName + ":read"}, svcIdentity, types.TypeUser) // Setup service - decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}}) + sut := testutils.Setup(t) // Create a secure value spec := secretv0alpha1.SecureValueSpec{ @@ -223,9 +223,10 @@ func TestIntegrationDecrypt(t *testing.T) { sv.Name = svName sv.Namespace = "default" - newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) - exposed, err := decryptSvc.Decrypt(authCtx, "default", svName) + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName) require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.Empty(t, exposed) }) @@ -242,7 +243,7 @@ func TestIntegrationDecrypt(t *testing.T) { authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/:decrypt"}, svcIdentity, types.TypeUser) // Setup service - decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}}) + sut := testutils.Setup(t) // Create a secure value spec := secretv0alpha1.SecureValueSpec{ @@ -254,9 +255,10 @@ func TestIntegrationDecrypt(t *testing.T) { sv.Name = "sv-test" sv.Namespace = "default" - newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) - exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test") + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test") require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.Empty(t, exposed) }) @@ -274,7 +276,7 @@ func TestIntegrationDecrypt(t *testing.T) { authCtx := createAuthContext(ctx, "default", []string{"wrong.group/securevalues/" + svName + ":decrypt"}, svcIdentity, types.TypeUser) // Setup service - decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}}) + sut := testutils.Setup(t) // Create a secure value spec := secretv0alpha1.SecureValueSpec{ @@ -286,9 +288,10 @@ func TestIntegrationDecrypt(t *testing.T) { sv.Name = svName sv.Namespace = "default" - newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) - exposed, err := decryptSvc.Decrypt(authCtx, "default", svName) + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName) require.Error(t, err) require.Equal(t, err.Error(), "not authorized") require.Empty(t, exposed) @@ -297,62 +300,6 @@ func TestIntegrationDecrypt(t *testing.T) { // TODO: add more tests for keeper failure scenarios, lets see how the async work will change this though. } -func setupDecryptTestService(t *testing.T, allowList map[string]struct{}) (*decryptStorage, contracts.SecureValueMetadataStorage, *secretkeeper.OSSKeeperService, contracts.KeeperMetadataStorage) { - t.Helper() - - // Initialize infra dependencies - cfg := &setting.Cfg{ - SecretsManagement: setting.SecretsManagerSettings{ - SecretKey: "sdDkslslld", - EncryptionProvider: "secretKey.v1", - }, - } - - features := featuremgmt.WithFeatures( - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, - featuremgmt.FlagSecretsManagementAppPlatform, - ) - - db := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - database := database.ProvideDatabase(db, tracer) - - // Initialize encryption manager and storage - dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil) - require.NoError(t, err) - - encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features) - require.NoError(t, err) - - encryptionManager, err := encryptionmanager.ProvideEncryptionManager( - tracer, - dataKeyStore, - cfg, - &usagestats.UsageStatsMock{}, - encryption.ProviderMap{}, - ) - require.NoError(t, err) - - // Initialize the keeper service - keeperService, err := secretkeeper.ProvideService(tracer, encValueStore, encryptionManager, nil) - require.NoError(t, err) - - keeperMetadataStorage, err := ProvideKeeperMetadataStorage(database, tracer, features, nil) - require.NoError(t, err) - - // Initialize the secure value storage - secureValueMetadataStorage, err := ProvideSecureValueMetadataStorage(database, tracer, features, nil) - require.NoError(t, err) - - decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, allowList) - - // Initialize the decrypt storage - decryptSvc, err := ProvideDecryptStorage(features, tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil) - require.NoError(t, err) - - return decryptSvc.(*decryptStorage), secureValueMetadataStorage, keeperService, keeperMetadataStorage -} - func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context { requester := &identity.StaticRequester{ Type: identityType, @@ -371,32 +318,3 @@ func createAuthContext(ctx context.Context, namespace string, permissions []stri return types.WithAuthInfo(ctx, requester) } - -// This helper will also delete the secureValue from the db when the test is done. -func newTestSecureValue(ctx context.Context, t *testing.T, db contracts.SecureValueMetadataStorage, keeperService *secretkeeper.OSSKeeperService, keeperMetadataStorage contracts.KeeperMetadataStorage, sv *secretv0alpha1.SecureValue, actorUID string) { - t.Helper() - - _, err := db.Create(ctx, sv, actorUID) - require.NoError(t, err) - - require.NoError(t, err) - - // Since creating secrets is async, store the secret in the keeper synchronously to make testing easier - cfg, err := keeperMetadataStorage.GetKeeperConfig(ctx, sv.Namespace, sv.Spec.Keeper, contracts.ReadOpts{}) - require.NoError(t, err) - - keeper, err := keeperService.KeeperForConfig(cfg) - require.NoError(t, err) - - externalID, err := keeper.Store(ctx, cfg, sv.Namespace, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) - require.NoError(t, err) - - // Set external id for the secure value - err = db.SetExternalID(ctx, xkube.Namespace(sv.Namespace), sv.Name, externalID) - require.NoError(t, err) - - t.Cleanup(func() { - require.NoError(t, keeper.Delete(ctx, cfg, sv.Namespace, externalID)) - require.NoError(t, db.Delete(ctx, xkube.Namespace(sv.Namespace), sv.Name)) - }) -} diff --git a/pkg/storage/secret/metadata/keeper_store_test.go b/pkg/storage/secret/metadata/keeper_store_test.go index 644b3ccf714..c5da14eaf2d 100644 --- a/pkg/storage/secret/metadata/keeper_store_test.go +++ b/pkg/storage/secret/metadata/keeper_store_test.go @@ -1,4 +1,4 @@ -package metadata +package metadata_test import ( "context" @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/storage/secret/database" + "github.com/grafana/grafana/pkg/storage/secret/metadata" "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" @@ -340,7 +341,7 @@ func initStorage(t *testing.T) contracts.KeeperMetadataStorage { features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) // Initialize the keeper storage - keeperMetadataStorage, err := ProvideKeeperMetadataStorage(db, tracer, features, nil) + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(db, tracer, features, nil) require.NoError(t, err) return keeperMetadataStorage } diff --git a/pkg/storage/secret/metadata/metrics/metrics.go b/pkg/storage/secret/metadata/metrics/metrics.go index bffbaebc604..bedaa03b4f8 100644 --- a/pkg/storage/secret/metadata/metrics/metrics.go +++ b/pkg/storage/secret/metadata/metrics/metrics.go @@ -13,16 +13,6 @@ const ( // StorageMetrics is a struct that contains all the metrics for all operations of secrets storage. type StorageMetrics struct { - OutboxAppendDuration *prometheus.HistogramVec - OutboxReceiveDuration prometheus.Histogram - OutboxAppendCount *prometheus.CounterVec - OutboxReceiveCount prometheus.Counter - OutboxDeleteDuration prometheus.Histogram - OutboxDeleteCount prometheus.Counter - OutboxIncrementReceiveCountDuration prometheus.Histogram - OutboxIncrementReceiveCountCount prometheus.Counter - OutboxTotalMessageLifetimeDuration *prometheus.HistogramVec - KeeperMetadataCreateDuration *prometheus.HistogramVec KeeperMetadataCreateCount *prometheus.CounterVec KeeperMetadataUpdateDuration *prometheus.HistogramVec @@ -55,67 +45,6 @@ type StorageMetrics struct { func newStorageMetrics() *StorageMetrics { return &StorageMetrics{ - // Outbox metrics - OutboxAppendDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_append_duration_seconds", - Help: "Duration of outbox message append operations", - Buckets: prometheus.DefBuckets, - }, []string{"message_type"}), - OutboxAppendCount: prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_append_count", - Help: "Count of outbox message append operations", - }, []string{"message_type"}), - OutboxReceiveDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_receive_duration_seconds", - Help: "Duration of outbox message receive operations", - Buckets: prometheus.DefBuckets, - }), - OutboxReceiveCount: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_receive_count", - Help: "Count of outbox message receive operations", - }), - OutboxDeleteDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_delete_duration_seconds", - Help: "Duration of outbox message delete operations", - Buckets: prometheus.DefBuckets, - }), - OutboxDeleteCount: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_delete_count", - Help: "Count of outbox message delete operations", - }), - OutboxIncrementReceiveCountDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_increment_receive_count_duration_seconds", - Help: "Duration of outbox message increment receive count operations", - Buckets: prometheus.DefBuckets, - }), - OutboxIncrementReceiveCountCount: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_increment_receive_count_count", - Help: "Count of outbox message increment receive count operations", - }), - OutboxTotalMessageLifetimeDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_total_message_lifetime_duration_seconds", - Help: "Total duration of outbox message lifetime", - Buckets: prometheus.DefBuckets, - }, []string{"message_type"}), - // Keeper metrics KeeperMetadataCreateDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: namespace, @@ -307,15 +236,6 @@ func NewStorageMetrics(reg prometheus.Registerer) *StorageMetrics { if reg != nil { reg.MustRegister( - m.OutboxAppendDuration, - m.OutboxAppendCount, - m.OutboxReceiveDuration, - m.OutboxReceiveCount, - m.OutboxDeleteDuration, - m.OutboxDeleteCount, - m.OutboxIncrementReceiveCountDuration, - m.OutboxIncrementReceiveCountCount, - m.OutboxTotalMessageLifetimeDuration, m.KeeperMetadataCreateDuration, m.KeeperMetadataCreateCount, m.KeeperMetadataUpdateDuration, diff --git a/pkg/storage/secret/metadata/outbox_store.go b/pkg/storage/secret/metadata/outbox_store.go deleted file mode 100644 index efe569384f4..00000000000 --- a/pkg/storage/secret/metadata/outbox_store.go +++ /dev/null @@ -1,397 +0,0 @@ -package metadata - -import ( - "context" - "database/sql" - "fmt" - "time" - - "github.com/grafana/grafana/pkg/storage/secret/metadata/metrics" - unifiedsql "github.com/grafana/grafana/pkg/storage/unified/sql" - "github.com/prometheus/client_golang/prometheus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" - - "github.com/grafana/grafana/pkg/registry/apis/secret/assert" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" -) - -type outboxStore struct { - db contracts.Database - dialect sqltemplate.Dialect - tracer trace.Tracer - metrics *metrics.StorageMetrics -} - -func ProvideOutboxQueue( - db contracts.Database, - tracer trace.Tracer, - reg prometheus.Registerer, -) contracts.OutboxQueue { - return &outboxStore{ - db: db, - dialect: sqltemplate.DialectForDriver(db.DriverName()), - metrics: metrics.NewStorageMetrics(reg), - tracer: tracer, - } -} - -type outboxMessageDB struct { - RequestID string - MessageID int64 - MessageType contracts.OutboxMessageType - Name string - Namespace string - EncryptedSecret sql.NullString - KeeperName sql.NullString - ExternalID sql.NullString - ReceiveCount int - Created int64 -} - -func (s *outboxStore) Append(ctx context.Context, input contracts.AppendOutboxMessage) (messageID int64, err error) { - ctx, span := s.tracer.Start(ctx, "outboxStore.Append", trace.WithAttributes( - attribute.String("name", input.Name), - attribute.String("namespace", input.Namespace), - attribute.String("type", string(input.Type)), - attribute.String("requestID", input.RequestID), - )) - defer span.End() - - defer func() { - if err != nil { - span.SetStatus(codes.Error, "failed to append outbox message") - span.RecordError(err) - } - - if messageID != 0 { - span.SetAttributes(attribute.Int64("messageID", messageID)) - } - }() - - assert.True(input.Type != "", "outboxStore.Append: outbox message type is required") - - start := time.Now() - messageID, err = s.insertMessage(ctx, input) - if err != nil { - return messageID, fmt.Errorf("inserting message into outbox table: %+w", err) - } - - s.metrics.OutboxAppendDuration.WithLabelValues(string(input.Type)).Observe(time.Since(start).Seconds()) - s.metrics.OutboxAppendCount.WithLabelValues(string(input.Type)).Inc() - - return messageID, nil -} - -func (s *outboxStore) insertMessage(ctx context.Context, input contracts.AppendOutboxMessage) (int64, error) { - keeperName := sql.NullString{} - if input.KeeperName != nil { - keeperName = sql.NullString{ - Valid: true, - String: *input.KeeperName, - } - } - - externalID := sql.NullString{} - if input.ExternalID != nil { - externalID = sql.NullString{ - Valid: true, - String: *input.ExternalID, - } - } - - encryptedSecret := sql.NullString{} - if input.Type == contracts.CreateSecretOutboxMessage || input.Type == contracts.UpdateSecretOutboxMessage { - encryptedSecret = sql.NullString{ - Valid: true, - String: input.EncryptedSecret, - } - } - - req := appendSecureValueOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - Row: &outboxMessageDB{ - RequestID: input.RequestID, - MessageType: input.Type, - Name: input.Name, - Namespace: input.Namespace, - EncryptedSecret: encryptedSecret, - KeeperName: keeperName, - ExternalID: externalID, - ReceiveCount: 0, - Created: time.Now().UTC().UnixMilli(), - }, - } - - query, err := sqltemplate.Execute(sqlSecureValueOutboxAppend, req) - if err != nil { - return 0, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxAppend.Name(), err) - } - - result, err := s.db.ExecContext(ctx, query, req.GetArgs()...) - if err != nil { - if unifiedsql.IsRowAlreadyExistsError(err) { - return 0, contracts.ErrSecureValueOperationInProgress - } - return 0, fmt.Errorf("inserting message into secure value outbox table: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return 0, fmt.Errorf("get rows affected: %w", err) - } - - if rowsAffected != 1 { - return 0, fmt.Errorf("expected to affect 1 row, but affected %d", rowsAffected) - } - - id, err := result.LastInsertId() - if err != nil { - return id, fmt.Errorf("fetching last inserted id: %w", err) - } - - return id, nil -} - -func (s *outboxStore) ReceiveN(ctx context.Context, limit uint) ([]contracts.OutboxMessage, error) { - messageIDs, err := s.fetchMessageIdsInQueue(ctx, limit) - if err != nil { - return nil, fmt.Errorf("fetching message ids from queue: %w", err) - } - // If queue is empty - if len(messageIDs) == 0 { - return nil, nil - } - req := receiveNSecureValueOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - MessageIDs: messageIDs, - } - - start := time.Now() - query, err := sqltemplate.Execute(sqlSecureValueOutboxReceiveN, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxReceiveN.Name(), err) - } - - rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return nil, fmt.Errorf("fetching rows from secure value outbox table: %w", err) - } - defer func() { _ = rows.Close() }() - - messages := make([]contracts.OutboxMessage, 0) - - for rows.Next() { - var row outboxMessageDB - if err := rows.Scan( - &row.RequestID, - &row.MessageID, - &row.MessageType, - &row.Name, - &row.Namespace, - &row.EncryptedSecret, - &row.KeeperName, - &row.ExternalID, - &row.ReceiveCount, - &row.Created, - ); err != nil { - return nil, fmt.Errorf("scanning row from secure value outbox table: %w", err) - } - - var keeperName *string - if row.KeeperName.Valid { - keeperName = &row.KeeperName.String - } - - var externalID *string - if row.ExternalID.Valid { - externalID = &row.ExternalID.String - } - - msg := contracts.OutboxMessage{ - RequestID: row.RequestID, - Type: row.MessageType, - MessageID: row.MessageID, - Name: row.Name, - Namespace: row.Namespace, - KeeperName: keeperName, - ExternalID: externalID, - ReceiveCount: row.ReceiveCount, - Created: row.Created, - } - - if row.MessageType != contracts.DeleteSecretOutboxMessage && row.EncryptedSecret.Valid { - msg.EncryptedSecret = row.EncryptedSecret.String - } - - messages = append(messages, msg) - } - - if err := rows.Err(); err != nil { - return messages, fmt.Errorf("reading rows: %w", err) - } - - s.metrics.OutboxReceiveDuration.Observe(time.Since(start).Seconds()) - s.metrics.OutboxReceiveCount.Add(float64(len(messages))) - - return messages, nil -} - -func (s *outboxStore) fetchMessageIdsInQueue(ctx context.Context, limit uint) ([]int64, error) { - req := fetchMessageIDsOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - ReceiveLimit: limit, - } - - query, err := sqltemplate.Execute(sqlSecureValueOutboxFetchMessageIDs, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxFetchMessageIDs.Name(), err) - } - - rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return nil, fmt.Errorf("fetching rows from secure value outbox table: %w", err) - } - defer func() { _ = rows.Close() }() - - messageIDs := make([]int64, 0, limit) - - for rows.Next() { - var id int64 - if err := rows.Scan(&id); err != nil { - return nil, fmt.Errorf("scanning row; %w", err) - } - messageIDs = append(messageIDs, id) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("reading rows: %w", err) - } - - return messageIDs, nil -} - -func (s *outboxStore) Delete(ctx context.Context, messageID int64) (err error) { - ctx, span := s.tracer.Start(ctx, "outboxStore.Append", trace.WithAttributes( - attribute.Int64("messageID", messageID), - )) - defer span.End() - - defer func() { - if err != nil { - span.SetStatus(codes.Error, "failed to delete message from outbox") - span.RecordError(err) - } - }() - - assert.True(messageID != 0, "outboxStore.Delete: messageID is required") - - start := time.Now() - if err := s.deleteMessage(ctx, messageID); err != nil { - return fmt.Errorf("deleting message from outbox table %+w", err) - } - - s.metrics.OutboxDeleteDuration.Observe(time.Since(start).Seconds()) - s.metrics.OutboxDeleteCount.Inc() - - return nil -} - -func (s *outboxStore) deleteMessage(ctx context.Context, messageID int64) error { - tsReq := getOutboxMessageTimestamp{ - SQLTemplate: sqltemplate.New(s.dialect), - MessageID: messageID, - } - - // First query the object so we can get the timestamp and calculate the total lifetime - timestampQuery, err := sqltemplate.Execute(sqlSecureValueOutboxQueryTimestamp, tsReq) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxQueryTimestamp.Name(), err) - } - - rows, err := s.db.QueryContext(ctx, timestampQuery, tsReq.GetArgs()...) - if err != nil { - return fmt.Errorf("querying timestamp from secure value outbox table: %w", err) - } - - if !rows.Next() { - _ = rows.Close() - return fmt.Errorf("no row found for message id=%v", messageID) - } - - var timestamp int64 - var messageType string - if err := rows.Scan(×tamp, &messageType); err != nil { - _ = rows.Close() - return fmt.Errorf("scanning timestamp: %w", err) - } - - // Explicitly close rows and check for errors before proceeding - if err := rows.Close(); err != nil { - return fmt.Errorf("closing rows: %w", err) - } - - if err := rows.Err(); err != nil { - return fmt.Errorf("rows error: %w", err) - } - - totalLifetime := time.Since(time.UnixMilli(timestamp)) - s.metrics.OutboxTotalMessageLifetimeDuration.WithLabelValues(messageType).Observe(totalLifetime.Seconds()) - - // Then delete the object - delReq := deleteSecureValueOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - MessageID: messageID, - } - - query, err := sqltemplate.Execute(sqlSecureValueOutboxDelete, delReq) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxDelete.Name(), err) - } - - result, err := s.db.ExecContext(ctx, query, delReq.GetArgs()...) - if err != nil { - return fmt.Errorf("deleting message id=%v from secure value outbox table: %w", messageID, err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("get rows affected: %w", err) - } - - // TODO: Presumably it's a bug if we delete 0 rows? - if rowsAffected > 1 { - return fmt.Errorf("bug: deleted more than one row from the outbox table, should delete only one at a time: deleted=%v", rowsAffected) - } - - return nil -} - -func (s *outboxStore) IncrementReceiveCount(ctx context.Context, messageIDs []int64) error { - if len(messageIDs) == 0 { - return nil - } - - req := incrementReceiveCountOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - MessageIDs: messageIDs, - } - - start := time.Now() - query, err := sqltemplate.Execute(sqlSecureValueOutboxUpdateReceiveCount, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxUpdateReceiveCount.Name(), err) - } - - _, err = s.db.ExecContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("updating outbox messages receive count: %w", err) - } - - s.metrics.OutboxIncrementReceiveCountDuration.Observe(time.Since(start).Seconds()) - s.metrics.OutboxIncrementReceiveCountCount.Add(float64(len(messageIDs))) - - return nil -} diff --git a/pkg/storage/secret/metadata/outbox_store_test.go b/pkg/storage/secret/metadata/outbox_store_test.go deleted file mode 100644 index f757e8b6b38..00000000000 --- a/pkg/storage/secret/metadata/outbox_store_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package metadata - -import ( - "context" - "fmt" - "math/rand" - "slices" - "testing" - "time" - - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/secret/database" - "github.com/grafana/grafana/pkg/storage/secret/migrator" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace/noop" -) - -type outboxStoreModel struct { - rows []contracts.OutboxMessage -} - -func newOutboxStoreModel() *outboxStoreModel { - return &outboxStoreModel{} -} - -func (model *outboxStoreModel) Append(messageID int64, message contracts.AppendOutboxMessage) { - model.rows = append(model.rows, contracts.OutboxMessage{ - Type: message.Type, - MessageID: messageID, - Name: message.Name, - Namespace: message.Namespace, - EncryptedSecret: message.EncryptedSecret, - KeeperName: message.KeeperName, - ExternalID: message.ExternalID, - }) -} - -func (model *outboxStoreModel) ReceiveN(n uint) []contracts.OutboxMessage { - maxMessages := min(len(model.rows), int(n)) - if maxMessages == 0 { - return nil - } - return model.rows[:maxMessages] -} - -func (model *outboxStoreModel) Delete(messageID int64) { - oldLen := len(model.rows) - model.rows = slices.DeleteFunc(model.rows, func(m contracts.OutboxMessage) bool { - return m.MessageID == messageID - }) - if len(model.rows) != oldLen-1 { - panic("Delete: deleted more than one message") - } -} - -func TestOutboxStoreModel(t *testing.T) { - t.Parallel() - - model := newOutboxStoreModel() - - require.Empty(t, model.ReceiveN(10)) - - appendOutboxMessage := contracts.AppendOutboxMessage{ - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-1", - EncryptedSecret: "value", - ExternalID: nil, - } - - outboxMessage1 := contracts.OutboxMessage{ - MessageID: 1, - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-1", - EncryptedSecret: "value", - ExternalID: nil, - } - - outboxMessage2 := contracts.OutboxMessage{ - MessageID: 2, - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-1", - EncryptedSecret: "value", - ExternalID: nil, - } - - model.Append(1, appendOutboxMessage) - - require.Equal(t, []contracts.OutboxMessage{outboxMessage1}, model.ReceiveN(10)) - - model.Append(2, appendOutboxMessage) - - require.Equal(t, []contracts.OutboxMessage{outboxMessage1, outboxMessage2}, model.ReceiveN(10)) - - model.Delete(outboxMessage1.MessageID) - - require.Equal(t, []contracts.OutboxMessage{outboxMessage2}, model.ReceiveN(5)) - - model.Delete(outboxMessage2.MessageID) - - require.Empty(t, model.ReceiveN(1)) -} - -func TestOutboxStoreSecureValueOperationInProgress(t *testing.T) { - t.Parallel() - - t.Run("Append returns error when the queue already contains an operation for the secure value", func(t *testing.T) { - t.Parallel() - - testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - - ctx := context.Background() - - outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil) - - _, err := outbox.Append(ctx, contracts.AppendOutboxMessage{ - RequestID: "1", - Type: contracts.CreateSecretOutboxMessage, - Name: "name1", - Namespace: "ns1", - EncryptedSecret: "v1", - KeeperName: nil, - ExternalID: nil, - }) - require.NoError(t, err) - - _, err = outbox.Append(ctx, contracts.AppendOutboxMessage{ - RequestID: "1", - Type: contracts.UpdateSecretOutboxMessage, - Name: "name1", - Namespace: "ns1", - EncryptedSecret: "v1", - KeeperName: nil, - ExternalID: nil, - }) - - require.ErrorIs(t, err, contracts.ErrSecureValueOperationInProgress) - }) -} - -func TestOutboxStore(t *testing.T) { - testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - - ctx := context.Background() - - outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil) - - m1 := contracts.AppendOutboxMessage{ - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-1", - EncryptedSecret: "value", - ExternalID: nil, - } - m2 := contracts.AppendOutboxMessage{ - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-2", - EncryptedSecret: "value", - ExternalID: nil, - } - - messages, err := outbox.ReceiveN(ctx, 10) - require.NoError(t, err) - require.Empty(t, messages) - - messageID1, err := outbox.Append(ctx, m1) - require.NoError(t, err) - - for range 2 { - messages, err = outbox.ReceiveN(ctx, 10) - require.NoError(t, err) - require.Equal(t, 1, len(messages)) - require.Equal(t, messageID1, messages[0].MessageID) - } - - messageID2, err := outbox.Append(ctx, m2) - require.NoError(t, err) - - messages, err = outbox.ReceiveN(ctx, 3) - require.NoError(t, err) - require.Equal(t, 2, len(messages)) - require.Equal(t, messageID1, messages[0].MessageID) - require.Equal(t, messageID2, messages[1].MessageID) - - require.NoError(t, outbox.Delete(ctx, messageID1)) - messages, err = outbox.ReceiveN(ctx, 10) - require.NoError(t, err) - require.Equal(t, 1, len(messages)) - require.Equal(t, messageID2, messages[0].MessageID) - - require.NoError(t, outbox.Delete(ctx, messageID2)) - - messages, err = outbox.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Empty(t, messages) -} - -func TestOutboxStoreProperty(t *testing.T) { - seed := time.Now().UnixMicro() - rng := rand.New(rand.NewSource(seed)) - - defer func() { - if t.Failed() { - fmt.Printf("TestOutboxStoreProperty: SEED=%+v\n\n", seed) - } - }() - - // The number of iterations was decided arbitrarily based on the time the test takes to run - for range 10 { - testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - - outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil) - - model := newOutboxStoreModel() - - ctx := context.Background() - - for i := range 100 { - n := rng.Intn(3) - switch n { - case 0: - time.Sleep(1 * time.Microsecond) - message := contracts.AppendOutboxMessage{ - Type: contracts.CreateSecretOutboxMessage, - Name: fmt.Sprintf("s-%d", i), - Namespace: fmt.Sprintf("n-%d", i), - EncryptedSecret: "value", - ExternalID: nil, - } - messageID, err := outbox.Append(ctx, message) - require.NoError(t, err) - - model.Append(messageID, message) - - case 1: - n := uint(rng.Intn(10)) - messages, err := outbox.ReceiveN(ctx, n) - require.NoError(t, err) - - modelMessages := model.ReceiveN(n) - - require.Equal(t, len(modelMessages), len(messages)) - for i := range len(modelMessages) { - require.Equal(t, modelMessages[i].MessageID, messages[i].MessageID) - } - - case 2: - if len(model.rows) == 0 { - continue - } - - message := model.rows[rng.Intn(len(model.rows))] - - model.Delete(message.MessageID) - require.NoError(t, outbox.Delete(ctx, message.MessageID)) - - default: - panic(fmt.Sprintf("unhandled action: %+v", n)) - } - } - } -} diff --git a/pkg/storage/secret/metadata/query.go b/pkg/storage/secret/metadata/query.go index 907a4d6cbea..2c85d762b87 100644 --- a/pkg/storage/secret/metadata/query.go +++ b/pkg/storage/secret/metadata/query.go @@ -27,18 +27,12 @@ var ( sqlSecureValueRead = mustTemplate("secure_value_read.sql") sqlSecureValueList = mustTemplate("secure_value_list.sql") sqlSecureValueCreate = mustTemplate("secure_value_create.sql") - sqlSecureValueDelete = mustTemplate("secure_value_delete.sql") - sqlSecureValueUpdate = mustTemplate("secure_value_update.sql") sqlSecureValueUpdateExternalId = mustTemplate("secure_value_updateExternalId.sql") - sqlSecureValueUpdateStatus = mustTemplate("secure_value_updateStatus.sql") sqlSecureValueReadForDecrypt = mustTemplate("secure_value_read_for_decrypt.sql") - sqlSecureValueOutboxAppend = mustTemplate("secure_value_outbox_append.sql") - sqlSecureValueOutboxFetchMessageIDs = mustTemplate("secure_value_outbox_fetch_message_ids.sql") - sqlSecureValueOutboxReceiveN = mustTemplate("secure_value_outbox_receiveN.sql") - sqlSecureValueOutboxDelete = mustTemplate("secure_value_outbox_delete.sql") - sqlSecureValueOutboxUpdateReceiveCount = mustTemplate("secure_value_outbox_update_receive_count.sql") - sqlSecureValueOutboxQueryTimestamp = mustTemplate("secure_value_outbox_query_timestamp.sql") + sqlGetLatestSecureValueVersion = mustTemplate("secure_value_get_latest_version.sql") + sqlSecureValueSetVersionToActive = mustTemplate("secure_value_set_version_to_active.sql") + sqlSecureValueSetVersionToInactive = mustTemplate("secure_value_set_version_to_inactive.sql") ) func mustTemplate(filename string) *template.Template { @@ -150,6 +144,38 @@ func (r readSecureValue) Validate() error { return nil // TODO } +type getLatestSecureValueVersion struct { + sqltemplate.SQLTemplate + Namespace string + Name string +} + +func (r getLatestSecureValueVersion) Validate() error { + return nil +} + +type secureValueSetVersionToActive struct { + sqltemplate.SQLTemplate + Namespace string + Name string + Version int64 +} + +func (r secureValueSetVersionToActive) Validate() error { + return nil +} + +type secureValueSetVersionToInactive struct { + sqltemplate.SQLTemplate + Namespace string + Name string + Version int64 +} + +func (r secureValueSetVersionToInactive) Validate() error { + return nil +} + type listSecureValue struct { sqltemplate.SQLTemplate Namespace string @@ -170,23 +196,12 @@ func (r createSecureValue) Validate() error { return nil // TODO } -// Delete -type deleteSecureValue struct { - sqltemplate.SQLTemplate - Namespace string - Name string -} - -// Validate is only used if we use `dbutil` from `unifiedstorage` -func (r deleteSecureValue) Validate() error { - return nil // TODO -} - // Update externalId type updateExternalIdSecureValue struct { sqltemplate.SQLTemplate Namespace string Name string + Version int64 ExternalID string } @@ -195,33 +210,6 @@ func (r updateExternalIdSecureValue) Validate() error { return nil // TODO } -// Update secure value -type updateSecureValue struct { - sqltemplate.SQLTemplate - Namespace string - Name string - Row *secureValueDB -} - -// Validate is only used if we use `dbutil` from `unifiedstorage` -func (r updateSecureValue) Validate() error { - return nil // TODO -} - -// update status message -type updateStatusSecureValue struct { - sqltemplate.SQLTemplate - Namespace string - Name string - Phase string - Message string -} - -// Validate is only used if we use `dbutil` from `unifiedstorage` -func (r updateStatusSecureValue) Validate() error { - return nil // TODO -} - type readSecureValueForDecrypt struct { sqltemplate.SQLTemplate Namespace string @@ -229,48 +217,3 @@ type readSecureValueForDecrypt struct { } func (r readSecureValueForDecrypt) Validate() error { return nil } - -/*************************************/ -/**-- Secure Value Outbox Queries --**/ -/*************************************/ -type appendSecureValueOutbox struct { - sqltemplate.SQLTemplate - Row *outboxMessageDB -} - -func (appendSecureValueOutbox) Validate() error { return nil } - -type receiveNSecureValueOutbox struct { - sqltemplate.SQLTemplate - MessageIDs []int64 -} - -func (receiveNSecureValueOutbox) Validate() error { return nil } - -type fetchMessageIDsOutbox struct { - sqltemplate.SQLTemplate - ReceiveLimit uint -} - -func (fetchMessageIDsOutbox) Validate() error { return nil } - -type deleteSecureValueOutbox struct { - sqltemplate.SQLTemplate - MessageID int64 -} - -func (deleteSecureValueOutbox) Validate() error { return nil } - -type getOutboxMessageTimestamp struct { - sqltemplate.SQLTemplate - MessageID int64 -} - -func (getOutboxMessageTimestamp) Validate() error { return nil } - -type incrementReceiveCountOutbox struct { - sqltemplate.SQLTemplate - MessageIDs []int64 -} - -func (incrementReceiveCountOutbox) Validate() error { return nil } diff --git a/pkg/storage/secret/metadata/query_test.go b/pkg/storage/secret/metadata/query_test.go index 8a5ff0e6a9a..dce469d6d87 100644 --- a/pkg/storage/secret/metadata/query_test.go +++ b/pkg/storage/secret/metadata/query_test.go @@ -1,7 +1,6 @@ package metadata import ( - "database/sql" "testing" "text/template" @@ -123,6 +122,27 @@ func TestSecureValueQueries(t *testing.T) { mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ RootDir: "testdata", Templates: map[*template.Template][]mocks.TemplateTestCase{ + sqlGetLatestSecureValueVersion: { + { + Name: "get latest secure value version", + Data: &getLatestSecureValueVersion{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Name: "name", + Namespace: "ns", + }, + }, + }, + sqlSecureValueSetVersionToActive: { + { + Name: "set secure value version to active", + Data: &secureValueSetVersionToActive{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Name: "name", + Namespace: "ns", + Version: 1, + }, + }, + }, sqlSecureValueRead: { { Name: "read", @@ -166,8 +186,7 @@ func TestSecureValueQueries(t *testing.T) { CreatedBy: "user:ryan", Updated: 5678, UpdatedBy: "user:cameron", - Phase: "creating", - Message: toNullString(nil), + Version: 1, Description: "description", Keeper: toNullString(nil), Decrypters: toNullString(nil), @@ -190,72 +209,7 @@ func TestSecureValueQueries(t *testing.T) { CreatedBy: "user:ryan", Updated: 5678, UpdatedBy: "user:cameron", - Phase: "creating", - Message: toNullString(ptr.To("message_test")), - Description: "description", - Keeper: toNullString(ptr.To("keeper_test")), - Decrypters: toNullString(ptr.To("decrypters_test")), - Ref: toNullString(ptr.To("ref_test")), - ExternalID: "extId", - }, - }, - }, - }, - sqlSecureValueDelete: { - { - Name: "delete", - Data: &deleteSecureValue{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Name: "name", - Namespace: "ns", - }, - }, - }, - sqlSecureValueUpdate: { - { - Name: "update-null", - Data: &updateSecureValue{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Name: "name", - Namespace: "ns", - Row: &secureValueDB{ - GUID: "abc", - Name: "name", - Namespace: "ns", - Annotations: `{"x":"XXXX"}`, - Labels: `{"a":"AAA", "b", "BBBB"}`, - Created: 1234, - CreatedBy: "user:ryan", - Updated: 5678, - UpdatedBy: "user:cameron", - Phase: "creating", - Message: toNullString(nil), - Description: "description", - Keeper: toNullString(nil), - Decrypters: toNullString(nil), - Ref: toNullString(nil), - ExternalID: "extId", - }, - }, - }, - { - Name: "update-not-null", - Data: &updateSecureValue{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Name: "name", - Namespace: "ns", - Row: &secureValueDB{ - GUID: "abc", - Name: "name", - Namespace: "ns", - Annotations: `{"x":"XXXX"}`, - Labels: `{"a":"AAA", "b", "BBBB"}`, - Created: 1234, - CreatedBy: "user:ryan", - Updated: 5678, - UpdatedBy: "user:cameron", - Phase: "creating", - Message: toNullString(ptr.To("message_test")), + Version: 1, Description: "description", Keeper: toNullString(ptr.To("keeper_test")), Decrypters: toNullString(ptr.To("decrypters_test")), @@ -276,18 +230,6 @@ func TestSecureValueQueries(t *testing.T) { }, }, }, - sqlSecureValueUpdateStatus: { - { - Name: "updateStatus", - Data: &updateStatusSecureValue{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Name: "name", - Namespace: "ns", - Phase: "Succeeded", - Message: "message-1", - }, - }, - }, sqlSecureValueReadForDecrypt: { { Name: "read-for-decrypt", @@ -301,111 +243,3 @@ func TestSecureValueQueries(t *testing.T) { }, }) } - -func TestSecureValueOutboxQueries(t *testing.T) { - mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ - RootDir: "testdata", - Templates: map[*template.Template][]mocks.TemplateTestCase{ - sqlSecureValueOutboxUpdateReceiveCount: { - { - - Name: "update-receive-count", - Data: &incrementReceiveCountOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - MessageIDs: []int64{1, 2, 3}, - }, - }, - }, - sqlSecureValueOutboxAppend: { - { - Name: "no-encrypted-secret", - Data: &appendSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Row: &outboxMessageDB{ - MessageID: 1, - MessageType: "some-type", - Name: "name", - Namespace: "namespace", - ExternalID: sql.NullString{Valid: true, String: "external-id"}, - KeeperName: sql.NullString{Valid: true, String: "keeper"}, - Created: 1234, - }, - }, - }, - { - Name: "no-external-id", - Data: &appendSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Row: &outboxMessageDB{ - MessageID: 1, - MessageType: "some-type", - Name: "name", - Namespace: "namespace", - EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"}, - KeeperName: sql.NullString{Valid: true, String: "keeper"}, - Created: 1234, - }, - }, - }, - { - Name: "no-keeper-name", - Data: &appendSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Row: &outboxMessageDB{ - MessageID: 1, - MessageType: "some-type", - Name: "name", - Namespace: "namespace", - EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"}, - ExternalID: sql.NullString{Valid: true, String: "external-id"}, - Created: 1234, - }, - }, - }, - { - Name: "all-fields-present", - Data: &appendSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Row: &outboxMessageDB{ - MessageID: 1, - MessageType: "some-type", - Name: "name", - Namespace: "namespace", - EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"}, - ExternalID: sql.NullString{Valid: true, String: ""}, // can be empty string - KeeperName: sql.NullString{Valid: true, String: "keeper"}, - Created: 1234, - }, - }, - }, - }, - sqlSecureValueOutboxFetchMessageIDs: { - { - Name: "basic", - Data: &fetchMessageIDsOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - ReceiveLimit: 10, - }, - }, - }, - sqlSecureValueOutboxReceiveN: { - { - Name: "basic", - Data: &receiveNSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - MessageIDs: []int64{1, 2, 3}, - }, - }, - }, - sqlSecureValueOutboxDelete: { - { - Name: "basic", - Data: &deleteSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - MessageID: 1, - }, - }, - }, - }, - }) -} diff --git a/pkg/storage/secret/metadata/secure_value_model.go b/pkg/storage/secret/metadata/secure_value_model.go index d90beaa6782..134128f8a25 100644 --- a/pkg/storage/secret/metadata/secure_value_model.go +++ b/pkg/storage/secret/metadata/secure_value_model.go @@ -29,8 +29,8 @@ type secureValueDB struct { UpdatedBy string // Kubernetes Status - Phase string - Message sql.NullString + Active bool + Version int64 // Spec Description string @@ -74,8 +74,8 @@ func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) { Decrypters: decrypters, }, Status: secretv0alpha1.SecureValueStatus{ - Phase: secretv0alpha1.SecureValuePhase(sv.Phase), ExternalID: sv.ExternalID, + Version: sv.Version, }, } @@ -85,10 +85,7 @@ func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) { if sv.Ref.Valid { resource.Spec.Ref = &sv.Ref.String } - if sv.Message.Valid { - resource.Status.Message = sv.Message.String - } - resource.Status.Phase = secretv0alpha1.SecureValuePhase(sv.Phase) + resource.Status.ExternalID = sv.ExternalID // Set all meta fields here for consistency. @@ -131,24 +128,6 @@ func toCreateRow(sv *secretv0alpha1.SecureValue, actorUID string) (*secureValueD return row, nil } -// toUpdateRow maps a Kubernetes resource into a DB row for existing resources being updated. -func toUpdateRow(currentRow *secureValueDB, newSecureValue *secretv0alpha1.SecureValue, actorUID, externalID string) (*secureValueDB, error) { - row, err := toRow(newSecureValue, externalID) - if err != nil { - return nil, fmt.Errorf("failed to create: %w", err) - } - - now := time.Now().UTC().Unix() - - row.GUID = currentRow.GUID - row.Created = currentRow.Created - row.CreatedBy = currentRow.CreatedBy - row.Updated = now - row.UpdatedBy = actorUID - - return row, nil -} - // toRow maps a Kubernetes resource into a DB row. func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, error) { var annotations string @@ -201,11 +180,6 @@ func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, e return nil, fmt.Errorf("failed to get resource version: %w", err) } - var statusMessage *string - if sv.Status.Message != "" { - statusMessage = &sv.Status.Message - } - return &secureValueDB{ GUID: string(sv.UID), Name: sv.Name, @@ -217,8 +191,7 @@ func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, e Updated: updatedTimestamp, UpdatedBy: meta.GetUpdatedBy(), - Phase: string(sv.Status.Phase), - Message: toNullString(statusMessage), + Version: sv.Status.Version, Description: sv.Spec.Description, Keeper: toNullString(sv.Spec.Keeper), @@ -233,6 +206,7 @@ type secureValueForDecrypt struct { Keeper sql.NullString Decrypters sql.NullString Ref sql.NullString + Active bool ExternalID string } diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index 6f79dfbd353..ecc22df28bd 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -55,33 +55,18 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp )) defer span.End() - sv.Status.Phase = secretv0alpha1.SecureValuePhasePending - sv.Status.Message = "Creating secure value" + // Set inside of the transaction callback + var row *secureValueDB - row, err := toCreateRow(sv, actorUID) - if err != nil { - return nil, fmt.Errorf("to create row: %w", err) - } - - req := createSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Row: row, - } - - query, err := sqltemplate.Execute(sqlSecureValueCreate, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueCreate.Name(), err) - } - - err = s.db.Transaction(ctx, func(ctx context.Context) error { - if row.Keeper.Valid { + err := s.db.Transaction(ctx, func(ctx context.Context) error { + if sv.Spec.Keeper != nil { // Validate before inserting that the chosen `keeper` exists. // -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage req := &readKeeper{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: row.Namespace, - Name: row.Keeper.String, + Namespace: sv.Namespace, + Name: *sv.Spec.Keeper, IsForUpdate: true, } @@ -101,23 +86,61 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp } } - res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) + latestVersion, err := s.getLatestVersion(ctx, xkube.Namespace(sv.Namespace), sv.Name) if err != nil { - if sql.IsRowAlreadyExistsError(err) { - return fmt.Errorf("namespace=%+v name=%+v %w", sv.Namespace, sv.Name, contracts.ErrSecureValueAlreadyExists) + return fmt.Errorf("fetching latest secure value version: %w", err) + } + + version := int64(1) + if latestVersion != nil { + version = *latestVersion + 1 + } + + // Some other concurrent request may have created the version we're trying to create, + // if that's the case, we'll retry with a new version up to max attempts. + maxAttempts := 3 + attempts := 0 + for { + sv.Status.Version = version + + row, err = toCreateRow(sv, actorUID) + if err != nil { + return fmt.Errorf("to create row: %w", err) } - return fmt.Errorf("inserting row: %w", err) - } - rowsAffected, err := res.RowsAffected() - if err != nil { - return fmt.Errorf("getting rows affected: %w", err) - } + req := createSecureValue{ + SQLTemplate: sqltemplate.New(s.dialect), + Row: row, + } - if rowsAffected != 1 { - return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, row.Name, row.Namespace) + query, err := sqltemplate.Execute(sqlSecureValueCreate, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlSecureValueCreate.Name(), err) + } + + res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) + if err != nil { + if sql.IsRowAlreadyExistsError(err) { + if attempts < maxAttempts { + attempts += 1 + version += 1 + continue + } + return fmt.Errorf("namespace=%+v name=%+v %w", sv.Namespace, sv.Name, contracts.ErrSecureValueAlreadyExists) + } + return fmt.Errorf("inserting row: %w", err) + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("getting rows affected: %w", err) + } + + if rowsAffected != 1 { + return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, row.Name, row.Namespace) + } + return nil } - return nil }) if err != nil { return nil, fmt.Errorf("db failure: %w", err) @@ -134,6 +157,138 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp return createdSecureValue, nil } +func (s *secureValueMetadataStorage) getLatestVersion(ctx context.Context, namespace xkube.Namespace, name string) (*int64, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.getLatestVersion", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + )) + defer span.End() + + req := getLatestSecureValueVersion{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + } + + q, err := sqltemplate.Execute(sqlGetLatestSecureValueVersion, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlGetLatestSecureValueVersion.Name(), err) + } + + rows, err := s.db.QueryContext(ctx, q, req.GetArgs()...) + if err != nil { + return nil, fmt.Errorf("fetching latest version for secure value: namespace=%+v name=%+v %w", namespace, name, err) + } + defer func() { _ = rows.Close() }() + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error executing query: %w", err) + } + + if !rows.Next() { + return nil, nil + } + + var version int64 + if err := rows.Scan(&version); err != nil { + return nil, fmt.Errorf("scanning version from returned rows: %w", err) + } + + return &version, nil +} + +func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*contracts.DecryptSecureValue, error) { + start := time.Now() + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.ReadForDecrypt", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + )) + defer span.End() + + req := readSecureValueForDecrypt{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + } + + query, err := sqltemplate.Execute(sqlSecureValueReadForDecrypt, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueReadForDecrypt.Name(), err) + } + + res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) + if err != nil { + return nil, fmt.Errorf("reading row: %w", err) + } + defer func() { _ = res.Close() }() + + var row secureValueForDecrypt + if !res.Next() { + return nil, contracts.ErrSecureValueNotFound + } + if err := res.Scan( + &row.Keeper, &row.Decrypters, + &row.Ref, &row.ExternalID, &row.Active); err != nil { + return nil, fmt.Errorf("failed to scan secure value row: %w", err) + } + + if err := res.Err(); err != nil { + return nil, fmt.Errorf("read rows error: %w", err) + } + + if !row.Active { + return nil, fmt.Errorf("bug: read an inactive version: row=%+v", row) + } + + secureValue, err := row.toDecrypt() + if err != nil { + return nil, fmt.Errorf("convert to kubernetes object: %w", err) + } + + s.metrics.SecureValueGetForDecryptDuration.Observe(time.Since(start).Seconds()) + + return secureValue, nil +} + +func (s *secureValueMetadataStorage) readActiveVersion(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (secureValueDB, error) { + req := readSecureValue{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + IsForUpdate: opts.ForUpdate, + } + + query, err := sqltemplate.Execute(sqlSecureValueRead, req) + if err != nil { + return secureValueDB{}, fmt.Errorf("execute template %q: %w", sqlSecureValueRead.Name(), err) + } + + res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) + if err != nil { + return secureValueDB{}, fmt.Errorf("reading row: %w", err) + } + defer func() { _ = res.Close() }() + + var secureValue secureValueDB + if !res.Next() { + return secureValueDB{}, contracts.ErrSecureValueNotFound + } + + if err := res.Scan( + &secureValue.GUID, &secureValue.Name, &secureValue.Namespace, + &secureValue.Annotations, &secureValue.Labels, + &secureValue.Created, &secureValue.CreatedBy, + &secureValue.Updated, &secureValue.UpdatedBy, + &secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID, &secureValue.Active, &secureValue.Version); err != nil { + return secureValueDB{}, fmt.Errorf("failed to scan secure value row: %w", err) + } + + if err := res.Err(); err != nil { + return secureValueDB{}, fmt.Errorf("read rows error: %w", err) + } + return secureValue, nil +} + func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.SecureValue, error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Read", trace.WithAttributes( @@ -143,7 +298,7 @@ func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.N )) defer span.End() - secureValue, err := s.read(ctx, namespace, name, opts) + secureValue, err := s.readActiveVersion(ctx, namespace, name, opts) if err != nil { return nil, err } @@ -159,134 +314,6 @@ func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.N return secureValueKub, nil } -func (s *secureValueMetadataStorage) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Update", trace.WithAttributes( - attribute.String("name", newSecureValue.GetName()), - attribute.String("namespace", newSecureValue.GetNamespace()), - attribute.String("actorUID", actorUID), - )) - defer span.End() - - var newRow *secureValueDB - - err := s.db.Transaction(ctx, func(ctx context.Context) error { - read, err := s.read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{ForUpdate: true}) - if err != nil { - return fmt.Errorf("reading secure value: %w", err) - } - - // TODO: Confirm the ExternalID should come from the read model. - var updateErr error - newRow, updateErr = toUpdateRow(&read, newSecureValue, actorUID, read.ExternalID) - if updateErr != nil { - return fmt.Errorf("model to update row: %w", updateErr) - } - - if newRow.Keeper.Valid { - // Validate before updating that the new `keeper` exists. - - // -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage - req := &readKeeper{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: newRow.Namespace, - Name: newRow.Keeper.String, - IsForUpdate: true, - } - - query, err := sqltemplate.Execute(sqlKeeperRead, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlKeeperRead.Name(), err) - } - - res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("getting row: %w", err) - } - defer func() { _ = res.Close() }() - - if !res.Next() { - return contracts.ErrKeeperNotFound - } - } - - req := &updateSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: newRow.Namespace, - Name: newRow.Name, - Row: newRow, - } - - query, err := sqltemplate.Execute(sqlSecureValueUpdate, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdate.Name(), err) - } - - result, err := s.db.ExecContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("updating row: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("getting rows affected: %w", err) - } - - if rowsAffected != 1 { - return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, newRow.Name, newRow.Namespace) - } - - return nil - }) - if err != nil { - return nil, fmt.Errorf("db failure: %w", err) - } - - secureValue, err := newRow.toKubernetes() - if err != nil { - return nil, fmt.Errorf("convert to kubernetes object: %w", err) - } - - s.metrics.SecureValueMetadataUpdateDuration.Observe(time.Since(start).Seconds()) - s.metrics.SecureValueMetadataUpdateCount.Inc() - - return secureValue, nil -} - -func (s *secureValueMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Delete", trace.WithAttributes( - attribute.String("name", name), - attribute.String("namespace", namespace.String()), - )) - defer span.End() - - req := deleteSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace.String(), - Name: name, - } - - query, err := sqltemplate.Execute(sqlSecureValueDelete, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueDelete.Name(), err) - } - - res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("deleting secure value row: %w", err) - } - - if rowsAffected, err := res.RowsAffected(); err != nil || rowsAffected != 1 { - return fmt.Errorf("deleting secure value rowsAffected=%d error=%w", rowsAffected, err) - } - - s.metrics.SecureValueMetadataDeleteDuration.Observe(time.Since(start).Seconds()) - s.metrics.SecureValueMetadataDeleteCount.Inc() - - return nil -} - func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) (svList []secretv0alpha1.SecureValue, error error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.List", trace.WithAttributes( @@ -323,15 +350,18 @@ func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.N &row.Labels, &row.Created, &row.CreatedBy, &row.Updated, &row.UpdatedBy, - &row.Phase, &row.Message, &row.Description, &row.Keeper, &row.Decrypters, - &row.Ref, &row.ExternalID, + &row.Ref, &row.ExternalID, &row.Version, &row.Active, ) if err != nil { return nil, fmt.Errorf("error reading secure value row: %w", err) } + if !row.Active { + return nil, fmt.Errorf("bug: read an inactive version: row=%+v", row) + } + secureValue, err := row.toKubernetes() if err != nil { return nil, fmt.Errorf("convert to kubernetes object: %w", err) @@ -349,12 +379,86 @@ func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.N return secureValues, nil } -func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, externalID contracts.ExternalID) error { +func (s *secureValueMetadataStorage) SetVersionToActive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + attribute.Int64("version", version), + )) + defer span.End() + + req := secureValueSetVersionToActive{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + Version: version, + } + + q, err := sqltemplate.Execute(sqlSecureValueSetVersionToActive, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlSecureValueSetVersionToActive.Name(), err) + } + + res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) + if err != nil { + return fmt.Errorf("setting secure value version to active: namespace=%+v name=%+v version=%+v %w", namespace, name, version, err) + } + + // validate modified cound + modifiedCount, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("fetching number of modified rows: %w", err) + } + if modifiedCount == 0 { + return fmt.Errorf("expected to modify at least one row but modified 0: modifiedCount=%d", modifiedCount) + } + + return nil +} + +func (s *secureValueMetadataStorage) SetVersionToInactive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + attribute.Int64("version", version), + )) + defer span.End() + + req := secureValueSetVersionToInactive{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + Version: version, + } + + q, err := sqltemplate.Execute(sqlSecureValueSetVersionToInactive, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlSecureValueSetVersionToInactive.Name(), err) + } + + res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) + if err != nil { + return fmt.Errorf("setting secure value version to active: namespace=%+v name=%+v version=%+v %w", namespace, name, version, err) + } + + modifiedCount, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("fetching number of modified rows: %w", err) + } + if modifiedCount > 1 { + return fmt.Errorf("expected to modify at at most one row but modified more: modifiedCount=%d", modifiedCount) + } + + return nil +} + +func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, version int64, externalID contracts.ExternalID) error { start := time.Now() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace.String()), attribute.String("externalID", externalID.String()), + attribute.Int64("version", version), )) defer span.End() @@ -362,6 +466,7 @@ func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespac SQLTemplate: sqltemplate.New(s.dialect), Namespace: namespace.String(), Name: name, + Version: version, ExternalID: externalID.String(), } @@ -387,134 +492,3 @@ func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespac return nil } - -func (s *secureValueMetadataStorage) SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetStatus", trace.WithAttributes( - attribute.String("name", name), - attribute.String("namespace", namespace.String()), - attribute.String("status.phase", string(status.Phase)), - attribute.String("status.message", status.Message), - attribute.String("status.externalID", status.ExternalID), - )) - defer span.End() - - req := updateStatusSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace.String(), - Name: name, - Phase: string(status.Phase), - Message: status.Message, - } - - q, err := sqltemplate.Execute(sqlSecureValueUpdateStatus, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateStatus.Name(), err) - } - - res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) - if err != nil { - return fmt.Errorf("setting secure value status to Succeeded id: namespace=%+v name=%+v %w", namespace, name, err) - } - - // validate modified cound - modifiedCount, err := res.RowsAffected() - if err != nil { - return fmt.Errorf("getting updated rows update status secure value: %w", err) - } - if modifiedCount > 1 { - return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount) - } - s.metrics.SecureValueSetStatusDuration.Observe(time.Since(start).Seconds()) - - return nil -} - -func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*contracts.DecryptSecureValue, error) { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.ReadForDecrypt", trace.WithAttributes( - attribute.String("name", name), - attribute.String("namespace", namespace.String()), - )) - defer span.End() - - req := readSecureValueForDecrypt{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace.String(), - Name: name, - } - - query, err := sqltemplate.Execute(sqlSecureValueReadForDecrypt, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueReadForDecrypt.Name(), err) - } - - res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return nil, fmt.Errorf("reading row: %w", err) - } - defer func() { _ = res.Close() }() - - var row secureValueForDecrypt - if !res.Next() { - return nil, contracts.ErrSecureValueNotFound - } - if err := res.Scan( - &row.Keeper, &row.Decrypters, - &row.Ref, &row.ExternalID); err != nil { - return nil, fmt.Errorf("failed to scan secure value row: %w", err) - } - - if err := res.Err(); err != nil { - return nil, fmt.Errorf("read rows error: %w", err) - } - - secureValue, err := row.toDecrypt() - if err != nil { - return nil, fmt.Errorf("convert to kubernetes object: %w", err) - } - - s.metrics.SecureValueGetForDecryptDuration.Observe(time.Since(start).Seconds()) - - return secureValue, nil -} - -func (s *secureValueMetadataStorage) read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (secureValueDB, error) { - req := readSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace.String(), - Name: name, - IsForUpdate: opts.ForUpdate, - } - - query, err := sqltemplate.Execute(sqlSecureValueRead, req) - if err != nil { - return secureValueDB{}, fmt.Errorf("execute template %q: %w", sqlSecureValueRead.Name(), err) - } - - res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return secureValueDB{}, fmt.Errorf("reading row: %w", err) - } - defer func() { _ = res.Close() }() - - var secureValue secureValueDB - if !res.Next() { - return secureValueDB{}, contracts.ErrSecureValueNotFound - } - - if err := res.Scan( - &secureValue.GUID, &secureValue.Name, &secureValue.Namespace, - &secureValue.Annotations, &secureValue.Labels, - &secureValue.Created, &secureValue.CreatedBy, - &secureValue.Updated, &secureValue.UpdatedBy, - &secureValue.Phase, &secureValue.Message, - &secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID); err != nil { - return secureValueDB{}, fmt.Errorf("failed to scan secure value row: %w", err) - } - - if err := res.Err(); err != nil { - return secureValueDB{}, fmt.Errorf("read rows error: %w", err) - } - return secureValue, nil -} diff --git a/pkg/storage/secret/metadata/secure_value_store_test.go b/pkg/storage/secret/metadata/secure_value_store_test.go index b9ec0023430..1de2dbf73f1 100644 --- a/pkg/storage/secret/metadata/secure_value_store_test.go +++ b/pkg/storage/secret/metadata/secure_value_store_test.go @@ -1,4 +1,4 @@ -package metadata +package metadata_test import ( "context" @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/storage/secret/database" + "github.com/grafana/grafana/pkg/storage/secret/metadata" "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" @@ -43,11 +44,11 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) // Initialize the secure value storage - secureValueStorage, err := ProvideSecureValueMetadataStorage(db, tracer, features, nil) + secureValueStorage, err := metadata.ProvideSecureValueMetadataStorage(db, tracer, features, nil) require.NoError(t, err) // Initialize the keeper storage - keeperStorage, err := ProvideKeeperMetadataStorage(db, tracer, features, nil) + keeperStorage, err := metadata.ProvideKeeperMetadataStorage(db, tracer, features, nil) require.NoError(t, err) t.Run("create and read a secure value", func(t *testing.T) { @@ -73,7 +74,8 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "default", createdSecureValue.Namespace) require.Equal(t, "test description", createdSecureValue.Spec.Description) require.Equal(t, keeperName, *createdSecureValue.Spec.Keeper) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, createdSecureValue.Status.Phase) + + require.NoError(t, secureValueStorage.SetVersionToActive(ctx, xkube.Namespace(createdSecureValue.Namespace), createdSecureValue.Name, createdSecureValue.Status.Version)) // Read the secure value back readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test", contracts.ReadOpts{}) @@ -83,7 +85,6 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "default", readSecureValue.Namespace) require.Equal(t, "test description", readSecureValue.Spec.Description) require.Equal(t, keeperName, *readSecureValue.Spec.Keeper) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, readSecureValue.Status.Phase) // List secure values and verify our value is in the list secureValues, err := secureValueStorage.List(ctx, xkube.Namespace("default")) @@ -98,7 +99,6 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "default", sv.Namespace) require.Equal(t, "test description", sv.Spec.Description) require.Equal(t, keeperName, *sv.Spec.Keeper) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase) break } } @@ -125,6 +125,8 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.NoError(t, err) require.NotNil(t, createdSecureValue) + require.NoError(t, secureValueStorage.SetVersionToActive(ctx, xkube.Namespace(createdSecureValue.Namespace), createdSecureValue.Name, createdSecureValue.Status.Version)) + // Read the secure value to verify it exists readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test-2", contracts.ReadOpts{}) require.NoError(t, err) @@ -132,7 +134,7 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "sv-test-2", readSecureValue.Name) // Delete the secure value - err = secureValueStorage.Delete(ctx, xkube.Namespace("default"), "sv-test-2") + err = secureValueStorage.SetVersionToInactive(ctx, xkube.Namespace("default"), "sv-test-2", readSecureValue.Status.Version) require.NoError(t, err) // Try to read the deleted secure value - should return error diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql index 8dea7db9fa8..aadea1d47a9 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql @@ -8,8 +8,8 @@ INSERT INTO `secret_secure_value` ( `created_by`, `updated`, `updated_by`, - `status_phase`, - `status_message`, + `active`, + `version`, `description`, `keeper`, `decrypters`, @@ -25,8 +25,8 @@ INSERT INTO `secret_secure_value` ( 'user:ryan', 5678, 'user:cameron', - 'creating', - 'message_test', + FALSE, + 1, 'description', 'keeper_test', 'decrypters_test', diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql index fcaa4271d49..ff0875fc55c 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql @@ -8,7 +8,8 @@ INSERT INTO `secret_secure_value` ( `created_by`, `updated`, `updated_by`, - `status_phase`, + `active`, + `version`, `description`, `external_id` ) VALUES ( @@ -21,7 +22,8 @@ INSERT INTO `secret_secure_value` ( 'user:ryan', 5678, 'user:cameron', - 'creating', + FALSE, + 1, 'description', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_delete-delete.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_delete-delete.sql deleted file mode 100755 index e8597c9fc8b..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_delete-delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql new file mode 100755 index 00000000000..8118fc81489 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql @@ -0,0 +1,10 @@ +SELECT + `version` +FROM + `secret_secure_value` +WHERE + `namespace` = 'ns' AND + `name` = 'name' +ORDER BY `version` DESC +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql index c47a02c16b2..d73828bfae8 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql @@ -8,15 +8,17 @@ SELECT `created_by`, `updated`, `updated_by`, - `status_phase`, - `status_message`, `description`, `keeper`, `decrypters`, `ref`, - `external_id` + `external_id`, + `version`, + `active` FROM `secret_secure_value` -WHERE `namespace` = 'ns' +WHERE + `namespace` = 'ns' AND + `active` = true ORDER BY `updated` DESC ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql index 91ac79af6ae..91850979fdb 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql @@ -3,7 +3,9 @@ SELECT `keeper` FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` IN ('a', 'b') +WHERE + `namespace` = 'ns' AND + `name` IN ('a', 'b') AND + `active` = true FOR UPDATE ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-all-fields-present.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-all-fields-present.sql deleted file mode 100755 index a1cff11dd5d..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-all-fields-present.sql +++ /dev/null @@ -1,21 +0,0 @@ -INSERT INTO `secret_secure_value_outbox` ( - `request_id`, - `message_type`, - `name`, - `namespace`, - `encrypted_secret`, - `keeper_name`, - `external_id`, - `receive_count`, - `created` -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - '', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-encrypted-secret.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-encrypted-secret.sql deleted file mode 100755 index 139d262d49a..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-encrypted-secret.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO `secret_secure_value_outbox` ( - `request_id`, - `message_type`, - `name`, - `namespace`, - `keeper_name`, - `external_id`, - `receive_count`, - `created` -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'keeper', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-external-id.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-external-id.sql deleted file mode 100755 index 62af967a284..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-external-id.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO `secret_secure_value_outbox` ( - `request_id`, - `message_type`, - `name`, - `namespace`, - `encrypted_secret`, - `keeper_name`, - `receive_count`, - `created` -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-keeper-name.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-keeper-name.sql deleted file mode 100755 index 924b7d6277f..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-keeper-name.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO `secret_secure_value_outbox` ( - `request_id`, - `message_type`, - `name`, - `namespace`, - `encrypted_secret`, - `external_id`, - `receive_count`, - `created` -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_delete-basic.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_delete-basic.sql deleted file mode 100755 index 2a6fc52ea18..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_delete-basic.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - `secret_secure_value_outbox` -WHERE - `id` = 1 -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_fetch_message_ids-basic.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_fetch_message_ids-basic.sql deleted file mode 100755 index 47b006df9e3..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_fetch_message_ids-basic.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT - `id` -FROM `secret_secure_value_outbox` -ORDER BY id ASC -LIMIT 10 -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_receiveN-basic.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_receiveN-basic.sql deleted file mode 100755 index 710f96bf187..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_receiveN-basic.sql +++ /dev/null @@ -1,19 +0,0 @@ -SELECT - `request_id`, - `id`, - `message_type`, - `name`, - `namespace`, - `encrypted_secret`, - `keeper_name`, - `external_id`, - `receive_count`, - `created` -FROM - `secret_secure_value_outbox` -WHERE - `id` IN (1, 2, 3) -ORDER BY - `id` ASC -FOR UPDATE SKIP LOCKED -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_update_receive_count-update-receive-count.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_update_receive_count-update-receive-count.sql deleted file mode 100755 index e9df2c03014..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_update_receive_count-update-receive-count.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE - `secret_secure_value_outbox` -SET - `receive_count` = `receive_count` + 1 -WHERE - `id` IN (1, 2, 3) -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql index 8f433497d22..4d8525dd0a0 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql @@ -8,16 +8,18 @@ SELECT `created_by`, `updated`, `updated_by`, - `status_phase`, - `status_message`, `description`, `keeper`, `decrypters`, `ref`, - `external_id` + `external_id`, + `active`, + `version` FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` = 'name' +WHERE + `namespace` = 'ns' AND + `name` = 'name' AND + `active` = true FOR UPDATE ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql index b45713eb594..c42fd0037c5 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql @@ -8,15 +8,17 @@ SELECT `created_by`, `updated`, `updated_by`, - `status_phase`, - `status_message`, `description`, `keeper`, `decrypters`, `ref`, - `external_id` + `external_id`, + `active`, + `version` FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` = 'name' +WHERE + `namespace` = 'ns' AND + `name` = 'name' AND + `active` = true ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql index 2694f0376d8..9d9095cf1e7 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql @@ -2,9 +2,12 @@ SELECT `keeper`, `decrypters`, `ref`, - `external_id` + `external_id`, + `active` FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` = 'name' +WHERE + `namespace` = 'ns' AND + `name` = 'name' AND + `active` = true ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_set_version_to_active-set secure value version to active.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_set_version_to_active-set secure value version to active.sql new file mode 100755 index 00000000000..ce98578b8b5 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_set_version_to_active-set secure value version to active.sql @@ -0,0 +1,8 @@ +UPDATE + `secret_secure_value` +SET + `active` = (`version` = 1) +WHERE + `namespace` = 'ns' AND + `name` = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-not-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-not-null.sql deleted file mode 100755 index 6b39e09d23f..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-not-null.sql +++ /dev/null @@ -1,22 +0,0 @@ -UPDATE - `secret_secure_value` -SET - `guid` = 'abc', - `name` = 'name', - `namespace` = 'ns', - `annotations` = '{"x":"XXXX"}', - `labels` = '{"a":"AAA", "b", "BBBB"}', - `created` = 1234, - `created_by` = 'user:ryan', - `updated` = 5678, - `updated_by` = 'user:cameron', - `status_phase` = 'creating', - `status_message` = 'message_test', - `description` = 'description', - `keeper` = 'keeper_test', - `decrypters` = 'decrypters_test', - `ref` = 'ref_test', - `external_id` = 'extId' -WHERE `namespace` = 'ns' AND - `name` = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-null.sql deleted file mode 100755 index 378a2934ec3..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-null.sql +++ /dev/null @@ -1,18 +0,0 @@ -UPDATE - `secret_secure_value` -SET - `guid` = 'abc', - `name` = 'name', - `namespace` = 'ns', - `annotations` = '{"x":"XXXX"}', - `labels` = '{"a":"AAA", "b", "BBBB"}', - `created` = 1234, - `created_by` = 'user:ryan', - `updated` = 5678, - `updated_by` = 'user:cameron', - `status_phase` = 'creating', - `description` = 'description', - `external_id` = 'extId' -WHERE `namespace` = 'ns' AND - `name` = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql index d6d7db45247..eb862fcb072 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql @@ -2,6 +2,8 @@ UPDATE `secret_secure_value` SET `external_id` = 'extId' -WHERE `namespace` = 'ns' AND - `name` = 'name' +WHERE + `namespace` = 'ns' AND + `name` = 'name' AND + `version` = 0 ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateStatus-updateStatus.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateStatus-updateStatus.sql deleted file mode 100755 index 25815855c68..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateStatus-updateStatus.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE - `secret_secure_value` -SET - `status_phase` = 'Succeeded', - `status_message` = 'message-1' -WHERE `namespace` = 'ns' AND - `name` = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql index d9f191a9c63..cf43e0c130c 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql @@ -8,8 +8,8 @@ INSERT INTO "secret_secure_value" ( "created_by", "updated", "updated_by", - "status_phase", - "status_message", + "active", + "version", "description", "keeper", "decrypters", @@ -25,8 +25,8 @@ INSERT INTO "secret_secure_value" ( 'user:ryan', 5678, 'user:cameron', - 'creating', - 'message_test', + FALSE, + 1, 'description', 'keeper_test', 'decrypters_test', diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql index 69dd74be229..564c5728f14 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql @@ -8,7 +8,8 @@ INSERT INTO "secret_secure_value" ( "created_by", "updated", "updated_by", - "status_phase", + "active", + "version", "description", "external_id" ) VALUES ( @@ -21,7 +22,8 @@ INSERT INTO "secret_secure_value" ( 'user:ryan', 5678, 'user:cameron', - 'creating', + FALSE, + 1, 'description', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_delete-delete.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_delete-delete.sql deleted file mode 100755 index f2fbdc59f70..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_delete-delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql new file mode 100755 index 00000000000..1549114582d --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql @@ -0,0 +1,10 @@ +SELECT + "version" +FROM + "secret_secure_value" +WHERE + "namespace" = 'ns' AND + "name" = 'name' +ORDER BY "version" DESC +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql index 681ee5708cd..40ee42ebcf7 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "version", + "active" FROM "secret_secure_value" -WHERE "namespace" = 'ns' +WHERE + "namespace" = 'ns' AND + "active" = true ORDER BY "updated" DESC ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql index 93a449fa17a..d476e297129 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql @@ -3,7 +3,9 @@ SELECT "keeper" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" IN ('a', 'b') +WHERE + "namespace" = 'ns' AND + "name" IN ('a', 'b') AND + "active" = true FOR UPDATE ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-all-fields-present.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-all-fields-present.sql deleted file mode 100755 index 3c2b2588c08..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-all-fields-present.sql +++ /dev/null @@ -1,21 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - '', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-encrypted-secret.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-encrypted-secret.sql deleted file mode 100755 index bcecf30a749..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-encrypted-secret.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "keeper_name", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'keeper', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-external-id.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-external-id.sql deleted file mode 100755 index 81eb522e3a8..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-external-id.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-keeper-name.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-keeper-name.sql deleted file mode 100755 index 3c0e3abf5e8..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-keeper-name.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_delete-basic.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_delete-basic.sql deleted file mode 100755 index f0c8984c8a0..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_delete-basic.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - "secret_secure_value_outbox" -WHERE - "id" = 1 -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_fetch_message_ids-basic.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_fetch_message_ids-basic.sql deleted file mode 100755 index 4c590706948..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_fetch_message_ids-basic.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT - "id" -FROM "secret_secure_value_outbox" -ORDER BY id ASC -LIMIT 10 -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_receiveN-basic.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_receiveN-basic.sql deleted file mode 100755 index f4602caba07..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_receiveN-basic.sql +++ /dev/null @@ -1,19 +0,0 @@ -SELECT - "request_id", - "id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "external_id", - "receive_count", - "created" -FROM - "secret_secure_value_outbox" -WHERE - "id" IN (1, 2, 3) -ORDER BY - "id" ASC -FOR UPDATE SKIP LOCKED -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_update_receive_count-update-receive-count.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_update_receive_count-update-receive-count.sql deleted file mode 100755 index d9404fd8212..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_update_receive_count-update-receive-count.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE - "secret_secure_value_outbox" -SET - "receive_count" = "receive_count" + 1 -WHERE - "id" IN (1, 2, 3) -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql index 76ca4a8c155..cce38e6db7a 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql @@ -8,16 +8,18 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active", + "version" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true FOR UPDATE ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql index d7fbc77a21b..2d16c211a85 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active", + "version" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql index 999e79c2fa1..2a764e2677a 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql @@ -2,9 +2,12 @@ SELECT "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_set_version_to_active-set secure value version to active.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_set_version_to_active-set secure value version to active.sql new file mode 100755 index 00000000000..d659f944431 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_set_version_to_active-set secure value version to active.sql @@ -0,0 +1,8 @@ +UPDATE + "secret_secure_value" +SET + "active" = ("version" = 1) +WHERE + "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-not-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-not-null.sql deleted file mode 100755 index 7371f13f6e5..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-not-null.sql +++ /dev/null @@ -1,22 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "guid" = 'abc', - "name" = 'name', - "namespace" = 'ns', - "annotations" = '{"x":"XXXX"}', - "labels" = '{"a":"AAA", "b", "BBBB"}', - "created" = 1234, - "created_by" = 'user:ryan', - "updated" = 5678, - "updated_by" = 'user:cameron', - "status_phase" = 'creating', - "status_message" = 'message_test', - "description" = 'description', - "keeper" = 'keeper_test', - "decrypters" = 'decrypters_test', - "ref" = 'ref_test', - "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-null.sql deleted file mode 100755 index 0524e525096..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-null.sql +++ /dev/null @@ -1,18 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "guid" = 'abc', - "name" = 'name', - "namespace" = 'ns', - "annotations" = '{"x":"XXXX"}', - "labels" = '{"a":"AAA", "b", "BBBB"}', - "created" = 1234, - "created_by" = 'user:ryan', - "updated" = 5678, - "updated_by" = 'user:cameron', - "status_phase" = 'creating', - "description" = 'description', - "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql index 3d9f474776e..29bedaa7afe 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql @@ -2,6 +2,8 @@ UPDATE "secret_secure_value" SET "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "version" = 0 ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateStatus-updateStatus.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateStatus-updateStatus.sql deleted file mode 100755 index c5cf29a5e7a..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateStatus-updateStatus.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "status_phase" = 'Succeeded', - "status_message" = 'message-1' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql index d9f191a9c63..cf43e0c130c 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql @@ -8,8 +8,8 @@ INSERT INTO "secret_secure_value" ( "created_by", "updated", "updated_by", - "status_phase", - "status_message", + "active", + "version", "description", "keeper", "decrypters", @@ -25,8 +25,8 @@ INSERT INTO "secret_secure_value" ( 'user:ryan', 5678, 'user:cameron', - 'creating', - 'message_test', + FALSE, + 1, 'description', 'keeper_test', 'decrypters_test', diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql index 69dd74be229..564c5728f14 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql @@ -8,7 +8,8 @@ INSERT INTO "secret_secure_value" ( "created_by", "updated", "updated_by", - "status_phase", + "active", + "version", "description", "external_id" ) VALUES ( @@ -21,7 +22,8 @@ INSERT INTO "secret_secure_value" ( 'user:ryan', 5678, 'user:cameron', - 'creating', + FALSE, + 1, 'description', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_delete-delete.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_delete-delete.sql deleted file mode 100755 index f2fbdc59f70..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_delete-delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql new file mode 100755 index 00000000000..1549114582d --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql @@ -0,0 +1,10 @@ +SELECT + "version" +FROM + "secret_secure_value" +WHERE + "namespace" = 'ns' AND + "name" = 'name' +ORDER BY "version" DESC +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql index 681ee5708cd..40ee42ebcf7 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "version", + "active" FROM "secret_secure_value" -WHERE "namespace" = 'ns' +WHERE + "namespace" = 'ns' AND + "active" = true ORDER BY "updated" DESC ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql index 48ae01800ae..22d2006de82 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql @@ -3,6 +3,8 @@ SELECT "keeper" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" IN ('a', 'b') +WHERE + "namespace" = 'ns' AND + "name" IN ('a', 'b') AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-all-fields-present.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-all-fields-present.sql deleted file mode 100755 index 3c2b2588c08..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-all-fields-present.sql +++ /dev/null @@ -1,21 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - '', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-encrypted-secret.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-encrypted-secret.sql deleted file mode 100755 index bcecf30a749..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-encrypted-secret.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "keeper_name", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'keeper', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-external-id.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-external-id.sql deleted file mode 100755 index 81eb522e3a8..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-external-id.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-keeper-name.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-keeper-name.sql deleted file mode 100755 index 3c0e3abf5e8..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-keeper-name.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_delete-basic.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_delete-basic.sql deleted file mode 100755 index f0c8984c8a0..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_delete-basic.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - "secret_secure_value_outbox" -WHERE - "id" = 1 -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_fetch_message_ids-basic.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_fetch_message_ids-basic.sql deleted file mode 100755 index 4c590706948..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_fetch_message_ids-basic.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT - "id" -FROM "secret_secure_value_outbox" -ORDER BY id ASC -LIMIT 10 -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_receiveN-basic.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_receiveN-basic.sql deleted file mode 100755 index 62b16f507ba..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_receiveN-basic.sql +++ /dev/null @@ -1,18 +0,0 @@ -SELECT - "request_id", - "id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "external_id", - "receive_count", - "created" -FROM - "secret_secure_value_outbox" -WHERE - "id" IN (1, 2, 3) -ORDER BY - "id" ASC -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_update_receive_count-update-receive-count.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_update_receive_count-update-receive-count.sql deleted file mode 100755 index d9404fd8212..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_update_receive_count-update-receive-count.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE - "secret_secure_value_outbox" -SET - "receive_count" = "receive_count" + 1 -WHERE - "id" IN (1, 2, 3) -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql index d7fbc77a21b..2d16c211a85 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active", + "version" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql index d7fbc77a21b..2d16c211a85 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active", + "version" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read_for_decrypt-read-for-decrypt.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read_for_decrypt-read-for-decrypt.sql index 999e79c2fa1..2a764e2677a 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read_for_decrypt-read-for-decrypt.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read_for_decrypt-read-for-decrypt.sql @@ -2,9 +2,12 @@ SELECT "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_set_version_to_active-set secure value version to active.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_set_version_to_active-set secure value version to active.sql new file mode 100755 index 00000000000..d659f944431 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_set_version_to_active-set secure value version to active.sql @@ -0,0 +1,8 @@ +UPDATE + "secret_secure_value" +SET + "active" = ("version" = 1) +WHERE + "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-not-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-not-null.sql deleted file mode 100755 index 7371f13f6e5..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-not-null.sql +++ /dev/null @@ -1,22 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "guid" = 'abc', - "name" = 'name', - "namespace" = 'ns', - "annotations" = '{"x":"XXXX"}', - "labels" = '{"a":"AAA", "b", "BBBB"}', - "created" = 1234, - "created_by" = 'user:ryan', - "updated" = 5678, - "updated_by" = 'user:cameron', - "status_phase" = 'creating', - "status_message" = 'message_test', - "description" = 'description', - "keeper" = 'keeper_test', - "decrypters" = 'decrypters_test', - "ref" = 'ref_test', - "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-null.sql deleted file mode 100755 index 0524e525096..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-null.sql +++ /dev/null @@ -1,18 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "guid" = 'abc', - "name" = 'name', - "namespace" = 'ns', - "annotations" = '{"x":"XXXX"}', - "labels" = '{"a":"AAA", "b", "BBBB"}', - "created" = 1234, - "created_by" = 'user:ryan', - "updated" = 5678, - "updated_by" = 'user:cameron', - "status_phase" = 'creating', - "description" = 'description', - "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateExternalId-updateExternalId.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateExternalId-updateExternalId.sql index 3d9f474776e..29bedaa7afe 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateExternalId-updateExternalId.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateExternalId-updateExternalId.sql @@ -2,6 +2,8 @@ UPDATE "secret_secure_value" SET "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "version" = 0 ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateStatus-updateStatus.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateStatus-updateStatus.sql deleted file mode 100755 index c5cf29a5e7a..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateStatus-updateStatus.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "status_phase" = 'Succeeded', - "status_message" = 'message-1' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/migrator/migrator.go b/pkg/storage/secret/migrator/migrator.go index f453d6dd3af..6ee7209bc8a 100644 --- a/pkg/storage/secret/migrator/migrator.go +++ b/pkg/storage/secret/migrator/migrator.go @@ -12,11 +12,10 @@ import ( ) const ( - TableNameKeeper = "secret_keeper" - TableNameSecureValue = "secret_secure_value" - TableNameSecureValueOutbox = "secret_secure_value_outbox" - TableNameDataKey = "secret_data_key" - TableNameEncryptedValue = "secret_encrypted_value" + TableNameKeeper = "secret_keeper" + TableNameSecureValue = "secret_secure_value" + TableNameDataKey = "secret_data_key" + TableNameEncryptedValue = "secret_encrypted_value" ) type SecretDB struct { @@ -61,18 +60,19 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { {Name: "updated_by", Type: migrator.DB_Text, Nullable: false}, // Kubernetes Status - {Name: "status_phase", Type: migrator.DB_Text, Nullable: false}, - {Name: "status_message", Type: migrator.DB_Text, Nullable: true}, + {Name: "external_id", Type: migrator.DB_Text, Nullable: false}, + {Name: "active", Type: migrator.DB_Bool, Nullable: false}, + {Name: "version", Type: migrator.DB_BigInt, Nullable: false}, // Spec {Name: "description", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Chosen arbitrarily, but should be enough. {Name: "keeper", Type: migrator.DB_NVarchar, Length: 253, Nullable: true}, // Keeper name, if not set, use default keeper. {Name: "decrypters", Type: migrator.DB_Text, Nullable: true}, {Name: "ref", Type: migrator.DB_NVarchar, Length: 1024, Nullable: true}, // Reference to third-party storage secret path.Chosen arbitrarily, but should be enough. - {Name: "external_id", Type: migrator.DB_Text, Nullable: false}, }, Indices: []*migrator.Index{ - {Cols: []string{"namespace", "name"}, Type: migrator.UniqueIndex}, + {Cols: []string{"namespace", "name", "version", "active"}, Type: migrator.UniqueIndex}, + {Cols: []string{"namespace", "name", "version"}, Type: migrator.UniqueIndex}, }, }) @@ -130,27 +130,6 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { Indices: []*migrator.Index{}, // TODO: add indexes based on the queries we make. }) - tables = append(tables, migrator.Table{ - Name: TableNameSecureValueOutbox, - Columns: []*migrator.Column{ - {Name: "request_id", Type: migrator.DB_NVarchar, Length: 1024, Nullable: false}, // Safer upper limit because we hex-encode traceparent+tracestate to form the request_id. - {Name: "id", Type: migrator.DB_BigInt, Length: 36, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "message_type", Type: migrator.DB_NVarchar, Length: 16, Nullable: false}, - {Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s. - {Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s. - {Name: "encrypted_secret", Type: migrator.DB_Blob, Nullable: true}, - {Name: "keeper_name", Type: migrator.DB_NVarchar, Length: 253, Nullable: true}, // Keeper name, if not set, use default keeper. - {Name: "external_id", Type: migrator.DB_NVarchar, Length: 36, Nullable: true}, // Fixed size of a UUID. - {Name: "receive_count", Type: migrator.DB_SmallInt, Nullable: false}, - {Name: "created", Type: migrator.DB_BigInt, Nullable: false}, - }, - Indices: []*migrator.Index{ - // There's only one operation per secret in the queue at all times, - // meaning the namespace + name combination should be unique - {Cols: []string{"namespace", "name"}, Type: migrator.UniqueIndex}, - }, - }) - // Initialize all tables for t := range tables { mg.AddMigration("drop table "+tables[t].Name, migrator.NewDropTableMigration(tables[t].Name)) diff --git a/pkg/storage/unified/apistore/watcher_test.go b/pkg/storage/unified/apistore/watcher_test.go index c68415bca97..91a251b04d0 100644 --- a/pkg/storage/unified/apistore/watcher_test.go +++ b/pkg/storage/unified/apistore/watcher_test.go @@ -40,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + "github.com/grafana/grafana/pkg/tests" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -134,9 +135,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte _, err = server.IsHealthy(ctx, &resourcepb.HealthCheckRequest{}) require.NoError(t, err) case StorageTypeUnified: - if testing.Short() { - t.Skip("skipping integration test") - } + tests.SkipIntegrationTestInShortMode(t) dbstore := infraDB.InitTestDB(t) cfg := setting.NewCfg() @@ -190,7 +189,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte return ctx, store, destroyFunc, nil } -func TestWatch(t *testing.T) { +func TestIntegrationWatch(t *testing.T) { for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { t.Run(string(s), func(t *testing.T) { ctx, store, destroyFunc, err := testSetup(t, withStorageType(s)) diff --git a/pkg/storage/unified/resource/kv.go b/pkg/storage/unified/resource/kv.go index e4af06a9aa1..21343984c8c 100644 --- a/pkg/storage/unified/resource/kv.go +++ b/pkg/storage/unified/resource/kv.go @@ -68,6 +68,10 @@ func NewBadgerKV(db *badger.DB) *badgerKV { } func (k *badgerKV) Get(ctx context.Context, section string, key string) (KVObject, error) { + if k.db.IsClosed() { + return KVObject{}, fmt.Errorf("database is closed") + } + txn := k.db.NewTransaction(false) defer txn.Discard() @@ -101,6 +105,10 @@ func (k *badgerKV) Get(ctx context.Context, section string, key string) (KVObjec } func (k *badgerKV) Save(ctx context.Context, section string, key string, value io.Reader) error { + if k.db.IsClosed() { + return fmt.Errorf("database is closed") + } + if section == "" { return fmt.Errorf("section is required") } @@ -123,6 +131,10 @@ func (k *badgerKV) Save(ctx context.Context, section string, key string, value i } func (k *badgerKV) Delete(ctx context.Context, section string, key string) error { + if k.db.IsClosed() { + return fmt.Errorf("database is closed") + } + if section == "" { return fmt.Errorf("section is required") } @@ -149,6 +161,11 @@ func (k *badgerKV) Delete(ctx context.Context, section string, key string) error } func (k *badgerKV) Keys(ctx context.Context, section string, opt ListOptions) iter.Seq2[string, error] { + if k.db.IsClosed() { + return func(yield func(string, error) bool) { + yield("", fmt.Errorf("database is closed")) + } + } if section == "" { return func(yield func(string, error) bool) { yield("", fmt.Errorf("section is required")) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 2cc2b6f8b46..0a88d9a035a 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -14,6 +14,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" + "golang.org/x/sync/singleflight" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -80,28 +81,16 @@ type ResourceIndex interface { // SearchBackend contains the technology specific logic to support search type SearchBackend interface { - // This will return nil if the key does not exist + // GetIndex returns existing index, or nil. GetIndex(ctx context.Context, key NamespacedResource) (ResourceIndex, error) - // Build an index from scratch - BuildIndex(ctx context.Context, - key NamespacedResource, + // BuildIndex builds an index from scratch. + // Depending on the size, the backend may choose different options (eg: memory vs disk). + // The last known resource version can be used to detect that nothing has changed, and existing on-disk index can be reused. + // The builder will write all documents before returning. + BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, nonStandardFields SearchableDocumentFields, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) - // When the size is known, it will be passed along here - // Depending on the size, the backend may choose different options (eg: memory vs disk) - size int64, - - // The last known resource version (can be used to know that nothing has changed) - resourceVersion int64, - - // The non-standard index fields - fields SearchableDocumentFields, - - // The builder will write all documents before returning - builder func(index ResourceIndex) (int64, error), - ) (ResourceIndex, error) - - // Gets the total number of documents across all indexes + // TotalDocs returns the total number of documents across all indexes. TotalDocs() int64 } @@ -120,6 +109,8 @@ type searchSupport struct { initMinSize int initMaxSize int + buildIndex singleflight.Group + // Index queue processors indexQueueProcessorsMutex sync.Mutex indexQueueProcessors map[string]*indexQueueProcessor @@ -608,24 +599,53 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"GetOrCreateIndex") defer span.End() - // TODO??? - // We want to block while building the index and return the same index for the key - // simple mutex not great... we don't want to block while anything in building, just the same key idx, err := s.search.GetIndex(ctx, key) if err != nil { return nil, err } - if idx == nil { - idx, _, err = s.build(ctx, key, 10, 0) // unknown size and RV + if idx != nil { + return idx, nil + } + + idxInt, err, _ := s.buildIndex.Do(key.String(), func() (interface{}, error) { + // Recheck if some other goroutine managed to build an index in the meantime. + // (That is, it finished running this function and stored the index into the cache) + idx, err := s.search.GetIndex(ctx, key) + if err == nil && idx != nil { + return idx, nil + } + + // Get correct value of size + RV for building the index. This is important for our Bleve + // backend to decide whether to build index in-memory or as file-based. + stats, err := s.storage.GetResourceStats(ctx, key.Namespace, 0) + if err != nil { + return nil, fmt.Errorf("failed to get resource stats: %w", err) + } + + size := int64(0) + rv := int64(0) + for _, stat := range stats { + if stat.Namespace == key.Namespace && stat.Group == key.Group && stat.Resource == key.Resource { + size = stat.Count + rv = stat.ResourceVersion + break + } + } + + idx, _, err = s.build(ctx, key, size, rv) if err != nil { return nil, fmt.Errorf("error building search index, %w", err) } if idx == nil { return nil, fmt.Errorf("nil index after build") } + return idx, nil + }) + if err != nil { + return nil, err } - return idx, nil + return idxInt.(ResourceIndex), nil } func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size int64, rv int64) (ResourceIndex, int64, error) { @@ -640,8 +660,6 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size } fields := s.builders.GetFields(nsr) - logger.Debug("Building index", "resource", nsr.Resource, "size", size, "rv", rv) - index, err := s.search.BuildIndex(ctx, nsr, size, rv, fields, func(index ResourceIndex) (int64, error) { rv, err = s.storage.ListIterator(ctx, &resourcepb.ListRequest{ Limit: 1000000000000, // big number diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index b474eac8f9d..25f7f3280c0 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -2,7 +2,9 @@ package resource import ( "context" + "sync" "testing" + "time" "github.com/grafana/authlib/types" "github.com/stretchr/testify/mock" @@ -96,6 +98,7 @@ func (m *mockStorageBackend) ListHistory(ctx context.Context, req *resourcepb.Li // mockSearchBackend implements SearchBackend for testing with tracking capabilities type mockSearchBackend struct { + mu sync.Mutex buildIndexCalls []buildIndexCall buildEmptyIndexCalls []buildEmptyIndexCall } @@ -129,6 +132,9 @@ func (m *mockSearchBackend) BuildIndex(ctx context.Context, key NamespacedResour return nil, err } + m.mu.Lock() + defer m.mu.Unlock() + // Determine if this is an empty index based on size // Empty indexes are characterized by size == 0 if size == 0 { @@ -271,3 +277,58 @@ func TestBuildIndexes_MaxCountThreshold(t *testing.T) { }) } } + +func TestSearchGetOrCreateIndex(t *testing.T) { + // Setup mock implementations + storage := &mockStorageBackend{ + resourceStats: []ResourceStats{ + {NamespacedResource: NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, Count: 50, ResourceVersion: 11111111}, + }, + } + search := &mockSearchBackend{ + buildIndexCalls: []buildIndexCall{}, + buildEmptyIndexCalls: []buildEmptyIndexCall{}, + } + supplier := &TestDocumentBuilderSupplier{ + GroupsResources: map[string]string{ + "group": "resource", + }, + } + + // Create search support with the specified initMaxSize + opts := SearchOptions{ + Backend: search, + Resources: supplier, + WorkerThreads: 1, + InitMinCount: 1, // set min count to default for this test + InitMaxCount: 0, + } + + support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil) + require.NoError(t, err) + require.NotNil(t, support) + + start := make(chan struct{}) + + const concurrency = 100 + wg := sync.WaitGroup{} + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + _, _ = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}) + }() + } + + // Wait a bit for goroutines to start (hopefully) + time.Sleep(10 * time.Millisecond) + // Unblock all goroutines. + close(start) + wg.Wait() + + require.NotEmpty(t, search.buildIndexCalls) + require.Less(t, len(search.buildIndexCalls), concurrency, "Should not have built index more than a few times (ideally once)") + require.Equal(t, int64(50), search.buildIndexCalls[0].size) + require.Equal(t, int64(11111111), search.buildIndexCalls[0].resourceVersion) +} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 35dc9de717b..00567a3774c 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -782,6 +782,11 @@ func (s *server) delete(ctx context.Context, user claims.AuthInfo, req *resource return nil, apierrors.NewBadRequest( fmt.Sprintf("unable to read previous object, %v", err)) } + oldObj, err := utils.MetaAccessor(marker) + if err != nil { + return nil, err + } + obj, err := utils.MetaAccessor(marker) if err != nil { return nil, err @@ -793,6 +798,8 @@ func (s *server) delete(ctx context.Context, user claims.AuthInfo, req *resource obj.SetUpdatedBy(user.GetUID()) obj.SetGeneration(utils.DeletedGeneration) obj.SetAnnotation(utils.AnnoKeyKubectlLastAppliedConfig, "") // clears it + event.ObjectOld = oldObj + event.Object = obj event.Value, err = marker.MarshalJSON() if err != nil { return nil, apierrors.NewBadRequest( diff --git a/pkg/storage/unified/resource/storage_backend.go b/pkg/storage/unified/resource/storage_backend.go index 53a898e50b2..dff33f63b36 100644 --- a/pkg/storage/unified/resource/storage_backend.go +++ b/pkg/storage/unified/resource/storage_backend.go @@ -63,6 +63,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in } rv := k.snowflake.Generate().Int64() + obj := event.Object // Write data. var action DataAction switch event.Type { @@ -87,10 +88,15 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in action = DataActionUpdated case resourcepb.WatchEvent_DELETED: action = DataActionDeleted + obj = event.ObjectOld default: return 0, fmt.Errorf("invalid event type: %d", event.Type) } + if obj == nil { + return 0, fmt.Errorf("object is nil") + } + // Build the search document doc, err := k.builder.BuildDocument(ctx, event.Key, rv, event.Value) if err != nil { @@ -119,7 +125,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in Name: event.Key.Name, ResourceVersion: rv, Action: action, - Folder: event.Object.GetFolder(), + Folder: obj.GetFolder(), }, Value: MetaData{ IndexableDocument: *doc, @@ -137,7 +143,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in Name: event.Key.Name, ResourceVersion: rv, Action: action, - Folder: event.Object.GetFolder(), + Folder: obj.GetFolder(), PreviousRV: event.PreviousRV, }) if err != nil { diff --git a/pkg/storage/unified/resource/storage_backend_test.go b/pkg/storage/unified/resource/storage_backend_test.go index 899f7b2de25..bb1288458d0 100644 --- a/pkg/storage/unified/resource/storage_backend_test.go +++ b/pkg/storage/unified/resource/storage_backend_test.go @@ -72,6 +72,7 @@ func TestKvStorageBackend_WriteEvent_Success(t *testing.T) { }, Value: objectToJSONBytes(t, testObj), Object: metaAccessor, + ObjectOld: metaAccessor, PreviousRV: 100, } @@ -799,6 +800,8 @@ func TestKvStorageBackend_ListTrash_Success(t *testing.T) { // Delete the resource writeEvent.Type = resourcepb.WatchEvent_DELETED writeEvent.PreviousRV = rv1 + writeEvent.Object = metaAccessor + writeEvent.ObjectOld = metaAccessor rv2, err := backend.WriteEvent(ctx, writeEvent) require.NoError(t, err) @@ -989,8 +992,12 @@ func writeObject(t *testing.T, backend *kvStorageBackend, obj *unstructured.Unst }, Value: objectToJSONBytes(t, obj), Object: metaAccessor, + ObjectOld: metaAccessor, PreviousRV: previousRV, } + if eventType == resourcepb.WatchEvent_ADDED { + writeEvent.ObjectOld = nil + } return backend.WriteEvent(context.Background(), writeEvent) } diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 503bd7a8dfb..a2415c528b0 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -3,6 +3,7 @@ package search import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "math" @@ -11,6 +12,7 @@ import ( "slices" "strconv" "strings" + "sync" "time" "github.com/blevesearch/bleve/v2" @@ -31,7 +33,6 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -39,8 +40,6 @@ import ( const ( // tracingPrexfixBleve is the prefix used for tracing spans in the Bleve backend tracingPrexfixBleve = "unified_search.bleve." - // Default index cache cleanup TTL is 1 minute - indexCacheCleanupInterval = time.Minute ) var _ resource.SearchBackend = &bleveBackend{} @@ -57,7 +56,7 @@ type BleveOptions struct { // ?? not totally sure the units BatchSize int - // Index cache TTL for bleve indices + // Index cache TTL for bleve indices. 0 disables expiration for in-memory indexes. IndexCacheTTL time.Duration } @@ -65,9 +64,9 @@ type bleveBackend struct { tracer trace.Tracer log *slog.Logger opts BleveOptions - start time.Time - cache *localcache.CacheService + cacheMx sync.RWMutex + cache map[resource.NamespacedResource]*bleveIndex features featuremgmt.FeatureToggles indexMetrics *resource.BleveIndexMetrics @@ -77,6 +76,12 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, features featuremgm if opts.Root == "" { return nil, fmt.Errorf("bleve backend missing root folder configuration") } + absRoot, err := filepath.Abs(opts.Root) + if err != nil { + return nil, fmt.Errorf("error getting absolute path for bleve root folder %w", err) + } + opts.Root = absRoot + root, err := os.Stat(opts.Root) if err != nil { return nil, fmt.Errorf("error opening bleve root folder %w", err) @@ -85,35 +90,64 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, features featuremgm return nil, fmt.Errorf("bleve root is configured against a file (not folder)") } - bleveBackend := &bleveBackend{ + be := &bleveBackend{ log: slog.Default().With("logger", "bleve-backend"), tracer: tracer, - cache: localcache.New(opts.IndexCacheTTL, indexCacheCleanupInterval), + cache: map[resource.NamespacedResource]*bleveIndex{}, opts: opts, - start: time.Now(), features: features, indexMetrics: indexMetrics, } - go bleveBackend.updateIndexSizeMetric(opts.Root) + go be.updateIndexSizeMetric(opts.Root) - return bleveBackend, nil + return be, nil } -// This will return nil if the key does not exist -func (b *bleveBackend) GetIndex(ctx context.Context, key resource.NamespacedResource) (resource.ResourceIndex, error) { - val, ok := b.cache.Get(key.String()) - if !ok { +// GetIndex will return nil if the key does not exist +func (b *bleveBackend) GetIndex(_ context.Context, key resource.NamespacedResource) (resource.ResourceIndex, error) { + idx := b.getCachedIndex(key) + // Avoid returning typed nils. + if idx == nil { return nil, nil } - - idx, ok := val.(*bleveIndex) - if !ok { - return nil, fmt.Errorf("cache item is not a bleve index: %s", key.String()) - } return idx, nil } +func (b *bleveBackend) getCachedIndex(key resource.NamespacedResource) *bleveIndex { + // Check index with read-lock first. + b.cacheMx.RLock() + val := b.cache[key] + b.cacheMx.RUnlock() + + if val == nil { + return nil + } + + if val.expiration.IsZero() || val.expiration.After(time.Now()) { + // Not expired yet. + return val + } + + // We're dealing with expired index. We need to remove it from the cache and close it. + b.cacheMx.Lock() + val = b.cache[key] + delete(b.cache, key) + b.cacheMx.Unlock() + + if val == nil { + return nil + } + + // Index is no longer in the cache, but we need to close it. + err := val.index.Close() + if err != nil { + b.log.Error("failed to close index", "key", key, "err", err) + } + b.log.Info("index evicted from cache", "key", key) + return nil +} + // updateIndexSizeMetric sets the total size of all file-based indices metric. func (b *bleveBackend) updateIndexSizeMetric(indexPath string) { if b.indexMetrics == nil { @@ -147,28 +181,21 @@ func (b *bleveBackend) updateIndexSizeMetric(indexPath string) { } } -// Build an index from scratch -func (b *bleveBackend) BuildIndex(ctx context.Context, +// BuildIndex builds an index from scratch. +// If built successfully, the new index replaces the old index in the cache (if there was any). +func (b *bleveBackend) BuildIndex( + ctx context.Context, key resource.NamespacedResource, - - // When the size is known, it will be passed along here - // Depending on the size, the backend may choose different options (eg: memory vs disk) size int64, - - // The last known resource version can be used to know that we can skip calling the builder resourceVersion int64, - - // the non-standard searchable fields fields resource.SearchableDocumentFields, - - // The builder will write all documents before returning builder func(index resource.ResourceIndex) (int64, error), ) (resource.ResourceIndex, error) { _, span := b.tracer.Start(ctx, tracingPrexfixBleve+"BuildIndex") defer span.End() - var err error var index bleve.Index + fileIndexName := "" // Name of the file-based index, or empty for in-memory indexes. build := true mapper, err := GetBleveMappings(fields) @@ -176,61 +203,62 @@ func (b *bleveBackend) BuildIndex(ctx context.Context, return nil, err } - if size > b.opts.FileThreshold { - resourceDir := filepath.Join(b.opts.Root, key.Namespace, - fmt.Sprintf("%s.%s", key.Resource, key.Group), - ) - fname := fmt.Sprintf("rv%d", resourceVersion) - if resourceVersion == 0 { - fname = b.start.Format("tmp-20060102-150405") - } - dir := filepath.Join(resourceDir, fname) - if !isValidPath(dir, b.opts.Root) { - b.log.Error("Directory is not valid", "directory", dir) - } - if resourceVersion > 0 { - info, _ := os.Stat(dir) - if info != nil && info.IsDir() { - index, err = bleve.Open(dir) // NOTE, will use the same mappings!!! - if err == nil { - found, err := index.DocCount() - if err != nil || int64(found) != size { - b.log.Info("this size changed since the last time the index opened") - _ = index.Close() + cachedIndex := b.getCachedIndex(key) - // Pick a new file name - fname = b.start.Format("tmp-20060102-150405-changed") - dir = filepath.Join(resourceDir, fname) - index = nil - } else { - build = false // no need to build the index - } + logWithDetails := b.log.With("namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "size", size, "rv", resourceVersion) + + resourceDir := filepath.Join(b.opts.Root, cleanFileSegment(key.Namespace), cleanFileSegment(fmt.Sprintf("%s.%s", key.Resource, key.Group))) + + if size > b.opts.FileThreshold { + // We only check for the existing file-based index if we don't already have an open index for this key. + // This happens on startup, or when memory-based index has expired. (We don't expire file-based indexes) + // If we do have an unexpired cached index already, we always build a new index from scratch. + if cachedIndex == nil && resourceVersion > 0 { + index, fileIndexName = b.findPreviousFileBasedIndex(resourceDir, resourceVersion, size) + } + + if index != nil { + build = false + logWithDetails.Debug("Existing index found on filesystem", "directory", filepath.Join(resourceDir, fileIndexName)) + } else { + // Building index from scratch. Index name has a time component in it to be unique, but if + // we happen to create non-unique name, we bump the time and try again. + + indexDir := "" + now := time.Now() + for index == nil { + fileIndexName = formatIndexName(time.Now(), resourceVersion) + indexDir = filepath.Join(resourceDir, fileIndexName) + if !isPathWithinRoot(indexDir, b.opts.Root) { + return nil, fmt.Errorf("invalid path %s", indexDir) + } + + index, err = bleve.New(indexDir, mapper) + if errors.Is(err, bleve.ErrorIndexPathExists) { + now = now.Add(time.Second) // Bump time for next try + index = nil // Bleve actually returns non-nil value with ErrorIndexPathExists + continue + } + if err != nil { + return nil, fmt.Errorf("error creating new bleve index: %s %w", indexDir, err) } } + + logWithDetails.Info("Building index using filesystem", "directory", indexDir) } - if index == nil { - index, err = bleve.New(dir, mapper) - if err != nil { - err = fmt.Errorf("error creating new bleve index: %s %w", dir, err) - } - } - - // Start a background task to cleanup the old index directories - if index != nil && err == nil { - go b.cleanOldIndexes(resourceDir, fname) - } if b.indexMetrics != nil { b.indexMetrics.IndexTenants.WithLabelValues("file").Inc() } } else { index, err = bleve.NewMemOnly(mapper) + if err != nil { + return nil, fmt.Errorf("error creating new in-memory bleve index: %w", err) + } if b.indexMetrics != nil { b.indexMetrics.IndexTenants.WithLabelValues("memory").Inc() } - } - if err != nil { - return nil, err + logWithDetails.Info("Building index using memory") } // Batch all the changes @@ -249,28 +277,72 @@ func (b *bleveBackend) BuildIndex(ctx context.Context, } if build { + start := time.Now() _, err = builder(idx) if err != nil { return nil, err } + elapsed := time.Since(start) + logWithDetails.Info("Finished building index", "elapsed", elapsed) } - b.cache.SetDefault(key.String(), idx) + // Set expiration after building the index. Only expire in-memory indexes. + if fileIndexName == "" && b.opts.IndexCacheTTL > 0 { + idx.expiration = time.Now().Add(b.opts.IndexCacheTTL) + } + + // Store the index in the cache. + if idx.expiration.IsZero() { + logWithDetails.Info("Storing index in cache, with no expiration", "key", key) + } else { + logWithDetails.Info("Storing index in cache", "key", key, "expiration", idx.expiration) + } + + b.cacheMx.Lock() + prev := b.cache[key] + b.cache[key] = idx + b.cacheMx.Unlock() + + // If there was a previous index in the cache, close it. + if prev != nil { + err := prev.index.Close() + if err != nil { + logWithDetails.Error("failed to close previous index", "key", key, "err", err) + } + } + + // Start a background task to cleanup the old index directories. If we have built a new file-based index, + // the new name is ignored. If we have created in-memory index and fileIndexName is empty, all old directories can be removed. + go b.cleanOldIndexes(resourceDir, fileIndexName) + return idx, nil } -func (b *bleveBackend) cleanOldIndexes(dir string, skip string) { +func cleanFileSegment(input string) string { + input = strings.ReplaceAll(input, string(filepath.Separator), "_") + input = strings.ReplaceAll(input, "..", "_") + return input +} + +// cleanOldIndexes deletes all subdirectories inside dir, skipping directory with "skipName". +// "skipName" can be empty. +func (b *bleveBackend) cleanOldIndexes(dir string, skipName string) { files, err := os.ReadDir(dir) if err != nil { + if os.IsNotExist(err) { + return + } b.log.Warn("error cleaning folders from", "directory", dir, "error", err) return } for _, file := range files { - if file.IsDir() && file.Name() != skip { + if file.IsDir() && file.Name() != skipName { fpath := filepath.Join(dir, file.Name()) - if !isValidPath(dir, b.opts.Root) { - b.log.Error("Path is not valid", "directory", fpath, "error", err) + if !isPathWithinRoot(fpath, b.opts.Root) { + b.log.Warn("Skipping cleanup of directory", "directory", fpath) + continue } + err = os.RemoveAll(fpath) if err != nil { b.log.Error("Unable to remove old index folder", "directory", fpath, "error", err) @@ -281,31 +353,45 @@ func (b *bleveBackend) cleanOldIndexes(dir string, skip string) { } } -// isValidPath does a sanity check in case it tries to access a different dir -func isValidPath(path, safeDir string) bool { - if path == "" || safeDir == "" { +// isPathWithinRoot verifies that path is within given absoluteRoot. +func isPathWithinRoot(path, absoluteRoot string) bool { + if path == "" || absoluteRoot == "" { return false } - cleanPath := filepath.Clean(path) - cleanSafeDir := filepath.Clean(safeDir) - rel, err := filepath.Rel(cleanSafeDir, cleanPath) + path, err := filepath.Abs(path) if err != nil { return false } - return !strings.HasPrefix(rel, "..") && !strings.Contains(rel, "\\") + if !strings.HasPrefix(path, absoluteRoot) { + return false + } + return true +} + +// cacheKeys returns list of keys for indexes in the cache (including possibly expired ones). +func (b *bleveBackend) cacheKeys() []resource.NamespacedResource { + b.cacheMx.RLock() + defer b.cacheMx.RUnlock() + + keys := make([]resource.NamespacedResource, 0, len(b.cache)) + for k := range b.cache { + keys = append(keys, k) + } + return keys } // TotalDocs returns the total number of documents across all indices func (b *bleveBackend) TotalDocs() int64 { var totalDocs int64 - for _, v := range b.cache.Items() { - idx, ok := v.Object.(*bleveIndex) - if !ok { - b.log.Warn("cache item is not a bleve index", "key", v.Object) + // We iterate over keys and call getCachedIndex for each index individually. + // We do this to avoid keeping a lock for the entire TotalDocs function, since DocCount may be slow (due to disk access). + // Calling getCachedIndex also handles index expiration. + for _, key := range b.cacheKeys() { + idx := b.getCachedIndex(key) + if idx == nil { continue } - c, err := idx.index.DocCount() if err != nil { continue @@ -315,6 +401,74 @@ func (b *bleveBackend) TotalDocs() int64 { return totalDocs } +func formatIndexName(now time.Time, resourceVersion int64) string { + timestamp := now.Format("20060102-150405") + return fmt.Sprintf("%s-%d", timestamp, resourceVersion) +} + +func (b *bleveBackend) findPreviousFileBasedIndex(resourceDir string, resourceVersion int64, size int64) (bleve.Index, string) { + entries, err := os.ReadDir(resourceDir) + if err != nil { + return nil, "" + } + + indexName := "" + for _, ent := range entries { + if !ent.IsDir() { + continue + } + + parts := strings.Split(ent.Name(), "-") + if len(parts) != 3 { + continue + } + + // Last part is resourceVersion + indexRv, err := strconv.ParseInt(parts[2], 10, 64) + if err != nil { + continue + } + if indexRv != resourceVersion { + continue + } + indexName = ent.Name() + break + } + + if indexName == "" { + return nil, "" + } + + indexDir := filepath.Join(resourceDir, indexName) + idx, err := bleve.Open(indexDir) + if err != nil { + return nil, "" + } + + cnt, err := idx.DocCount() + if err != nil { + _ = idx.Close() + return nil, "" + } + + if uint64(size) != cnt { + _ = idx.Close() + return nil, "" + } + + return idx, indexName +} + +func (b *bleveBackend) closeAllIndexes() { + b.cacheMx.Lock() + defer b.cacheMx.Unlock() + + for key, idx := range b.cache { + _ = idx.index.Close() + delete(b.cache, key) + } +} + type bleveIndex struct { key resource.NamespacedResource index bleve.Index @@ -322,6 +476,10 @@ type bleveIndex struct { standard resource.SearchableDocumentFields fields resource.SearchableDocumentFields + // When to expire and close the index. Zero value = no expiration. + // We only expire in-memory indexes. + expiration time.Time + // The values returned with all allFields []*resourcepb.ResourceTableColumnDefinition features featuremgmt.FeatureToggles diff --git a/pkg/storage/unified/search/bleve_integration_test.go b/pkg/storage/unified/search/bleve_integration_test.go index 6c28d560087..f76f89255e0 100644 --- a/pkg/storage/unified/search/bleve_integration_test.go +++ b/pkg/storage/unified/search/bleve_integration_test.go @@ -13,10 +13,6 @@ import ( ) func TestBleveSearchBackend(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - // Run the search backend test suite unitest.RunSearchBackendTest(t, func(ctx context.Context) resource.SearchBackend { tempDir := t.TempDir() diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index a2ea6afaced..b6e2bd05611 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -8,7 +8,9 @@ import ( "os" "path/filepath" "testing" + "time" + "github.com/blevesearch/bleve/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -671,65 +673,301 @@ func TestSafeInt64ToInt(t *testing.T) { } } -func Test_isValidPath(t *testing.T) { +func Test_isPathWithinRoot(t *testing.T) { tests := []struct { - name string - dir string - safeDir string - want bool + name string + dir string + root string + want bool }{ { - name: "valid path", - dir: "/path/to/my-file/", - safeDir: "/path/to/", - want: true, + name: "valid path", + dir: "/path/to/my-file/", + root: "/path/to/", + want: true, }, { - name: "valid path without trailing slash", - dir: "/path/to/my-file", - safeDir: "/path/to", - want: true, + name: "valid path without trailing slash", + dir: "/path/to/my-file", + root: "/path/to", + want: true, }, { - name: "path with double slashes", - dir: "/path//to//my-file/", - safeDir: "/path/to/", - want: true, + name: "path with double slashes", + dir: "/path//to//my-file/", + root: "/path/to/", + want: true, }, { - name: "invalid path: ..", - dir: "/path/../above/", - safeDir: "/path/to/", + name: "invalid path: ..", + dir: "/path/../above/", + root: "/path/to/", }, { - name: "invalid path: \\", - dir: "\\path/to", - safeDir: "/path/to/", + name: "invalid path: \\", + dir: "\\path/to", + root: "/path/to/", }, { - name: "invalid path: not under safe dir", - dir: "/path/to.txt", - safeDir: "/path/to/", + name: "invalid path: not under safe dir", + dir: "/path/to.txt", + root: "/path/to/", }, { - name: "invalid path: empty paths", - dir: "", - safeDir: "/path/to/", + name: "invalid path: empty paths", + dir: "", + root: "/path/to/", }, { - name: "invalid path: different path", - dir: "/other/path/to/my-file/", - safeDir: "/Some/other/path", + name: "invalid path: different path", + dir: "/other/path/to/my-file/", + root: "/Some/other/path", }, { - name: "invalid path: empty safe path", - dir: "/path/to/", - safeDir: "", + name: "invalid path: empty safe path", + dir: "/path/to/", + root: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, isValidPath(tt.dir, tt.safeDir)) + require.Equal(t, tt.want, isPathWithinRoot(tt.dir, tt.root)) }) } } + +func setupBleveBackend(t *testing.T, fileThreshold int, cacheTTL time.Duration, dir string) *bleveBackend { + if dir == "" { + dir = t.TempDir() + } + backend, err := NewBleveBackend(BleveOptions{ + Root: dir, + FileThreshold: int64(fileThreshold), + IndexCacheTTL: cacheTTL, + }, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering), nil) + require.NoError(t, err) + require.NotNil(t, backend) + t.Cleanup(backend.closeAllIndexes) + return backend +} + +func TestBleveInMemoryIndexExpiration(t *testing.T) { + backend := setupBleveBackend(t, 5, time.Nanosecond, "") + + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + builtIndex, err := backend.BuildIndex(context.Background(), ns, 1 /* below FileThreshold */, 100, nil, indexTestDocs(ns, 1)) + require.NoError(t, err) + + // Wait for index expiration, which is 1ns + time.Sleep(10 * time.Millisecond) + idx, err := backend.GetIndex(context.Background(), ns) + require.NoError(t, err) + require.Nil(t, idx) + + // Verify that builtIndex is now closed. + _, err = builtIndex.DocCount(context.Background(), "") + require.ErrorIs(t, err, bleve.ErrorIndexClosed) +} + +func TestBleveFileIndexExpiration(t *testing.T) { + backend := setupBleveBackend(t, 5, time.Nanosecond, "") + + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + // size=100 is above FileThreshold, this will be file-based index + builtIndex, err := backend.BuildIndex(context.Background(), ns, 100, 100, nil, indexTestDocs(ns, 1)) + require.NoError(t, err) + + // Wait for index expiration, which is 1ns + time.Sleep(10 * time.Millisecond) + idx, err := backend.GetIndex(context.Background(), ns) + require.NoError(t, err) + require.NotNil(t, idx) + + // Verify that builtIndex is still open. + cnt, err := builtIndex.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(1), cnt) +} + +func TestFileIndexIsReusedOnSameSizeAndRV(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + tmpDir := t.TempDir() + + backend1 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + _, err := backend1.BuildIndex(context.Background(), ns, 10 /* file based */, 100, nil, indexTestDocs(ns, 10)) + require.NoError(t, err) + backend1.closeAllIndexes() + + // We open new backend using same directory, and run indexing with same size (10) and RV (100). This should reuse existing index, and skip indexing. + backend2 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + idx, err := backend2.BuildIndex(context.Background(), ns, 10 /* file based */, 100, nil, indexTestDocs(ns, 1000)) + require.NoError(t, err) + + // Verify that we're reusing existing index and there is only 10 documents in it, not 1000. + cnt, err := idx.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(10), cnt) +} + +func TestFileIndexIsNotReusedOnDifferentSize(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + tmpDir := t.TempDir() + + backend1 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + _, err := backend1.BuildIndex(context.Background(), ns, 10, 100, nil, indexTestDocs(ns, 10)) + require.NoError(t, err) + backend1.closeAllIndexes() + + // We open new backend using same directory, but with different size. Index should be rebuilt. + backend2 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + idx, err := backend2.BuildIndex(context.Background(), ns, 100, 100, nil, indexTestDocs(ns, 100)) + require.NoError(t, err) + + // Verify that index has updated number of documents. + cnt, err := idx.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(100), cnt) +} + +func TestFileIndexIsNotReusedOnDifferentRV(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + tmpDir := t.TempDir() + + backend1 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + _, err := backend1.BuildIndex(context.Background(), ns, 10, 100, nil, indexTestDocs(ns, 10)) + require.NoError(t, err) + backend1.closeAllIndexes() + + // We open new backend using same directory, but with different RV. Index should be rebuilt. + backend2 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + idx, err := backend2.BuildIndex(context.Background(), ns, 10 /* file based */, 999999, nil, indexTestDocs(ns, 100)) + require.NoError(t, err) + + // Verify that index has updated number of documents. + cnt, err := idx.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(100), cnt) +} + +func TestRebuildingIndexClosesPreviousCachedIndex(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + for name, testCase := range map[string]struct { + firstInMemory bool + secondInMemory bool + }{ + "in-memory, in-memory": {true, true}, + "in-memory, file": {true, false}, + "file, in-memory": {false, true}, + "file, file": {false, false}, + } { + t.Run(name, func(t *testing.T) { + backend := setupBleveBackend(t, 5, time.Nanosecond, "") + + firstSize := 100 + if testCase.firstInMemory { + firstSize = 1 + } + firstIndex, err := backend.BuildIndex(context.Background(), ns, int64(firstSize), 100, nil, indexTestDocs(ns, firstSize)) + require.NoError(t, err) + + secondSize := 100 + if testCase.firstInMemory { + secondSize = 1 + } + secondIndex, err := backend.BuildIndex(context.Background(), ns, int64(secondSize), 100, nil, indexTestDocs(ns, secondSize)) + require.NoError(t, err) + + // Verify that first and second index are different, and first one is now closed. + require.NotEqual(t, firstIndex, secondIndex) + + _, err = firstIndex.DocCount(context.Background(), "") + require.ErrorIs(t, err, bleve.ErrorIndexClosed) + + cnt, err := secondIndex.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(secondSize), cnt) + }) + } +} + +func indexTestDocs(ns resource.NamespacedResource, docs int) func(index resource.ResourceIndex) (int64, error) { + return func(index resource.ResourceIndex) (int64, error) { + var items []*resource.BulkIndexItem + for i := 0; i < docs; i++ { + items = append(items, &resource.BulkIndexItem{ + Action: resource.ActionIndex, + Doc: &resource.IndexableDocument{ + Key: &resourcepb.ResourceKey{ + Namespace: ns.Namespace, + Group: ns.Group, + Resource: ns.Resource, + Name: fmt.Sprintf("doc%d", i), + }, + Title: fmt.Sprintf("Document %d", i), + }, + }) + } + + err := index.BulkIndex(&resource.BulkIndexRequest{Items: items}) + return int64(docs), err + } +} + +func TestCleanOldIndexes(t *testing.T) { + dir := t.TempDir() + + b := setupBleveBackend(t, 5, time.Nanosecond, dir) + + t.Run("with skip", func(t *testing.T) { + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-1/a"), 0750)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-2/b"), 0750)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-3/c"), 0750)) + + b.cleanOldIndexes(dir, "index-2") + files, err := os.ReadDir(dir) + require.NoError(t, err) + require.Len(t, files, 1) + require.Equal(t, "index-2", files[0].Name()) + }) + + t.Run("without skip", func(t *testing.T) { + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-1/a"), 0750)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-2/b"), 0750)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-3/c"), 0750)) + + b.cleanOldIndexes(dir, "") + files, err := os.ReadDir(dir) + require.NoError(t, err) + require.Len(t, files, 0) + }) +} diff --git a/pkg/storage/unified/sql/db/mocks/DB.go b/pkg/storage/unified/sql/db/mocks/DB.go index ceda9e2865d..0177a0b3c1e 100644 --- a/pkg/storage/unified/sql/db/mocks/DB.go +++ b/pkg/storage/unified/sql/db/mocks/DB.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -83,7 +83,7 @@ func (_c *DB_BeginTx_Call) RunAndReturn(run func(context.Context, *sql.TxOptions return _c } -// DriverName provides a mock function with given fields: +// DriverName provides a mock function with no fields func (_m *DB) DriverName() string { ret := _m.Called() @@ -374,7 +374,7 @@ func (_c *DB_QueryRowContext_Call) RunAndReturn(run func(context.Context, string return _c } -// Stats provides a mock function with given fields: +// Stats provides a mock function with no fields func (_m *DB) Stats() sql.DBStats { ret := _m.Called() diff --git a/pkg/storage/unified/sql/db/mocks/Row.go b/pkg/storage/unified/sql/db/mocks/Row.go index 26b1b68c178..b4d0228d3c1 100644 --- a/pkg/storage/unified/sql/db/mocks/Row.go +++ b/pkg/storage/unified/sql/db/mocks/Row.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -17,7 +17,7 @@ func (_m *Row) EXPECT() *Row_Expecter { return &Row_Expecter{mock: &_m.Mock} } -// Err provides a mock function with given fields: +// Err provides a mock function with no fields func (_m *Row) Err() error { ret := _m.Called() diff --git a/pkg/storage/unified/sql/db/mocks/Rows.go b/pkg/storage/unified/sql/db/mocks/Rows.go index 33beddd5eed..dd653194844 100644 --- a/pkg/storage/unified/sql/db/mocks/Rows.go +++ b/pkg/storage/unified/sql/db/mocks/Rows.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -17,7 +17,7 @@ func (_m *Rows) EXPECT() *Rows_Expecter { return &Rows_Expecter{mock: &_m.Mock} } -// Close provides a mock function with given fields: +// Close provides a mock function with no fields func (_m *Rows) Close() error { ret := _m.Called() @@ -62,7 +62,7 @@ func (_c *Rows_Close_Call) RunAndReturn(run func() error) *Rows_Close_Call { return _c } -// Err provides a mock function with given fields: +// Err provides a mock function with no fields func (_m *Rows) Err() error { ret := _m.Called() @@ -107,7 +107,7 @@ func (_c *Rows_Err_Call) RunAndReturn(run func() error) *Rows_Err_Call { return _c } -// Next provides a mock function with given fields: +// Next provides a mock function with no fields func (_m *Rows) Next() bool { ret := _m.Called() @@ -152,7 +152,7 @@ func (_c *Rows_Next_Call) RunAndReturn(run func() bool) *Rows_Next_Call { return _c } -// NextResultSet provides a mock function with given fields: +// NextResultSet provides a mock function with no fields func (_m *Rows) NextResultSet() bool { ret := _m.Called() diff --git a/pkg/storage/unified/sql/db/mocks/Tx.go b/pkg/storage/unified/sql/db/mocks/Tx.go index 342e12a493c..4f70231aae3 100644 --- a/pkg/storage/unified/sql/db/mocks/Tx.go +++ b/pkg/storage/unified/sql/db/mocks/Tx.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -24,7 +24,7 @@ func (_m *Tx) EXPECT() *Tx_Expecter { return &Tx_Expecter{mock: &_m.Mock} } -// Commit provides a mock function with given fields: +// Commit provides a mock function with no fields func (_m *Tx) Commit() error { ret := _m.Called() @@ -269,7 +269,7 @@ func (_c *Tx_QueryRowContext_Call) RunAndReturn(run func(context.Context, string return _c } -// Rollback provides a mock function with given fields: +// Rollback provides a mock function with no fields func (_m *Tx) Rollback() error { ret := _m.Called() diff --git a/pkg/storage/unified/sql/db/mocks/result.go b/pkg/storage/unified/sql/db/mocks/result.go index 73aa331cd82..6999e42e194 100644 --- a/pkg/storage/unified/sql/db/mocks/result.go +++ b/pkg/storage/unified/sql/db/mocks/result.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -17,7 +17,7 @@ func (_m *Result) EXPECT() *Result_Expecter { return &Result_Expecter{mock: &_m.Mock} } -// LastInsertId provides a mock function with given fields: +// LastInsertId provides a mock function with no fields func (_m *Result) LastInsertId() (int64, error) { ret := _m.Called() @@ -72,7 +72,7 @@ func (_c *Result_LastInsertId_Call) RunAndReturn(run func() (int64, error)) *Res return _c } -// RowsAffected provides a mock function with given fields: +// RowsAffected provides a mock function with no fields func (_m *Result) RowsAffected() (int64, error) { ret := _m.Called() diff --git a/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go b/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplate.go similarity index 97% rename from pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go rename to pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplate.go index 86cbb32fd5b..baeb4d1542e 100644 --- a/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go +++ b/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplate.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -171,7 +171,7 @@ func (_c *SQLTemplate_ArgPlaceholder_Call) RunAndReturn(run func(int) string) *S return _c } -// CurrentEpoch provides a mock function with given fields: +// CurrentEpoch provides a mock function with no fields func (_m *SQLTemplate) CurrentEpoch() string { ret := _m.Called() @@ -216,7 +216,7 @@ func (_c *SQLTemplate_CurrentEpoch_Call) RunAndReturn(run func() string) *SQLTem return _c } -// DialectName provides a mock function with given fields: +// DialectName provides a mock function with no fields func (_m *SQLTemplate) DialectName() string { ret := _m.Called() @@ -261,7 +261,7 @@ func (_c *SQLTemplate_DialectName_Call) RunAndReturn(run func() string) *SQLTemp return _c } -// GetArgs provides a mock function with given fields: +// GetArgs provides a mock function with no fields func (_m *SQLTemplate) GetArgs() []interface{} { ret := _m.Called() @@ -308,7 +308,7 @@ func (_c *SQLTemplate_GetArgs_Call) RunAndReturn(run func() []interface{}) *SQLT return _c } -// GetColNames provides a mock function with given fields: +// GetColNames provides a mock function with no fields func (_m *SQLTemplate) GetColNames() []string { ret := _m.Called() @@ -355,7 +355,7 @@ func (_c *SQLTemplate_GetColNames_Call) RunAndReturn(run func() []string) *SQLTe return _c } -// GetScanDest provides a mock function with given fields: +// GetScanDest provides a mock function with no fields func (_m *SQLTemplate) GetScanDest() []interface{} { ret := _m.Called() @@ -515,7 +515,7 @@ func (_c *SQLTemplate_Into_Call) RunAndReturn(run func(reflect.Value, string) (s return _c } -// Reset provides a mock function with given fields: +// Reset provides a mock function with no fields func (_m *SQLTemplate) Reset() { _m.Called() } @@ -543,7 +543,7 @@ func (_c *SQLTemplate_Reset_Call) Return() *SQLTemplate_Reset_Call { } func (_c *SQLTemplate_Reset_Call) RunAndReturn(run func()) *SQLTemplate_Reset_Call { - _c.Call.Return(run) + _c.Run(run) return _c } @@ -645,11 +645,11 @@ func (_c *SQLTemplate_SetDialect_Call) Return() *SQLTemplate_SetDialect_Call { } func (_c *SQLTemplate_SetDialect_Call) RunAndReturn(run func(sqltemplate.Dialect)) *SQLTemplate_SetDialect_Call { - _c.Call.Return(run) + _c.Run(run) return _c } -// Validate provides a mock function with given fields: +// Validate provides a mock function with no fields func (_m *SQLTemplate) Validate() error { ret := _m.Called() diff --git a/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go b/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go index 50f71da1e28..c109512de5d 100644 --- a/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go +++ b/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -171,7 +171,7 @@ func (_c *WithResults_ArgPlaceholder_Call[T]) RunAndReturn(run func(int) string) return _c } -// CurrentEpoch provides a mock function with given fields: +// CurrentEpoch provides a mock function with no fields func (_m *WithResults[T]) CurrentEpoch() string { ret := _m.Called() @@ -216,7 +216,7 @@ func (_c *WithResults_CurrentEpoch_Call[T]) RunAndReturn(run func() string) *Wit return _c } -// DialectName provides a mock function with given fields: +// DialectName provides a mock function with no fields func (_m *WithResults[T]) DialectName() string { ret := _m.Called() @@ -261,7 +261,7 @@ func (_c *WithResults_DialectName_Call[T]) RunAndReturn(run func() string) *With return _c } -// GetArgs provides a mock function with given fields: +// GetArgs provides a mock function with no fields func (_m *WithResults[T]) GetArgs() []interface{} { ret := _m.Called() @@ -308,7 +308,7 @@ func (_c *WithResults_GetArgs_Call[T]) RunAndReturn(run func() []interface{}) *W return _c } -// GetColNames provides a mock function with given fields: +// GetColNames provides a mock function with no fields func (_m *WithResults[T]) GetColNames() []string { ret := _m.Called() @@ -355,7 +355,7 @@ func (_c *WithResults_GetColNames_Call[T]) RunAndReturn(run func() []string) *Wi return _c } -// GetScanDest provides a mock function with given fields: +// GetScanDest provides a mock function with no fields func (_m *WithResults[T]) GetScanDest() []interface{} { ret := _m.Called() @@ -515,7 +515,7 @@ func (_c *WithResults_Into_Call[T]) RunAndReturn(run func(reflect.Value, string) return _c } -// Reset provides a mock function with given fields: +// Reset provides a mock function with no fields func (_m *WithResults[T]) Reset() { _m.Called() } @@ -543,11 +543,11 @@ func (_c *WithResults_Reset_Call[T]) Return() *WithResults_Reset_Call[T] { } func (_c *WithResults_Reset_Call[T]) RunAndReturn(run func()) *WithResults_Reset_Call[T] { - _c.Call.Return(run) + _c.Run(run) return _c } -// Results provides a mock function with given fields: +// Results provides a mock function with no fields func (_m *WithResults[T]) Results() (T, error) { ret := _m.Called() @@ -563,7 +563,9 @@ func (_m *WithResults[T]) Results() (T, error) { if rf, ok := ret.Get(0).(func() T); ok { r0 = rf() } else { - r0 = ret.Get(0).(T) + if ret.Get(0) != nil { + r0 = ret.Get(0).(T) + } } if rf, ok := ret.Get(1).(func() error); ok { @@ -700,11 +702,11 @@ func (_c *WithResults_SetDialect_Call[T]) Return() *WithResults_SetDialect_Call[ } func (_c *WithResults_SetDialect_Call[T]) RunAndReturn(run func(sqltemplate.Dialect)) *WithResults_SetDialect_Call[T] { - _c.Call.Return(run) + _c.Run(run) return _c } -// Validate provides a mock function with given fields: +// Validate provides a mock function with no fields func (_m *WithResults[T]) Validate() error { ret := _m.Called() diff --git a/pkg/storage/unified/sql/test/benchmark_test.go b/pkg/storage/unified/sql/test/benchmark_test.go index d937abb70ab..88f9ed2c955 100644 --- a/pkg/storage/unified/sql/test/benchmark_test.go +++ b/pkg/storage/unified/sql/test/benchmark_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" test "github.com/grafana/grafana/pkg/storage/unified/testing" + "github.com/grafana/grafana/pkg/tests" ) func newTestBackend(b testing.TB) resource.StorageBackend { @@ -38,9 +39,7 @@ func newTestBackend(b testing.TB) resource.StorageBackend { } func TestIntegrationBenchmarkSQLStorageBackend(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } + tests.SkipIntegrationTestInShortMode(t) opts := test.DefaultBenchmarkOptions() if db.IsTestDbSQLite() { opts.Concurrency = 1 // to avoid SQLite database is locked error @@ -49,9 +48,7 @@ func TestIntegrationBenchmarkSQLStorageBackend(t *testing.T) { } func TestIntegrationBenchmarkResourceServer(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } + tests.SkipIntegrationTestInShortMode(t) ctx := context.Background() opts := &test.BenchmarkOptions{ diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 99ff031aab1..e33af48713f 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" unitest "github.com/grafana/grafana/pkg/storage/unified/testing" + "github.com/grafana/grafana/pkg/tests" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -95,9 +96,7 @@ func TestIntegrationSQLStorageBackend(t *testing.T) { } func TestIntegrationSearchAndStorage(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } + tests.SkipIntegrationTestInShortMode(t) ctx := context.Background() diff --git a/pkg/storage/unified/testing/kv.go b/pkg/storage/unified/testing/kv.go index 5c205697e69..0c3b745fdf0 100644 --- a/pkg/storage/unified/testing/kv.go +++ b/pkg/storage/unified/testing/kv.go @@ -43,10 +43,6 @@ func GenerateRandomKVPrefix() string { // RunKVTest runs the KV test suite func RunKVTest(t *testing.T, newKV NewKVFunc, opts *KVTestOptions) { - if testing.Short() { - t.Skip("skipping integration test") - } - if opts == nil { opts = &KVTestOptions{} } diff --git a/pkg/storage/unified/testing/search_backend.go b/pkg/storage/unified/testing/search_backend.go index 505e1b1a43d..57d887682c6 100644 --- a/pkg/storage/unified/testing/search_backend.go +++ b/pkg/storage/unified/testing/search_backend.go @@ -24,10 +24,6 @@ type NewSearchBackendFunc func(ctx context.Context) resource.SearchBackend // RunSearchBackendTest runs the search backend test suite func RunSearchBackendTest(t *testing.T, newBackend NewSearchBackendFunc, opts *TestOptions) { - if testing.Short() { - t.Skip("skipping integration test") - } - if opts == nil { opts = &TestOptions{} } diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go index 2f369c6d233..e3314376715 100644 --- a/pkg/storage/unified/testing/storage_backend.go +++ b/pkg/storage/unified/testing/storage_backend.go @@ -1106,7 +1106,7 @@ func writeEvent(ctx context.Context, store resource.StorageBackend, name string, } meta.SetFolder(options.Folder) - return store.WriteEvent(ctx, resource.WriteEvent{ + event := resource.WriteEvent{ Type: action, Value: options.Value, GUID: uuid.New().String(), @@ -1116,8 +1116,32 @@ func writeEvent(ctx context.Context, store resource.StorageBackend, name string, Resource: options.Resource, Name: name, }, - Object: meta, - }) + } + switch action { + case resourcepb.WatchEvent_DELETED: + event.ObjectOld = meta + + obj, err := utils.MetaAccessor(res) + if err != nil { + return 0, err + } + now := metav1.Now() + obj.SetDeletionTimestamp(&now) + obj.SetUpdatedTimestamp(&now.Time) + obj.SetManagedFields(nil) + obj.SetFinalizers(nil) + obj.SetGeneration(utils.DeletedGeneration) + obj.SetAnnotation(utils.AnnoKeyKubectlLastAppliedConfig, "") // clears it + event.Object = obj + case resourcepb.WatchEvent_ADDED: + event.Object = meta + case resourcepb.WatchEvent_MODIFIED: + event.Object = meta // + event.ObjectOld = meta + default: + panic(fmt.Sprintf("invalid action: %s", action)) + } + return store.WriteEvent(ctx, event) } func newServer(t *testing.T, b resource.StorageBackend) resource.ResourceServer { diff --git a/pkg/storage/unified/testing/storage_backend_test.go b/pkg/storage/unified/testing/storage_backend_test.go index c0be0065c2f..a1da3b82e72 100644 --- a/pkg/storage/unified/testing/storage_backend_test.go +++ b/pkg/storage/unified/testing/storage_backend_test.go @@ -11,7 +11,6 @@ import ( ) func TestBadgerKVStorageBackend(t *testing.T) { - t.Skip("failing with 'panic: DB Closed'") RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { opts := badger.DefaultOptions("").WithInMemory(true).WithLogger(nil) db, err := badger.Open(opts) diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 45b82107af4..e74d718d9bf 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -22,8 +22,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg", - "large": "public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg" + "small": "public/plugins/alertlist/img/icn-singlestat-panel.svg", + "large": "public/plugins/alertlist/img/icn-singlestat-panel.svg" }, "build": {}, "screenshots": null, @@ -76,8 +76,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/alertmanager/img/logo.svg", - "large": "public/app/plugins/datasource/alertmanager/img/logo.svg" + "small": "public/plugins/alertmanager/img/logo.svg", + "large": "public/plugins/alertmanager/img/logo.svg" }, "build": {}, "screenshots": null, @@ -133,8 +133,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/annolist/img/icn-annolist-panel.svg", - "large": "public/app/plugins/panel/annolist/img/icn-annolist-panel.svg" + "small": "public/plugins/annolist/img/icn-annolist-panel.svg", + "large": "public/plugins/annolist/img/icn-annolist-panel.svg" }, "build": {}, "screenshots": null, @@ -191,22 +191,22 @@ } ], "logos": { - "small": "public/app/plugins/datasource/azuremonitor/img/logo.jpg", - "large": "public/app/plugins/datasource/azuremonitor/img/logo.jpg" + "small": "public/plugins/grafana-azure-monitor-datasource/img/logo.jpg", + "large": "public/plugins/grafana-azure-monitor-datasource/img/logo.jpg" }, "build": {}, "screenshots": [ { "name": "Azure Contoso Loans", - "path": "public/app/plugins/datasource/azuremonitor/img/contoso_loans_grafana_dashboard.png" + "path": "public/plugins/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png" }, { "name": "Azure Monitor Network", - "path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_network.png" + "path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_network.png" }, { "name": "Azure Monitor CPU", - "path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_cpu.png" + "path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png" } ], "version": "12.1.0-pre", @@ -260,8 +260,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/barchart/img/barchart.svg", - "large": "public/app/plugins/panel/barchart/img/barchart.svg" + "small": "public/plugins/barchart/img/barchart.svg", + "large": "public/plugins/barchart/img/barchart.svg" }, "build": {}, "screenshots": null, @@ -310,8 +310,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg", - "large": "public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg" + "small": "public/plugins/bargauge/img/icon_bar_gauge.svg", + "large": "public/plugins/bargauge/img/icon_bar_gauge.svg" }, "build": {}, "screenshots": null, @@ -360,8 +360,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/candlestick/img/candlestick.svg", - "large": "public/app/plugins/panel/candlestick/img/candlestick.svg" + "small": "public/plugins/candlestick/img/candlestick.svg", + "large": "public/plugins/candlestick/img/candlestick.svg" }, "build": {}, "screenshots": null, @@ -415,8 +415,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/canvas/img/icn-canvas.svg", - "large": "public/app/plugins/panel/canvas/img/icn-canvas.svg" + "small": "public/plugins/canvas/img/icn-canvas.svg", + "large": "public/plugins/canvas/img/icn-canvas.svg" }, "build": {}, "screenshots": null, @@ -465,8 +465,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - "large": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png" + "small": "public/plugins/cloudwatch/img/amazon-web-services.png", + "large": "public/plugins/cloudwatch/img/amazon-web-services.png" }, "build": {}, "screenshots": null, @@ -518,8 +518,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg" + "small": "public/plugins/dashlist/img/icn-dashlist-panel.svg", + "large": "public/plugins/dashlist/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -568,8 +568,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/datagrid/img/icn-table-panel.svg", - "large": "public/app/plugins/panel/datagrid/img/icn-table-panel.svg" + "small": "public/plugins/datagrid/img/icn-table-panel.svg", + "large": "public/plugins/datagrid/img/icn-table-panel.svg" }, "build": {}, "screenshots": null, @@ -622,8 +622,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg", - "large": "public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg" + "small": "public/plugins/elasticsearch/img/elasticsearch.svg", + "large": "public/plugins/elasticsearch/img/elasticsearch.svg" }, "build": {}, "screenshots": null, @@ -679,8 +679,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/flamegraph/img/icn-flamegraph.svg", - "large": "public/app/plugins/panel/flamegraph/img/icn-flamegraph.svg" + "small": "public/plugins/flamegraph/img/icn-flamegraph.svg", + "large": "public/plugins/flamegraph/img/icn-flamegraph.svg" }, "build": {}, "screenshots": null, @@ -729,8 +729,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/gauge/img/icon_gauge.svg", - "large": "public/app/plugins/panel/gauge/img/icon_gauge.svg" + "small": "public/plugins/gauge/img/icon_gauge.svg", + "large": "public/plugins/gauge/img/icon_gauge.svg" }, "build": {}, "screenshots": null, @@ -779,8 +779,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/geomap/img/icn-geomap.svg", - "large": "public/app/plugins/panel/geomap/img/icn-geomap.svg" + "small": "public/plugins/geomap/img/icn-geomap.svg", + "large": "public/plugins/geomap/img/icn-geomap.svg" }, "build": {}, "screenshots": null, @@ -825,8 +825,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg" + "small": "public/plugins/gettingstarted/img/icn-dashlist-panel.svg", + "large": "public/plugins/gettingstarted/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -875,8 +875,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg", - "large": "public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg" + "small": "public/plugins/stackdriver/img/cloud_monitoring_logo.svg", + "large": "public/plugins/stackdriver/img/cloud_monitoring_logo.svg" }, "build": {}, "screenshots": null, @@ -929,8 +929,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg", - "large": "public/app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg" + "small": "public/plugins/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg", + "large": "public/plugins/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg" }, "build": {}, "screenshots": null, @@ -995,8 +995,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/graphite/img/graphite_logo.png", - "large": "public/app/plugins/datasource/graphite/img/graphite_logo.png" + "small": "public/plugins/graphite/img/graphite_logo.png", + "large": "public/plugins/graphite/img/graphite_logo.png" }, "build": {}, "screenshots": null, @@ -1045,8 +1045,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg", - "large": "public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg" + "small": "public/plugins/heatmap/img/icn-heatmap-panel.svg", + "large": "public/plugins/heatmap/img/icn-heatmap-panel.svg" }, "build": {}, "screenshots": null, @@ -1095,8 +1095,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/histogram/img/histogram.svg", - "large": "public/app/plugins/panel/histogram/img/histogram.svg" + "small": "public/plugins/histogram/img/histogram.svg", + "large": "public/plugins/histogram/img/histogram.svg" }, "build": {}, "screenshots": null, @@ -1150,8 +1150,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/influxdb/img/influxdb_logo.svg", - "large": "public/app/plugins/datasource/influxdb/img/influxdb_logo.svg" + "small": "public/plugins/influxdb/img/influxdb_logo.svg", + "large": "public/plugins/influxdb/img/influxdb_logo.svg" }, "build": {}, "screenshots": null, @@ -1212,8 +1212,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/jaeger/img/jaeger_logo.svg", - "large": "public/app/plugins/datasource/jaeger/img/jaeger_logo.svg" + "small": "public/plugins/jaeger/img/jaeger_logo.svg", + "large": "public/plugins/jaeger/img/jaeger_logo.svg" }, "build": {}, "screenshots": null, @@ -1262,8 +1262,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/logs/img/icn-logs-panel.svg", - "large": "public/app/plugins/panel/logs/img/icn-logs-panel.svg" + "small": "public/plugins/logs/img/icn-logs-panel.svg", + "large": "public/plugins/logs/img/icn-logs-panel.svg" }, "build": {}, "screenshots": null, @@ -1320,8 +1320,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/loki/img/loki_icon.svg", - "large": "public/app/plugins/datasource/loki/img/loki_icon.svg" + "small": "public/plugins/loki/img/loki_icon.svg", + "large": "public/plugins/loki/img/loki_icon.svg" }, "build": {}, "screenshots": null, @@ -1370,8 +1370,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/mssql/img/sql_server_logo.svg", - "large": "public/app/plugins/datasource/mssql/img/sql_server_logo.svg" + "small": "public/plugins/mssql/img/sql_server_logo.svg", + "large": "public/plugins/mssql/img/sql_server_logo.svg" }, "build": {}, "screenshots": null, @@ -1420,8 +1420,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/mysql/img/mysql_logo.svg", - "large": "public/app/plugins/datasource/mysql/img/mysql_logo.svg" + "small": "public/plugins/mysql/img/mysql_logo.svg", + "large": "public/plugins/mysql/img/mysql_logo.svg" }, "build": {}, "screenshots": null, @@ -1470,8 +1470,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/news/img/news.svg", - "large": "public/app/plugins/panel/news/img/news.svg" + "small": "public/plugins/news/img/news.svg", + "large": "public/plugins/news/img/news.svg" }, "build": {}, "screenshots": null, @@ -1520,8 +1520,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg", - "large": "public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg" + "small": "public/plugins/nodeGraph/img/icn-node-graph.svg", + "large": "public/plugins/nodeGraph/img/icn-node-graph.svg" }, "build": {}, "screenshots": null, @@ -1570,8 +1570,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png", - "large": "public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png" + "small": "public/plugins/opentsdb/img/opentsdb_logo.png", + "large": "public/plugins/opentsdb/img/opentsdb_logo.png" }, "build": {}, "screenshots": null, @@ -1624,8 +1624,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/parca/img/logo-small.svg", - "large": "public/app/plugins/datasource/parca/img/logo-small.svg" + "small": "public/plugins/parca/img/logo-small.svg", + "large": "public/plugins/parca/img/logo-small.svg" }, "build": {}, "screenshots": null, @@ -1679,8 +1679,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/piechart/img/icon_piechart.svg", - "large": "public/app/plugins/panel/piechart/img/icon_piechart.svg" + "small": "public/plugins/piechart/img/icon_piechart.svg", + "large": "public/plugins/piechart/img/icon_piechart.svg" }, "build": {}, "screenshots": null, @@ -1729,8 +1729,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/grafana-postgresql-datasource/img/postgresql_logo.svg", - "large": "public/app/plugins/datasource/grafana-postgresql-datasource/img/postgresql_logo.svg" + "small": "public/plugins/grafana-postgresql-datasource/img/postgresql_logo.svg", + "large": "public/plugins/grafana-postgresql-datasource/img/postgresql_logo.svg" }, "build": {}, "screenshots": null, @@ -1783,8 +1783,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/prometheus/img/prometheus_logo.svg", - "large": "public/app/plugins/datasource/prometheus/img/prometheus_logo.svg" + "small": "public/plugins/prometheus/img/prometheus_logo.svg", + "large": "public/plugins/prometheus/img/prometheus_logo.svg" }, "build": {}, "screenshots": null, @@ -1833,8 +1833,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/stat/img/icn-singlestat-panel.svg", - "large": "public/app/plugins/panel/stat/img/icn-singlestat-panel.svg" + "small": "public/plugins/stat/img/icn-singlestat-panel.svg", + "large": "public/plugins/stat/img/icn-singlestat-panel.svg" }, "build": {}, "screenshots": null, @@ -1883,8 +1883,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/state-timeline/img/timeline.svg", - "large": "public/app/plugins/panel/state-timeline/img/timeline.svg" + "small": "public/plugins/state-timeline/img/timeline.svg", + "large": "public/plugins/state-timeline/img/timeline.svg" }, "build": {}, "screenshots": null, @@ -1933,8 +1933,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/status-history/img/status.svg", - "large": "public/app/plugins/panel/status-history/img/status.svg" + "small": "public/plugins/status-history/img/status.svg", + "large": "public/plugins/status-history/img/status.svg" }, "build": {}, "screenshots": null, @@ -1983,8 +1983,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/table/img/icn-table-panel.svg", - "large": "public/app/plugins/panel/table/img/icn-table-panel.svg" + "small": "public/plugins/table/img/icn-table-panel.svg", + "large": "public/plugins/table/img/icn-table-panel.svg" }, "build": {}, "screenshots": null, @@ -2037,8 +2037,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/tempo/img/tempo_logo.svg", - "large": "public/app/plugins/datasource/tempo/img/tempo_logo.svg" + "small": "public/plugins/tempo/img/tempo_logo.svg", + "large": "public/plugins/tempo/img/tempo_logo.svg" }, "build": {}, "screenshots": null, @@ -2087,8 +2087,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/grafana-testdata-datasource/img/testdata.svg", - "large": "public/app/plugins/datasource/grafana-testdata-datasource/img/testdata.svg" + "small": "public/plugins/grafana-testdata-datasource/img/testdata.svg", + "large": "public/plugins/grafana-testdata-datasource/img/testdata.svg" }, "build": {}, "screenshots": null, @@ -2137,8 +2137,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/text/img/icn-text-panel.svg", - "large": "public/app/plugins/panel/text/img/icn-text-panel.svg" + "small": "public/plugins/text/img/icn-text-panel.svg", + "large": "public/plugins/text/img/icn-text-panel.svg" }, "build": {}, "screenshots": null, @@ -2187,8 +2187,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg", - "large": "public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg" + "small": "public/plugins/timeseries/img/icn-timeseries-panel.svg", + "large": "public/plugins/timeseries/img/icn-timeseries-panel.svg" }, "build": {}, "screenshots": null, @@ -2237,8 +2237,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/traces/img/traces-panel.svg", - "large": "public/app/plugins/panel/traces/img/traces-panel.svg" + "small": "public/plugins/traces/img/traces-panel.svg", + "large": "public/plugins/traces/img/traces-panel.svg" }, "build": {}, "screenshots": null, @@ -2287,8 +2287,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/trend/img/trend.svg", - "large": "public/app/plugins/panel/trend/img/trend.svg" + "small": "public/plugins/trend/img/trend.svg", + "large": "public/plugins/trend/img/trend.svg" }, "build": {}, "screenshots": null, @@ -2333,8 +2333,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg" + "small": "public/plugins/welcome/img/icn-dashlist-panel.svg", + "large": "public/plugins/welcome/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -2383,8 +2383,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/xychart/img/icn-xychart.svg", - "large": "public/app/plugins/panel/xychart/img/icn-xychart.svg" + "small": "public/plugins/xychart/img/icn-xychart.svg", + "large": "public/plugins/xychart/img/icn-xychart.svg" }, "build": {}, "screenshots": null, @@ -2440,8 +2440,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/zipkin/img/zipkin-logo.svg", - "large": "public/app/plugins/datasource/zipkin/img/zipkin-logo.svg" + "small": "public/plugins/zipkin/img/zipkin-logo.svg", + "large": "public/plugins/zipkin/img/zipkin-logo.svg" }, "build": {}, "screenshots": null, @@ -2467,4 +2467,4 @@ "signatureOrg": "", "angularDetected": false } -] \ No newline at end of file +] diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index c107eacb2c9..900b7ed7da2 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -2,6 +2,7 @@ package dashboards import ( "context" + "fmt" "strings" "testing" @@ -13,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/apis" @@ -112,74 +114,28 @@ func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper, gvr schema.Group func TestIntegrationDashboardsAppV0Alpha1(t *testing.T) { gvr := schema.GroupVersionResource{ - Group: dashboardV1.GROUP, - Version: dashboardV1.VERSION, + Group: dashboardV0.GROUP, + Version: dashboardV0.VERSION, Resource: "dashboards", } if testing.Short() { t.Skip("skipping integration test") } - t.Run("v0alpha1 with dual writer mode 0", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 0, + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("v0alpha1 with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, }, - }, + }) + runDashboardTest(t, helper, gvr) }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v0alpha1 with dual writer mode 1", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 1, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v0alpha1 with dual writer mode 2", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 2, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v0alpha1 with dual writer mode 3", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 3, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v0alpha1 with dual writer mode 4", func(t *testing.T) { - t.Skip("skipping test because of authorizer issue") - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 4, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) + } } func TestIntegrationDashboardsAppV1(t *testing.T) { @@ -192,66 +148,46 @@ func TestIntegrationDashboardsAppV1(t *testing.T) { t.Skip("skipping integration test") } - t.Run("v1 with dual writer mode 0", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 0, + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("v1beta1 with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, }, - }, + }) + runDashboardTest(t, helper, gvr) }) - runDashboardTest(t, helper, gvr) - }) + } +} - t.Run("v1 with dual writer mode 1", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 1, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) +func TestIntegrationDashboardsAppV2(t *testing.T) { + gvr := schema.GroupVersionResource{ + Group: dashboardV2.GROUP, + Version: dashboardV2.VERSION, + Resource: "dashboards", + } + if testing.Short() { + t.Skip("skipping integration test") + } - t.Run("v1 with dual writer mode 2", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 2, + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("v1beta1 with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, }, - }, + }) + runDashboardTest(t, helper, gvr) }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v1 with dual writer mode 3", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 3, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v1 with dual writer mode 4", func(t *testing.T) { - t.Skip("skipping test because of authorizer issue") - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 4, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) + } } func TestIntegrationLegacySupport(t *testing.T) { diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index 2215808970c..26e86c8f510 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -71,6 +71,7 @@ func TestIntegrationValidation(t *testing.T) { EnableFeatureToggles: []string{ featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature featuremgmt.FlagUnifiedStorageSearch, + featuremgmt.FlagKubernetesDashboards, // Enable FE-only dashboard feature flag }, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": { @@ -81,6 +82,36 @@ func TestIntegrationValidation(t *testing.T) { testIntegrationValidationForServer(t, helper, dualWriterMode) }) } + + for _, dualWriterMode := range dualWriterModes { + t.Run(fmt.Sprintf("DualWriterMode %d - kubernetesDashboards disabled", dualWriterMode), func(t *testing.T) { + // Create a K8sTestHelper which will set up a real API server + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature + featuremgmt.FlagUnifiedStorageSearch, + }, + DisableFeatureToggles: []string{ + featuremgmt.FlagKubernetesDashboards, + }, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: dualWriterMode, + }, + }}) + + t.Cleanup(func() { + helper.Shutdown() + }) + + org1Ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) + + t.Run("Dashboard permission tests", func(t *testing.T) { + runDashboardPermissionTests(t, org1Ctx, false) + }) + }) + } } func testIntegrationValidationForServer(t *testing.T, helper *apis.K8sTestHelper, dualWriterMode rest.DualWriterMode) { @@ -106,7 +137,7 @@ func testIntegrationValidationForServer(t *testing.T, helper *apis.K8sTestHelper }) t.Run("Dashboard permission tests", func(t *testing.T) { - runDashboardPermissionTests(t, org1Ctx) + runDashboardPermissionTests(t, org1Ctx, true) }) t.Run("Dashboard LIST API test", func(t *testing.T) { @@ -1225,7 +1256,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { } // Run tests for dashboard permissions -func runDashboardPermissionTests(t *testing.T, ctx TestContext) { +func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashboardsEnabled bool) { t.Helper() // Get clients for each user @@ -1348,8 +1379,22 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext) { // Clean up dashboard err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) - err = viewerClient.Resource.Delete(context.Background(), dashViewer.GetName(), v1.DeleteOptions{}) - require.NoError(t, err) + + if kubernetesDashboardsEnabled { + // In case kubernetesDashboards feature flag is set to true, + // we don't grant admin permission to dashboard creator on nested folders. + // This means that the viewer will not be able to delete the dashboard. + err = viewerClient.Resource.Delete(context.Background(), dashViewer.GetName(), v1.DeleteOptions{}) + require.Error(t, err) + err = adminClient.Resource.Delete(context.Background(), dashViewer.GetName(), v1.DeleteOptions{}) + require.NoError(t, err) + } else { + // In case kubernetesDashboards feature flag is set to false, + // we grant admin permission to dashboard creator on nested folders. + // This means that the viewer will be able to delete the dashboard. + err = viewerClient.Resource.Delete(context.Background(), dashViewer.GetName(), v1.DeleteOptions{}) + require.NoError(t, err) + } // Clean up the folder err = adminFolderClient.Resource.Delete(context.Background(), folderUID, v1.DeleteOptions{}) diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 35722cbc964..29a5a6f1814 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -18,6 +18,7 @@ import ( folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/api/dtos" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -44,6 +45,10 @@ func TestIntegrationFoldersApp(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, EnableFeatureToggles: []string{ @@ -119,71 +124,94 @@ func TestIntegrationFoldersApp(t *testing.T) { }`, string(v1Disco)) }) - t.Run("with dual write (unified storage, mode 0)", func(t *testing.T) { - doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode0, - }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - })) - }) + // test on all dualwriter modes + for mode := 0; mode <= 4; mode++ { + modeDw := grafanarest.DualWriterMode(mode) - t.Run("with dual write (unified storage, mode 1)", func(t *testing.T) { - doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + t.Run(fmt.Sprintf("with dual write (unified storage, mode %v)", modeDw), func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - })) - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + }, + })) + }) - t.Run("with dual write (unified storage, mode 1, create nested folders)", func(t *testing.T) { - doNestedCreateTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create nested folders)", modeDw), func(t *testing.T) { + doNestedCreateTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagNestedFolders, - }, - })) - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + featuremgmt.FlagNestedFolders, + }, + })) + }) - t.Run("with dual write (unified storage, mode 1, create existing folder)", func(t *testing.T) { - doCreateDuplicateFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create existing folder)", modeDw), func(t *testing.T) { + doCreateDuplicateFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagNestedFolders, - }, - })) - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + featuremgmt.FlagNestedFolders, + }, + })) + }) + + t.Run(fmt.Sprintf("when creating a folder, mode %v, it should trim leading and trailing spaces", modeDw), func(t *testing.T) { + doCreateEnsureTitleIsTrimmedTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + featuremgmt.FlagNestedFolders, + }, + })) + }) + + t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create circular reference folder)", modeDw), func(t *testing.T) { + doCreateCircularReferenceFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + featuremgmt.FlagNestedFolders, + }, + })) + }) + } // This is a general test for the unified storage list operation. We don't have a common test // directory for now, so we (search and storage) keep it here as we own this part of the tests. @@ -216,40 +244,6 @@ func TestIntegrationFoldersApp(t *testing.T) { }) } }) - - t.Run("when creating a folder it should trim leading and trailing spaces", func(t *testing.T) { - doCreateEnsureTitleIsTrimmedTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, - }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagNestedFolders, - }, - })) - }) - - t.Run("with dual write (unified storage, mode 1, create circular reference folder)", func(t *testing.T) { - doCreateCircularReferenceFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, - }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagNestedFolders, - }, - })) - }) } func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper { @@ -275,20 +269,25 @@ func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper require.NotNil(t, legacyCreate.Result) uid := legacyCreate.Result.UID require.NotEmpty(t, uid) + //nolint:staticcheck + id := legacyCreate.Result.ID + require.NotEmpty(t, id) + idStr := fmt.Sprintf("%d", id) expectedResult := `{ "apiVersion": "folder.grafana.app/v1beta1", "kind": "Folder", "metadata": { "creationTimestamp": "${creationTimestamp}", - "labels": {"grafana.app/deprecatedInternalID":"1"}, + "labels": {"grafana.app/deprecatedInternalID":"` + idStr + `"}, "name": "` + uid + `", "namespace": "default", "resourceVersion": "${resourceVersion}", "uid": "${uid}" }, "spec": { - "title": "Test" + "title": "Test", + "description": "" }, "status": {} }` @@ -591,6 +590,9 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}" folderWithParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" @@ -715,6 +717,9 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } type testCase struct { description string @@ -861,10 +866,13 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { } // TestFoldersCreateAPIEndpointK8S is the counterpart of pkg/api/folder_test.go TestFoldersCreateAPIEndpoint -func TestFoldersCreateAPIEndpointK8S(t *testing.T) { +func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}" folderWithTitleEmpty := "{ \"title\": \"\"}" @@ -902,7 +910,7 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) { description: "folder creation fails without permissions to create a folder", input: folderWithoutParentInput, expectedCode: http.StatusForbidden, - expectedMessage: dashboards.ErrFolderAccessDenied.Error(), + expectedMessage: fmt.Sprintf("You'll need additional permissions to perform this action. Permissions needed: %s", "folders:create"), permissions: []resourcepermissions.SetResourcePermissionCommand{}, }, { @@ -1022,10 +1030,13 @@ func testDescription(description string, expectedErr error) string { } // There are no counterpart of TestFoldersGetAPIEndpointK8S in pkg/api/folder_test.go -func TestFoldersGetAPIEndpointK8S(t *testing.T) { +func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } type testCase struct { description string @@ -1062,6 +1073,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { expectedOutput: []dtos.FolderSearchHit{ {UID: "foo", Title: "Folder 1"}, {UID: "qux", Title: "Folder 3"}, + {UID: folder.SharedWithMeFolder.UID, Title: folder.SharedWithMeFolder.Title}, }, permissions: folderReadAndCreatePermission, }, @@ -1107,7 +1119,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { } // test on all dualwriter modes - for mode := 1; mode <= 4; mode++ { + for mode := 0; mode <= 4; mode++ { for _, tc := range tcs { t.Run(fmt.Sprintf("Mode: %d, %s", mode, tc.description), func(t *testing.T) { modeDw := grafanarest.DualWriterMode(mode) @@ -1123,6 +1135,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { }, EnableFeatureToggles: []string{ featuremgmt.FlagNestedFolders, + featuremgmt.FlagUnifiedStorageSearch, featuremgmt.FlagKubernetesClientDashboardsFolders, }, }) diff --git a/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json index 77879dc0fc4..03fbbfc37c3 100644 --- a/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json @@ -2303,6 +2303,7 @@ ] }, "spec": { + "description": "Spec is the spec of the Check", "default": {}, "allOf": [ { @@ -2389,6 +2390,33 @@ } ] }, + "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReport": { + "type": "object", + "required": [ + "count", + "failures" + ], + "properties": { + "count": { + "description": "Number of elements analyzed", + "type": "integer", + "format": "int64", + "default": 0 + }, + "failures": { + "description": "List of failures", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReportFailure" + } + ] + } + } + } + }, "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReportFailure": { "type": "object", "required": [ @@ -2479,7 +2507,7 @@ "default": {}, "allOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckV0alpha1StatusReport" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReport" } ] } @@ -2510,6 +2538,7 @@ ] }, "spec": { + "description": "Spec is the spec of the CheckType", "default": {}, "allOf": [ { @@ -2682,33 +2711,6 @@ } } }, - "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckV0alpha1StatusReport": { - "type": "object", - "required": [ - "count", - "failures" - ], - "properties": { - "count": { - "description": "Number of elements analyzed", - "type": "integer", - "format": "int64", - "default": 0 - }, - "failures": { - "description": "List of failures", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReportFailure" - } - ] - } - } - } - }, "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckstatusOperatorState": { "type": "object", "required": [ diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index f59c4667769..a5262d3e078 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -1276,7 +1276,7 @@ "default": "" }, "legacyOptions": { - "description": "Catch-all field for datasource-specific properties. Should not be available in as code tooling.", + "description": "Catch-all field for datasource-specific properties", "type": "object", "additionalProperties": { "type": "object" diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index b1b2dc77570..6c3fae072bb 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -2923,11 +2923,16 @@ "type": "object", "required": [ "title", + "description", "version", "group", "permissions" ], "properties": { + "description": { + "type": "string", + "default": "" + }, "group": { "type": "string", "default": "" @@ -3236,11 +3241,16 @@ "type": "object", "required": [ "title", + "description", "version", "group", "permissions" ], "properties": { + "description": { + "type": "string", + "default": "" + }, "group": { "type": "string", "default": "" @@ -3723,11 +3733,16 @@ "type": "object", "required": [ "title", + "description", "version", "group", "permissions" ], "properties": { + "description": { + "type": "string", + "default": "" + }, "group": { "type": "string", "default": "" diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index bf63e2358fc..24ffe18b0e2 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -2,10 +2,7 @@ package provisioning import ( "context" - "crypto/sha256" - "encoding/hex" "encoding/json" - "net/http" "os" "path" "strings" @@ -13,7 +10,6 @@ import ( "text/template" "time" - gh "github.com/google/go-github/v70/github" ghmock "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -224,20 +220,8 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper } helper := apis.NewK8sTestHelper(t, opts) - helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient( - ghmock.WithRequestMatchHandler(ghmock.GetUser, ghAlwaysWrite(t, &gh.User{})), - ghmock.WithRequestMatchHandler(ghmock.GetReposHooksByOwnerByRepo, ghAlwaysWrite(t, []*gh.Hook{})), - ghmock.WithRequestMatchHandler(ghmock.PostReposHooksByOwnerByRepo, ghAlwaysWrite(t, &gh.Hook{})), - ghmock.WithRequestMatchHandler(ghmock.GetReposByOwnerByRepo, ghAlwaysWrite(t, &gh.Repository{})), - ghmock.WithRequestMatchHandler(ghmock.GetReposBranchesByOwnerByRepoByBranch, ghAlwaysWrite(t, &gh.Branch{})), - ghmock.WithRequestMatchHandler(ghmock.GetReposGitTreesByOwnerByRepoByTreeSha, ghAlwaysWrite(t, &gh.Tree{})), - ghmock.WithRequestMatchHandler( - ghmock.DeleteReposHooksByOwnerByRepoByHookId, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - }), - ), - ) + // FIXME: keeping this line here to keep the dependency around until we have tests which use this again. + helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient() repositories := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, @@ -310,33 +294,6 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper } } -func ghAlwaysWrite(t *testing.T, body any) http.HandlerFunc { - marshalled := ghmock.MustMarshal(body) - return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, err := w.Write(marshalled) - require.NoError(t, err, "failed to write body in mock") - }) -} - -func ghHandleTree(t *testing.T, refs map[string][]*gh.TreeEntry) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sha := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] - require.NotEmpty(t, sha, "sha path parameter was missing?") - - entries := refs[sha] - require.NotNil(t, entries, "no entries for sha %s", sha) - - tree := &gh.Tree{ - SHA: gh.Ptr(sha), - Truncated: gh.Ptr(false), - Entries: entries, - } - - _, err := w.Write(ghmock.MustMarshal(tree)) - require.NoError(t, err, "failed to write body in mock") - }) -} - func mustNestedString(obj map[string]interface{}, fields ...string) string { v, _, err := unstructured.NestedString(obj, fields...) if err != nil { @@ -350,15 +307,6 @@ func asJSON(obj any) []byte { return jj } -func treeEntryDir(dirName string, sha string) *gh.TreeEntry { - return &gh.TreeEntry{ - SHA: gh.Ptr(sha), - Path: gh.Ptr(dirName), - Type: gh.Ptr("tree"), - Mode: gh.Ptr("040000"), - } -} - func unstructuredToRepository(t *testing.T, obj *unstructured.Unstructured) *provisioning.Repository { bytes, err := obj.MarshalJSON() require.NoError(t, err) @@ -369,33 +317,3 @@ func unstructuredToRepository(t *testing.T, obj *unstructured.Unstructured) *pro return repo } - -func treeEntry(fpath string, content []byte) *gh.TreeEntry { - sha := sha256.Sum256(content) - - return &gh.TreeEntry{ - SHA: gh.Ptr(hex.EncodeToString(sha[:])), - Path: gh.Ptr(fpath), - Size: gh.Ptr(len(content)), - Type: gh.Ptr("blob"), - Mode: gh.Ptr("100644"), - Content: gh.Ptr(string(content)), - } -} - -func repoContent(fpath string, content []byte) *gh.RepositoryContent { - sha := sha256.Sum256(content) - typ := "blob" - if strings.HasSuffix(fpath, "/") { - typ = "tree" - } - - return &gh.RepositoryContent{ - SHA: gh.Ptr(hex.EncodeToString(sha[:])), - Name: gh.Ptr(path.Base(fpath)), - Path: &fpath, - Size: gh.Ptr(len(content)), - Type: &typ, - Content: gh.Ptr(string(content)), - } -} diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index f9a4be946fc..27bc459e179 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -3,16 +3,14 @@ package provisioning import ( "context" "encoding/json" + "fmt" "net/http" "os" "path/filepath" - "regexp" "strings" "testing" "time" - gh "github.com/google/go-github/v70/github" - ghmock "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -173,7 +171,7 @@ func TestIntegrationProvisioning_FailInvalidSchema(t *testing.T) { }, time.Second*10, time.Millisecond*10, "Expected to be able to start a sync job") require.EventuallyWithT(t, func(collect *assert.CollectT) { - //helper.TriggerJobProcessing(t) + // helper.TriggerJobProcessing(t) result, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "jobs", string(jobObj.GetUID())) @@ -211,50 +209,25 @@ func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) { helper := runGrafana(t) ctx := context.Background() - helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient( - ghmock.WithRequestMatchHandler(ghmock.GetUser, ghAlwaysWrite(t, &gh.User{Name: gh.Ptr("github-user")})), - ghmock.WithRequestMatchHandler(ghmock.GetReposHooksByOwnerByRepo, ghAlwaysWrite(t, []*gh.Hook{})), - ghmock.WithRequestMatchHandler(ghmock.PostReposHooksByOwnerByRepo, ghAlwaysWrite(t, &gh.Hook{ID: gh.Ptr(int64(123))})), - ghmock.WithRequestMatchHandler(ghmock.GetReposByOwnerByRepo, ghAlwaysWrite(t, &gh.Repository{ID: gh.Ptr(int64(234))})), - ghmock.WithRequestMatchHandler( - ghmock.GetReposBranchesByOwnerByRepoByBranch, - ghAlwaysWrite(t, &gh.Branch{ - Name: gh.Ptr("main"), - Commit: &gh.RepositoryCommit{SHA: gh.Ptr("deadbeef")}, - }), - ), - ghmock.WithRequestMatchHandler(ghmock.GetReposGitTreesByOwnerByRepoByTreeSha, - ghHandleTree(t, map[string][]*gh.TreeEntry{ - "deadbeef": { - treeEntryDir("grafana", "subtree"), - }, - "subtree": { - treeEntry("dashboard.json", helper.LoadFile("testdata/all-panels.json")), - treeEntryDir("subdir", "subtree2"), - treeEntry("subdir/dashboard2.yaml", helper.LoadFile("testdata/text-options.json")), - }, - })), - ghmock.WithRequestMatchHandler( - ghmock.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - pathRegex := regexp.MustCompile(`/repos/[^/]+/[^/]+/contents/(.*)`) - matches := pathRegex.FindStringSubmatch(r.URL.Path) - require.NotNil(t, matches, "no match for contents?") - path := matches[1] + // FIXME: instead of using an existing GitHub repository, we should create a new one for the tests and a branch + // This was the previous structure + // ghmock.WithRequestMatchHandler(ghmock.GetReposGitTreesByOwnerByRepoByTreeSha, + // ghHandleTree(t, map[string][]*gh.TreeEntry{ + // "deadbeef": { + // treeEntryDir("grafana", "subtree"), + // }, + // "subtree": { + // treeEntry("dashboard.json", helper.LoadFile("testdata/all-panels.json")), + // treeEntryDir("subdir", "subtree2"), + // treeEntry("subdir/dashboard2.yaml", helper.LoadFile("testdata/text-options.json")), + // }, + // })), - var err error - switch path { - case "grafana/dashboard.json": - _, err = w.Write(ghmock.MustMarshal(repoContent(path, helper.LoadFile("testdata/all-panels.json")))) - case "grafana/subdir/dashboard2.yaml": - _, err = w.Write(ghmock.MustMarshal(repoContent(path, helper.LoadFile("testdata/text-options.json")))) - default: - t.Fatalf("got unexpected path: %s", path) - } - require.NoError(t, err) - }), - ), - ) + // FIXME: uncomment these to implement webhook integration tests. + // helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient( + // ghmock.WithRequestMatchHandler(ghmock.GetReposHooksByOwnerByRepo, ghAlwaysWrite(t, []*gh.Hook{})), + // ghmock.WithRequestMatchHandler(ghmock.PostReposHooksByOwnerByRepo, ghAlwaysWrite(t, &gh.Hook{ID: gh.Ptr(int64(123))})), + // ) const repo = "github-create-test" _, err := helper.Repositories.Resource.Create(ctx, @@ -279,8 +252,10 @@ func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) { for _, v := range found.Items { names = append(names, v.GetName()) } - assert.Contains(t, names, "n1jR8vnnz", "should contain dashboard.json's contents") - assert.Contains(t, names, "WZ7AhQiVz", "should contain dashboard2.yaml's contents") + require.Len(t, names, 3, "should have three dashboards") + assert.Contains(t, names, "adg5vbj", "should contain dashboard.json's contents") + assert.Contains(t, names, "admfz74", "should contain dashboard2.yaml's contents") + assert.Contains(t, names, "adn5mxb", "should contain dashboard2.yaml's contents") err = helper.Repositories.Resource.Delete(ctx, repo, metav1.DeleteOptions{}) require.NoError(t, err, "should delete values") @@ -651,3 +626,128 @@ func TestProvisioning_ExportUnifiedToRepository(t *testing.T) { require.Nil(t, obj["status"], "should not have a status element") } } + +func TestIntegrationProvisioning_DeleteResources(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "delete-test-repo" + localTmp := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": repo, + "SyncEnabled": true, + "SyncTarget": "instance", + }) + _, err := helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{}) + require.NoError(t, err) + + // create the structure: + // dashboard1.json + // folder/ + // dashboard2.json + // nested/ + // dashboard3.json + dashboard1 := helper.LoadFile("testdata/all-panels.json") + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "dashboard1.json"). + Body(dashboard1). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error()) + dashboard2 := helper.LoadFile("testdata/text-options.json") + result = helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "folder", "dashboard2.json"). + Body(dashboard2). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error()) + dashboard3 := helper.LoadFile("testdata/timeline-demo.json") + result = helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "folder", "nested", "dashboard3.json"). + Body(dashboard3). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error()) + + // make sure we don't fail when there is a .keep file in a folder + helper.CopyToProvisioningPath(t, "testdata/.keep", "folder/nested/.keep") + + dashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 3, len(dashboards.Items)) + + folders, err := helper.Folders.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 2, len(folders.Items)) + + t.Run("delete individual dashboard file, should delete from repo and grafana", func(t *testing.T) { + result := helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "dashboard1.json"). + Do(ctx) + require.NoError(t, result.Error()) + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "dashboard1.json") + require.Error(t, err) + dashboards, err = helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 2, len(dashboards.Items)) + }) + + t.Run("delete folder, should delete from repo and grafana all nested resources too", func(t *testing.T) { + // need to delete directly through the url, because the k8s client doesn't support `/` in a subresource + // but that is needed by gitsync to know that it is a folder + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr().String() + url := fmt.Sprintf("http://admin:admin@%s/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/%s/files/folder/", addr, repo) + req, err := http.NewRequest(http.MethodDelete, url, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + // nolint:errcheck + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + // should be deleted from the repo + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder") + require.Error(t, err) + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "dashboard2.json") + require.Error(t, err) + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "nested") + require.Error(t, err) + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "nested", "dashboard3.json") + require.Error(t, err) + + // all should be deleted from grafana + for _, d := range dashboards.Items { + _, err = helper.DashboardsV1.Resource.Get(ctx, d.GetName(), metav1.GetOptions{}) + require.Error(t, err) + } + for _, f := range folders.Items { + _, err = helper.Folders.Resource.Get(ctx, f.GetName(), metav1.GetOptions{}) + require.Error(t, err) + } + }) + + t.Run("deleting a non-existent file should fail", func(t *testing.T) { + result := helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "non-existent.json"). + Do(ctx) + require.Error(t, result.Error()) + }) +} diff --git a/pkg/tests/apis/provisioning/testdata/.keep b/pkg/tests/apis/provisioning/testdata/.keep new file mode 100644 index 00000000000..633f69fd54e --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/.keep @@ -0,0 +1 @@ +# This file ensures the folder/nested directory is tracked in version control \ No newline at end of file diff --git a/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl b/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl index 220b0936b0e..86489c7d65c 100644 --- a/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl +++ b/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl @@ -9,10 +9,10 @@ "description": "{{ or .Description .Name "Load grafana dashboard from fake repository" }}", "type": "github", "github": { - "url": "{{ or .URL "https://github.com/grafana/git-ui-sync-demo" }}", - "branch": "{{ or .Branch "dummy" }}", + "url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}", + "branch": "{{ or .Branch "integration-test" }}", "generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }}, - "token": "{{ or .Token "github_pat_dummy" }}", + "token": "{{ or .Token "" }}", "path": "{{ or .Path "grafana/" }}" }, "sync": { diff --git a/pkg/tests/apis/provisioning/testdata/timeline-demo.json b/pkg/tests/apis/provisioning/testdata/timeline-demo.json new file mode 120000 index 00000000000..af7d0ce7aac --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/timeline-demo.json @@ -0,0 +1 @@ +../../../../../devenv/dev-dashboards/panel-timeline/timeline-demo.json \ No newline at end of file diff --git a/pkg/tests/utils.go b/pkg/tests/utils.go index f81683c80f4..4ea428df9f7 100644 --- a/pkg/tests/utils.go +++ b/pkg/tests/utils.go @@ -5,10 +5,13 @@ import ( "crypto/tls" "net/url" "os" + "strings" "testing" "github.com/go-openapi/strfmt" goapi "github.com/grafana/grafana-openapi-client-go/client" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" @@ -19,9 +22,18 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) +func SkipIntegrationTestInShortMode(t testing.TB) { + t.Helper() + if !strings.HasPrefix(t.Name(), "TestIntegration") { + t.Fatal("test is not an integration test") + } + if testing.Short() { + t.Skip("skipping integration test in short mode") + } +} + func CreateUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCommand) int64 { t.Helper() diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 28567e4e5d5..919b853887e 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "slices" "time" @@ -73,6 +74,7 @@ func (ds *DataSource) newAWSConfig(ctx context.Context, region string) (aws.Conf Region: region, AccessKey: ds.Settings.AccessKey, SecretKey: ds.Settings.SecretKey, + HTTPClient: &http.Client{}, } if ds.Settings.GrafanaSettings.SecureSocksDSProxyEnabled && ds.Settings.SecureSocksProxyEnabled { authSettings.ProxyOptions = ds.ProxyOpts diff --git a/pkg/util/testutil/mocks/T.go b/pkg/util/testutil/mocks/T.go index 2e2c110fb82..d85781703b0 100644 --- a/pkg/util/testutil/mocks/T.go +++ b/pkg/util/testutil/mocks/T.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -50,11 +50,11 @@ func (_c *T_Cleanup_Call) Return() *T_Cleanup_Call { } func (_c *T_Cleanup_Call) RunAndReturn(run func(func())) *T_Cleanup_Call { - _c.Call.Return(run) + _c.Run(run) return _c } -// Deadline provides a mock function with given fields: +// Deadline provides a mock function with no fields func (_m *T) Deadline() (time.Time, bool) { ret := _m.Called() @@ -149,11 +149,11 @@ func (_c *T_Errorf_Call) Return() *T_Errorf_Call { } func (_c *T_Errorf_Call) RunAndReturn(run func(string, ...interface{})) *T_Errorf_Call { - _c.Call.Return(run) + _c.Run(run) return _c } -// FailNow provides a mock function with given fields: +// FailNow provides a mock function with no fields func (_m *T) FailNow() { _m.Called() } @@ -181,11 +181,11 @@ func (_c *T_FailNow_Call) Return() *T_FailNow_Call { } func (_c *T_FailNow_Call) RunAndReturn(run func()) *T_FailNow_Call { - _c.Call.Return(run) + _c.Run(run) return _c } -// Helper provides a mock function with given fields: +// Helper provides a mock function with no fields func (_m *T) Helper() { _m.Called() } @@ -213,7 +213,7 @@ func (_c *T_Helper_Call) Return() *T_Helper_Call { } func (_c *T_Helper_Call) RunAndReturn(run func()) *T_Helper_Call { - _c.Call.Return(run) + _c.Run(run) return _c } diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index d431451f492..6d5885e3bdd 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5764,6 +5764,8 @@ "errorCode": { "type": "string", "enum": [ + "ALERT_RULES_QUOTA_REACHED", + "ALERT_RULES_GROUP_QUOTA_REACHED", "DATASOURCE_NAME_CONFLICT", "DATASOURCE_INVALID_URL", "DATASOURCE_ALREADY_MANAGED", diff --git a/public/api-merged.json b/public/api-merged.json index 0706252d551..0778d9d6d3d 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17319,6 +17319,8 @@ "errorCode": { "type": "string", "enum": [ + "ALERT_RULES_QUOTA_REACHED", + "ALERT_RULES_GROUP_QUOTA_REACHED", "DATASOURCE_NAME_CONFLICT", "DATASOURCE_INVALID_URL", "DATASOURCE_ALREADY_MANAGED", diff --git a/public/app/api/utils.ts b/public/app/api/utils.ts index 0815c34d288..1ad0ff690e5 100644 --- a/public/app/api/utils.ts +++ b/public/app/api/utils.ts @@ -1,8 +1,8 @@ import { config, isFetchError } from '@grafana/runtime'; +import { ThunkDispatch } from 'app/types/store'; import { notifyApp } from '../core/actions'; import { createErrorNotification } from '../core/copy/appNotification'; -import { ThunkDispatch } from '../types'; export const getAPINamespace = () => config.namespace; diff --git a/public/app/core/actions/cleanUp.ts b/public/app/core/actions/cleanUp.ts index 4322612bda0..854b10ec5c3 100644 --- a/public/app/core/actions/cleanUp.ts +++ b/public/app/core/actions/cleanUp.ts @@ -1,6 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { StoreState } from '../../types'; +import { StoreState } from 'app/types/store'; export type CleanUpAction = (state: StoreState) => void; diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index c7569ff09f6..f49988dcc1a 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -1,12 +1,12 @@ import { useEffect, useMemo, useState } from 'react'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, Select, Stack } from '@grafana/ui'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { ServiceAccountPicker } from 'app/core/components/Select/ServiceAccountPicker'; import { TeamPicker } from 'app/core/components/Select/TeamPicker'; import { UserPicker } from 'app/core/components/Select/UserPicker'; -import { OrgRole } from 'app/types/acl'; import { Assignments, PermissionTarget, SetPermission } from './types'; diff --git a/public/app/core/components/AccessControl/types.ts b/public/app/core/components/AccessControl/types.ts index c061bc77138..dcc69b58df4 100644 --- a/public/app/core/components/AccessControl/types.ts +++ b/public/app/core/components/AccessControl/types.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export type ResourcePermission = { id: number; diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index 8c91066066b..fc6c062b782 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -7,7 +7,7 @@ import { config, locationService, reportInteraction } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import store from 'app/core/store'; import { isShallowEqual } from 'app/core/utils/isShallowEqual'; -import { KioskMode } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { RouteDescriptor } from '../../navigation/types'; import { buildBreadcrumbs } from '../Breadcrumbs/utils'; diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx index 2b468d9f4c2..45b2c662cc7 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx @@ -229,4 +229,41 @@ describe('ExtensionToolbarItem', () => { expect(screen.getByTestId('is-open')).toHaveTextContent('false'); }); + + it('should render individual buttons when multiple plugins are available', async () => { + const plugin1Meta = { + pluginId: 'grafana-investigations-app', + addedComponents: [{ ...mockComponent, title: 'Investigations' }], + }; + + const plugin2Meta = { + pluginId: 'grafana-assistant-app', + addedComponents: [{ ...mockComponent, title: 'Assistant' }], + }; + + (usePluginLinks as jest.Mock).mockReturnValue({ + links: [ + { pluginId: plugin1Meta.pluginId, title: plugin1Meta.addedComponents[0].title }, + { pluginId: plugin2Meta.pluginId, title: plugin2Meta.addedComponents[0].title }, + ], + isLoading: false, + }); + + (getExtensionPointPluginMeta as jest.Mock).mockReturnValue( + new Map([ + [plugin1Meta.pluginId, plugin1Meta], + [plugin2Meta.pluginId, plugin2Meta], + ]) + ); + + setup(); + + // Should render two separate buttons, not a dropdown + const buttons = screen.getAllByTestId(/extension-toolbar-button-open/); + expect(buttons).toHaveLength(2); + + // Each button should have the correct title + expect(buttons[0]).toHaveAttribute('aria-label', 'Open Investigations'); + expect(buttons[1]).toHaveAttribute('aria-label', 'Open Assistant'); + }); }); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx index 02cad31ea91..50fe56630af 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx @@ -1,3 +1,4 @@ +import { ExtensionInfo } from '@grafana/data'; import { Dropdown, Menu } from '@grafana/ui'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; @@ -9,91 +10,75 @@ import { } from './ExtensionSidebarProvider'; import { ExtensionToolbarItemButton } from './ExtensionToolbarItemButton'; -export function ExtensionToolbarItem() { - const { availableComponents, dockedComponentId, setDockedComponentId, isOpen, isEnabled } = - useExtensionSidebarContext(); +type ComponentWithPluginId = ExtensionInfo & { pluginId: string }; - let dockedComponentTitle = ''; - let dockedPluginId = ''; - if (dockedComponentId) { - const dockedComponent = getComponentMetaFromComponentId(dockedComponentId); - if (dockedComponent) { - dockedComponentTitle = dockedComponent.componentTitle; - dockedPluginId = dockedComponent.pluginId; - } - } +export function ExtensionToolbarItem() { + const { availableComponents, dockedComponentId, setDockedComponentId, isEnabled } = useExtensionSidebarContext(); if (!isEnabled || availableComponents.size === 0) { return null; } - // get a flat list of all components with their pluginId - const components = Array.from(availableComponents.entries()).flatMap(([pluginId, { addedComponents }]) => - addedComponents.map((c) => ({ ...c, pluginId })) - ); + const dockedMeta = dockedComponentId ? getComponentMetaFromComponentId(dockedComponentId) : null; - if (components.length === 0) { - return null; - } + const renderPluginButton = (pluginId: string, components: ComponentWithPluginId[]) => { + if (components.length === 1) { + const component = components[0]; + const componentId = getComponentIdFromComponentMeta(pluginId, component); + const isActive = dockedComponentId === componentId; - if (components.length === 1) { - return ( - <> + return ( { - if (isOpen) { - setDockedComponentId(undefined); - } else { - setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0])); - } - }} - pluginId={components[0].pluginId} + key={pluginId} + isOpen={isActive} + title={component.title} + onClick={() => setDockedComponentId(isActive ? undefined : componentId)} + pluginId={pluginId} /> - - + ); + } + + const isPluginActive = dockedMeta?.pluginId === pluginId; + const MenuItems = ( + + {components.map((c) => { + const id = getComponentIdFromComponentMeta(pluginId, c); + return ( + setDockedComponentId(dockedComponentId === id ? undefined : id)} + /> + ); + })} + ); - } + + return isPluginActive ? ( + setDockedComponentId(undefined)} + pluginId={pluginId} + /> + ) : ( + + + + ); + }; - const MenuItems = ( - - {components.map((c) => { - const id = getComponentIdFromComponentMeta(c.pluginId, c); - return ( - { - if (isOpen && dockedComponentId === id) { - setDockedComponentId(undefined); - } else { - setDockedComponentId(id); - } - }} - /> - ); - })} - - ); return ( <> - {isOpen ? ( - { - if (isOpen) { - setDockedComponentId(undefined); - } - }} - pluginId={dockedPluginId} - /> - ) : ( - - - + {/* renders a single `ExtensionToolbarItemButton` for each plugin; if a plugin has multiple components, it renders them inside a `Dropdown` */} + {Array.from(availableComponents.entries()).map( + ([pluginId, { addedComponents }]: [string, { addedComponents: ExtensionInfo[] }]) => + renderPluginButton( + pluginId, + addedComponents.map((c: ExtensionInfo) => ({ ...c, pluginId })) + ) )} diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx index 44e33526095..3dcc53730a7 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx @@ -16,6 +16,8 @@ function getPluginIcon(pluginId?: string): string { switch (pluginId) { case 'grafana-grafanadocsplugin-app': return 'book'; + case 'grafana-investigations-app': + return 'eye'; default: return 'ai-sparkle'; } diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 9f85383b4d8..1c8b3959230 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -11,7 +11,7 @@ import { ScrollContainer, useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { setBookmark } from 'app/core/reducers/navBarTree'; import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { InviteUserButton } from '../../InviteUserButton/InviteUserButton'; import { shouldRenderInviteUserButton } from '../../InviteUserButton/utils'; diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx index 2a33cf03123..e2e1d35a962 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx @@ -5,7 +5,7 @@ import { SelectableValue, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Icon, Select, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; -import { UserOrg } from 'app/types'; +import { UserOrg } from 'app/types/user'; import { OrganizationBaseProps } from './types'; diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.test.tsx b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.test.tsx index d7897786e3e..79b9ab19f02 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.test.tsx +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.test.tsx @@ -4,7 +4,7 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { OrgRole } from '@grafana/data'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; import { getUserOrganizations } from 'app/features/org/state/actions'; -import * as appTypes from 'app/types'; +import { StoreState } from 'app/types/store'; import { OrganizationSwitcher } from './OrganizationSwitcher'; @@ -14,12 +14,12 @@ jest.mock('app/features/org/state/actions', () => ({ setUserOrganization: jest.fn(), })); -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => jest.fn(), })); -const renderWithProvider = ({ initialState }: { initialState?: Partial }) => { +const renderWithProvider = ({ initialState }: { initialState?: Partial }) => { render( diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx index 4138e69733b..67ad47cded0 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -5,7 +5,8 @@ import { locationService } from '@grafana/runtime'; import { Space, Text } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { getUserOrganizations, setUserOrganization } from 'app/features/org/state/actions'; -import { useDispatch, useSelector, UserOrg } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; +import { UserOrg } from 'app/types/user'; import { Branding } from '../../Branding/Branding'; diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/types.ts b/public/app/core/components/AppChrome/OrganizationSwitcher/types.ts index 3b2afef009b..0687bf2ca6c 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/types.ts +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/types.ts @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { UserOrg } from 'app/types'; +import { UserOrg } from 'app/types/user'; export interface OrganizationBaseProps { orgs: UserOrg[]; diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx index a528a0c8341..0a9a48ac6ed 100644 --- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx +++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx @@ -4,7 +4,7 @@ import { t } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { Menu, Dropdown, ToolbarButton } from '@grafana/ui'; import { getExternalUserMngLinkUrl } from 'app/features/users/utils'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { performInviteUserClick, shouldRenderInviteUserButton } from '../../InviteUserButton/utils'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index 0f26fdeeba1..6766d757b49 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -13,7 +13,7 @@ import { useGrafana } from 'app/core/context/GrafanaContext'; import { contextSrv } from 'app/core/core'; import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth'; import { HOME_NAV_ID } from 'app/core/reducers/navModel'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { Branding } from '../../Branding/Branding'; import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs'; diff --git a/public/app/core/components/AppNotifications/AppNotificationItem.tsx b/public/app/core/components/AppNotifications/AppNotificationItem.tsx index 2d3ad8ea0f7..13575340b6a 100644 --- a/public/app/core/components/AppNotifications/AppNotificationItem.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationItem.tsx @@ -4,7 +4,7 @@ import { useEffectOnce } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { Alert, useStyles2 } from '@grafana/ui'; -import { AppNotification, timeoutMap } from 'app/types'; +import { AppNotification, timeoutMap } from 'app/types/appNotifications'; interface Props { appNotification: AppNotification; diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx index edb686f0d55..bb473dd0905 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -6,7 +6,7 @@ import { useStyles2, Stack } from '@grafana/ui'; import { notifyApp, hideAppNotification } from 'app/core/actions'; import appEvents from 'app/core/app_events'; import { selectVisible } from 'app/core/reducers/appNotification'; -import { useSelector, useDispatch } from 'app/types'; +import { useSelector, useDispatch } from 'app/types/store'; import { createErrorNotification, diff --git a/public/app/core/components/FolderFilter/FolderFilter.tsx b/public/app/core/components/FolderFilter/FolderFilter.tsx index b427eed4569..2d310a6609b 100644 --- a/public/app/core/components/FolderFilter/FolderFilter.tsx +++ b/public/app/core/components/FolderFilter/FolderFilter.tsx @@ -9,7 +9,8 @@ import { config } from 'app/core/config'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { FolderInfo, PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; +import { FolderInfo } from 'app/types/folders'; export interface FolderFilterProps { onChange: (folder: FolderInfo[]) => void; diff --git a/public/app/core/components/InviteUserButton/utils.ts b/public/app/core/components/InviteUserButton/utils.ts index 708e5ff88d5..6fb774a059b 100644 --- a/public/app/core/components/InviteUserButton/utils.ts +++ b/public/app/core/components/InviteUserButton/utils.ts @@ -2,7 +2,7 @@ import { reportInteraction } from '@grafana/runtime'; import { config } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import { getExternalUserMngLinkUrl } from 'app/features/users/utils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export const shouldRenderInviteUserButton = config.featureToggles.inviteUserExperimental && diff --git a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx index 02f2f108e30..e149f84c75d 100644 --- a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx +++ b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx @@ -2,7 +2,7 @@ import { t } from '@grafana/i18n'; import { Badge } from '@grafana/ui'; import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { NestedFolderDTO } from 'app/features/search/service/types'; -import { FolderDTO, FolderListItemDTO } from 'app/types'; +import { FolderDTO, FolderListItemDTO } from 'app/types/folders'; export interface Props { folder?: FolderListItemDTO | NestedFolderDTO | FolderDTO; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx index 4a322b6ee27..46ef137c53f 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx @@ -12,7 +12,7 @@ import { Indent } from 'app/core/components/Indent/Indent'; import { childrenByParentUIDSelector, rootItemsSelector } from 'app/features/browse-dashboards/state/hooks'; import { DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { DashboardViewItem } from 'app/features/search/types'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { FolderRepo } from './FolderRepo'; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 28305640a2a..38d241c61e2 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -15,7 +15,7 @@ import { getGrafanaSearcher } from 'app/features/search/service/searcher'; import { QueryResponse } from 'app/features/search/service/types'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; -import { PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; import { FolderRepo } from './FolderRepo'; import { getDOMId, NestedFolderList } from './NestedFolderList'; diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts index bd3b6ba9098..6fe4c0ef440 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts @@ -1,6 +1,5 @@ import { config } from '@grafana/runtime'; - -import { PermissionLevelString } from '../../../types'; +import { PermissionLevelString } from 'app/types/acl'; import { useFoldersQueryAppPlatform } from './useFoldersQueryAppPlatform'; import { useFoldersQueryLegacy } from './useFoldersQueryLegacy'; diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts b/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts index 6b1286895bd..8381c61d1f2 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts @@ -7,7 +7,8 @@ import { ListFolderQueryArgs, browseDashboardsAPI } from 'app/features/browse-da import { PAGE_SIZE } from 'app/features/browse-dashboards/api/services'; import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils'; import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types'; -import { FolderListItemDTO, PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; +import { FolderListItemDTO } from 'app/types/folders'; import { useDispatch, useSelector } from 'app/types/store'; import { ROOT_FOLDER_ITEM } from './utils'; diff --git a/public/app/core/components/Page/usePageNav.ts b/public/app/core/components/Page/usePageNav.ts index da6bd0da218..66899e442da 100644 --- a/public/app/core/components/Page/usePageNav.ts +++ b/public/app/core/components/Page/usePageNav.ts @@ -3,7 +3,7 @@ import { createSelector } from 'reselect'; import { NavModel } from '@grafana/data'; import { getNavModel } from 'app/core/selectors/navModel'; import { store } from 'app/store/store'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; export function usePageNav(navId?: string, oldProp?: NavModel): NavModel | undefined { if (oldProp) { diff --git a/public/app/core/components/Page/usePageTitle.ts b/public/app/core/components/Page/usePageTitle.ts index c2e53b23251..4c338a77ef0 100644 --- a/public/app/core/components/Page/usePageTitle.ts +++ b/public/app/core/components/Page/usePageTitle.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { NavModel, NavModelItem } from '@grafana/data'; import { HOME_NAV_ID } from 'app/core/reducers/navModel'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { Branding } from '../Branding/Branding'; import { buildBreadcrumbs } from '../Breadcrumbs/utils'; diff --git a/public/app/core/components/RolePicker/BuiltinRoleSelector.tsx b/public/app/core/components/RolePicker/BuiltinRoleSelector.tsx index c0fa457d8c2..70e18b2a9c8 100644 --- a/public/app/core/components/RolePicker/BuiltinRoleSelector.tsx +++ b/public/app/core/components/RolePicker/BuiltinRoleSelector.tsx @@ -1,8 +1,7 @@ -import { SelectableValue } from '@grafana/data'; +import { OrgRole, SelectableValue } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { Icon, RadioButtonList, Tooltip, useStyles2, useTheme2, PopoverContent } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { OrgRole } from 'app/types'; import { getStyles } from './styles'; diff --git a/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx b/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx index 79e44be57e6..b4b3e30d738 100644 --- a/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx +++ b/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx @@ -1,7 +1,7 @@ import { forwardRef, useCallback, useState } from 'react'; import { useStyles2, getSelectStyles, useTheme2 } from '@grafana/ui'; -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { RoleMenuGroupOption } from './RoleMenuGroupOption'; import { RoleMenuOption } from './RoleMenuOption'; diff --git a/public/app/core/components/RolePicker/RoleMenuOption.tsx b/public/app/core/components/RolePicker/RoleMenuOption.tsx index 7700b00bdae..6e020ad6018 100644 --- a/public/app/core/components/RolePicker/RoleMenuOption.tsx +++ b/public/app/core/components/RolePicker/RoleMenuOption.tsx @@ -4,7 +4,7 @@ import { forwardRef, FormEvent } from 'react'; import { t } from '@grafana/i18n'; import { Checkbox, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/internal'; -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { getStyles } from './styles'; diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index 6cbf2e0450d..25cff0889a1 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -1,7 +1,8 @@ import { FormEvent, useCallback, useEffect, useState, useRef } from 'react'; +import { OrgRole } from '@grafana/data'; import { ClickOutsideWrapper, Portal, useTheme2 } from '@grafana/ui'; -import { Role, OrgRole } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { RolePickerInput } from './RolePickerInput'; import { RolePickerMenu } from './RolePickerMenu'; diff --git a/public/app/core/components/RolePicker/RolePickerInput.tsx b/public/app/core/components/RolePicker/RolePickerInput.tsx index 9acb2630745..9aaa4b447f5 100644 --- a/public/app/core/components/RolePicker/RolePickerInput.tsx +++ b/public/app/core/components/RolePicker/RolePickerInput.tsx @@ -6,8 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { useStyles2, getInputStyles, sharedInputStyle, Tooltip, Icon, Spinner } from '@grafana/ui'; import { getFocusStyles } from '@grafana/ui/internal'; - -import { Role } from '../../../types'; +import { Role } from 'app/types/accessControl'; import { ValueContainer } from './ValueContainer'; import { ROLE_PICKER_WIDTH } from './constants'; diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index 23cf52de471..81c38519ae6 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -1,10 +1,11 @@ import { css, cx } from '@emotion/css'; import { useEffect, useRef, useState } from 'react'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, ScrollContainer, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/internal'; -import { OrgRole, Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { BuiltinRoleSelector } from './BuiltinRoleSelector'; import { RoleMenuGroupsSection } from './RoleMenuGroupsSection'; diff --git a/public/app/core/components/RolePicker/RolePickerSubMenu.tsx b/public/app/core/components/RolePicker/RolePickerSubMenu.tsx index fdcd22224b9..3dff3fc88be 100644 --- a/public/app/core/components/RolePicker/RolePickerSubMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerSubMenu.tsx @@ -3,7 +3,7 @@ import { cx } from '@emotion/css'; import { Trans, t } from '@grafana/i18n'; import { Button, ScrollContainer, Stack, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/internal'; -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { RoleMenuOption } from './RoleMenuOption'; import { MENU_MAX_HEIGHT } from './constants'; diff --git a/public/app/core/components/RolePicker/TeamRolePicker.tsx b/public/app/core/components/RolePicker/TeamRolePicker.tsx index 83552236334..78e3b0f2c82 100644 --- a/public/app/core/components/RolePicker/TeamRolePicker.tsx +++ b/public/app/core/components/RolePicker/TeamRolePicker.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useAsyncFn } from 'react-use'; import { contextSrv } from 'app/core/core'; -import { Role, AccessControlAction } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; import { RolePicker } from './RolePicker'; import { fetchTeamRoles, updateTeamRoles } from './api'; diff --git a/public/app/core/components/RolePicker/UserRolePicker.tsx b/public/app/core/components/RolePicker/UserRolePicker.tsx index 5fa3c94286f..a4232f4b61a 100644 --- a/public/app/core/components/RolePicker/UserRolePicker.tsx +++ b/public/app/core/components/RolePicker/UserRolePicker.tsx @@ -1,8 +1,9 @@ import { useEffect } from 'react'; import { useAsyncFn } from 'react-use'; +import { OrgRole } from '@grafana/data'; import { contextSrv } from 'app/core/core'; -import { Role, OrgRole, AccessControlAction } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; import { RolePicker } from './RolePicker'; import { fetchUserRoles, updateUserRoles } from './api'; diff --git a/public/app/core/components/RolePicker/api.ts b/public/app/core/components/RolePicker/api.ts index 00e1926eea5..596463b4a9d 100644 --- a/public/app/core/components/RolePicker/api.ts +++ b/public/app/core/components/RolePicker/api.ts @@ -1,5 +1,5 @@ import { getBackendSrv, isFetchError } from '@grafana/runtime'; -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { addDisplayNameForFixedRole, addFilteredDisplayName } from './utils'; diff --git a/public/app/core/components/RolePicker/hooks.ts b/public/app/core/components/RolePicker/hooks.ts index 071f31eddaa..0b66a36c94f 100644 --- a/public/app/core/components/RolePicker/hooks.ts +++ b/public/app/core/components/RolePicker/hooks.ts @@ -4,7 +4,7 @@ import { useDeepCompareEffect } from 'react-use'; import useAsync from 'react-use/lib/useAsync'; import { contextSrv } from 'app/core/core'; -import { Role, AccessControlAction } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; import { fetchRoleOptions } from './api'; diff --git a/public/app/core/components/RolePicker/utils.ts b/public/app/core/components/RolePicker/utils.ts index ed1021f2d8c..413671f0090 100644 --- a/public/app/core/components/RolePicker/utils.ts +++ b/public/app/core/components/RolePicker/utils.ts @@ -1,4 +1,4 @@ -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; export const isNotDelegatable = (role: Role) => { return role.delegatable !== undefined && !role.delegatable; diff --git a/public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx b/public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx index 57789ebb518..522f752b9a6 100644 --- a/public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx +++ b/public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx @@ -5,7 +5,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { useStyles2, Badge, Stack } from '@grafana/ui'; -import { OrgUser } from 'app/types'; +import { OrgUser } from 'app/types/user'; import { RolePickerDrawer } from './RolePickerDrawer'; diff --git a/public/app/core/components/RolePickerDrawer/RolePickerDrawer.tsx b/public/app/core/components/RolePickerDrawer/RolePickerDrawer.tsx index 9b327449519..75afc684eab 100644 --- a/public/app/core/components/RolePickerDrawer/RolePickerDrawer.tsx +++ b/public/app/core/components/RolePickerDrawer/RolePickerDrawer.tsx @@ -1,9 +1,8 @@ import { Controller, useFormContext } from 'react-hook-form'; -import { toOption } from '@grafana/data'; +import { OrgRole, toOption } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Drawer, Field, RadioButtonGroup, TextLink } from '@grafana/ui'; -import { OrgRole } from 'app/types'; const roleOptions = Object.keys(OrgRole).map(toOption); diff --git a/public/app/core/components/Select/DashboardPicker.test.tsx b/public/app/core/components/Select/DashboardPicker.test.tsx index 2f662fe586b..b46c1f9d2df 100644 --- a/public/app/core/components/Select/DashboardPicker.test.tsx +++ b/public/app/core/components/Select/DashboardPicker.test.tsx @@ -10,7 +10,7 @@ import { import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { DashboardPicker } from './DashboardPicker'; diff --git a/public/app/core/components/Select/DashboardPicker.tsx b/public/app/core/components/Select/DashboardPicker.tsx index dfbb2458ade..bfb965a73bd 100644 --- a/public/app/core/components/Select/DashboardPicker.tsx +++ b/public/app/core/components/Select/DashboardPicker.tsx @@ -8,7 +8,7 @@ import { AnnoKeyFolder, AnnoKeyFolderTitle } from 'app/features/apiserver/types' import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { isDashboardV2Resource } from 'app/features/dashboard/api/utils'; import { DashboardSearchItem } from 'app/features/search/types'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; interface Props extends Omit, 'value' | 'onChange' | 'loadOptions' | ''> { value?: DashboardPickerDTO['uid']; diff --git a/public/app/core/components/Select/OldFolderPicker.tsx b/public/app/core/components/Select/OldFolderPicker.tsx index e5e684b7713..13ddca85986 100644 --- a/public/app/core/components/Select/OldFolderPicker.tsx +++ b/public/app/core/components/Select/OldFolderPicker.tsx @@ -13,7 +13,8 @@ import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/services/context_srv'; import { createFolder, getFolderByUid, searchFolders } from 'app/features/manage-dashboards/state/actions'; import { DashboardSearchHit } from 'app/features/search/types'; -import { AccessControlAction, PermissionLevelString, SearchQueryType } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { PermissionLevelString, SearchQueryType } from 'app/types/acl'; export type FolderPickerFilter = (hits: DashboardSearchHit[]) => DashboardSearchHit[]; diff --git a/public/app/core/components/Select/OrgPicker.tsx b/public/app/core/components/Select/OrgPicker.tsx index bc064c83d98..81ed7132d02 100644 --- a/public/app/core/components/Select/OrgPicker.tsx +++ b/public/app/core/components/Select/OrgPicker.tsx @@ -5,7 +5,8 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { AsyncSelect } from '@grafana/ui'; -import { Organization, UserOrg } from 'app/types'; +import { Organization } from 'app/types/organization'; +import { UserOrg } from 'app/types/user'; export type OrgSelectItem = SelectableValue; diff --git a/public/app/core/components/Select/ServiceAccountPicker.tsx b/public/app/core/components/Select/ServiceAccountPicker.tsx index b86a451dea1..037404dcb2b 100644 --- a/public/app/core/components/Select/ServiceAccountPicker.tsx +++ b/public/app/core/components/Select/ServiceAccountPicker.tsx @@ -6,7 +6,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { AsyncSelect } from '@grafana/ui'; -import { ServiceAccountDTO, ServiceAccountsState } from 'app/types'; +import { ServiceAccountDTO, ServiceAccountsState } from 'app/types/serviceaccount'; export interface Props { onSelected: (user: SelectableValue) => void; diff --git a/public/app/core/components/Select/TeamPicker.tsx b/public/app/core/components/Select/TeamPicker.tsx index b3608b80105..3f374fc8930 100644 --- a/public/app/core/components/Select/TeamPicker.tsx +++ b/public/app/core/components/Select/TeamPicker.tsx @@ -6,7 +6,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { AsyncSelect } from '@grafana/ui'; -import { Team } from 'app/types'; +import { Team } from 'app/types/teams'; export interface Props { onSelected: (team: SelectableValue) => void; diff --git a/public/app/core/components/Select/UserPicker.tsx b/public/app/core/components/Select/UserPicker.tsx index 0b1b5cb129f..5284cbeca5f 100644 --- a/public/app/core/components/Select/UserPicker.tsx +++ b/public/app/core/components/Select/UserPicker.tsx @@ -6,7 +6,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { AsyncSelect } from '@grafana/ui'; -import { OrgUser } from 'app/types'; +import { OrgUser } from 'app/types/user'; export interface Props { onSelected: (user: SelectableValue) => void; diff --git a/public/app/core/copy/appNotification.ts b/public/app/core/copy/appNotification.ts index 9f2fff8641f..3f1dd8f484f 100644 --- a/public/app/core/copy/appNotification.ts +++ b/public/app/core/copy/appNotification.ts @@ -3,7 +3,8 @@ import { v4 as uuidv4 } from 'uuid'; import { getMessageFromError } from 'app/core/utils/errors'; import { dispatch as storeDispatch } from 'app/store/store'; -import { AppNotification, AppNotificationSeverity, useDispatch } from 'app/types'; +import { AppNotificationSeverity, AppNotification } from 'app/types/appNotifications'; +import { useDispatch } from 'app/types/store'; import { notifyApp } from '../actions'; diff --git a/public/app/core/history/RichHistoryLocalStorage.test.ts b/public/app/core/history/RichHistoryLocalStorage.test.ts index 8899a7cc509..8cf739514e2 100644 --- a/public/app/core/history/RichHistoryLocalStorage.test.ts +++ b/public/app/core/history/RichHistoryLocalStorage.test.ts @@ -1,8 +1,8 @@ import { DataQuery } from '@grafana/data'; import store from 'app/core/store'; +import { RichHistoryQuery } from 'app/types/explore'; import { DatasourceSrv } from '../../features/plugins/datasource_srv'; -import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from '../utils/richHistoryTypes'; diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index 4396b73a1fa..165c2f06eac 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -2,8 +2,8 @@ import { find, isEqual, omit } from 'lodash'; import { DataQuery, SelectableValue } from '@grafana/data'; import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory'; +import { RichHistoryQuery } from 'app/types/explore'; -import { RichHistoryQuery } from '../../types'; import store from '../store'; import RichHistoryStorage, { RichHistoryServiceError, RichHistoryStorageWarning } from './RichHistoryStorage'; diff --git a/public/app/core/history/RichHistoryRemoteStorage.test.ts b/public/app/core/history/RichHistoryRemoteStorage.test.ts index 766712d1c0a..b99967b09b7 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.test.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.test.ts @@ -1,9 +1,9 @@ import { of } from 'rxjs'; import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen'; +import { RichHistoryQuery } from 'app/types/explore'; import { DatasourceSrv } from '../../features/plugins/datasource_srv'; -import { RichHistoryQuery } from '../../types'; import { SortOrder } from '../utils/richHistoryTypes'; import RichHistoryRemoteStorage, { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; @@ -45,7 +45,7 @@ jest.mock('../services/PreferencesService', () => ({ })); // FIXME: Tests break unless plugin loader is mocked. This is likely due to a circular dependency -jest.mock('app/features/plugins/plugin_loader', () => ({})); +jest.mock('app/features/plugins/pluginLoader', () => ({})); describe('RichHistoryRemoteStorage', () => { let storage: RichHistoryRemoteStorage; diff --git a/public/app/core/history/RichHistoryStorage.ts b/public/app/core/history/RichHistoryStorage.ts index 13d89a05172..158a5e7d6cb 100644 --- a/public/app/core/history/RichHistoryStorage.ts +++ b/public/app/core/history/RichHistoryStorage.ts @@ -1,6 +1,5 @@ import { RichHistorySearchBackendFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes'; - -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery } from 'app/types/explore'; /** * Errors are used when the operation on Rich History was not successful. diff --git a/public/app/core/history/localStorageConverter.test.ts b/public/app/core/history/localStorageConverter.test.ts index 20b8701a258..ffefb2ce8d4 100644 --- a/public/app/core/history/localStorageConverter.test.ts +++ b/public/app/core/history/localStorageConverter.test.ts @@ -1,6 +1,6 @@ import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { RichHistoryQuery } from 'app/types/explore'; -import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; import { RichHistoryLocalStorageDTO } from './RichHistoryLocalStorage'; diff --git a/public/app/core/history/localStorageConverter.ts b/public/app/core/history/localStorageConverter.ts index 62be110bcdd..485ebf3a1a3 100644 --- a/public/app/core/history/localStorageConverter.ts +++ b/public/app/core/history/localStorageConverter.ts @@ -1,6 +1,5 @@ import { getDataSourceSrv } from '@grafana/runtime'; - -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery } from 'app/types/explore'; import { RichHistoryLocalStorageDTO } from './RichHistoryLocalStorage'; diff --git a/public/app/core/history/remoteStorageConverter.test.ts b/public/app/core/history/remoteStorageConverter.test.ts index 6e42cc507ac..dd8f927ec66 100644 --- a/public/app/core/history/remoteStorageConverter.test.ts +++ b/public/app/core/history/remoteStorageConverter.test.ts @@ -1,5 +1,6 @@ +import { RichHistoryQuery } from 'app/types/explore'; + import { DatasourceSrv } from '../../features/plugins/datasource_srv'; -import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; import { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; diff --git a/public/app/core/history/remoteStorageConverter.ts b/public/app/core/history/remoteStorageConverter.ts index 6a743016e64..a4d15a677c3 100644 --- a/public/app/core/history/remoteStorageConverter.ts +++ b/public/app/core/history/remoteStorageConverter.ts @@ -1,6 +1,5 @@ import { getDataSourceSrv } from '@grafana/runtime'; - -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery } from 'app/types/explore'; import { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; diff --git a/public/app/core/history/richHistoryLocalStorageUtils.test.ts b/public/app/core/history/richHistoryLocalStorageUtils.test.ts index 7687f3f1bb5..29a4b7d79d2 100644 --- a/public/app/core/history/richHistoryLocalStorageUtils.test.ts +++ b/public/app/core/history/richHistoryLocalStorageUtils.test.ts @@ -1,7 +1,6 @@ import { DataQuery } from '@grafana/data'; import { SortOrder } from 'app/core/utils/richHistory'; - -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery } from 'app/types/explore'; import { filterAndSortQueries } from './richHistoryLocalStorageUtils'; diff --git a/public/app/core/history/richHistoryLocalStorageUtils.ts b/public/app/core/history/richHistoryLocalStorageUtils.ts index d22b4816781..caefdd7f45d 100644 --- a/public/app/core/history/richHistoryLocalStorageUtils.ts +++ b/public/app/core/history/richHistoryLocalStorageUtils.ts @@ -1,8 +1,8 @@ import { omit } from 'lodash'; import { DateTime, dateTime, dateTimeForTimeZone } from '@grafana/data'; +import { RichHistoryQuery } from 'app/types/explore'; -import { RichHistoryQuery } from '../../types'; import { SortOrder } from '../utils/richHistoryTypes'; /** diff --git a/public/app/core/hooks/useCleanup.ts b/public/app/core/hooks/useCleanup.ts index 86696b829d2..6cec29424d1 100644 --- a/public/app/core/hooks/useCleanup.ts +++ b/public/app/core/hooks/useCleanup.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { cleanUpAction, CleanUpAction } from '../actions/cleanUp'; diff --git a/public/app/core/hooks/useNavModel.ts b/public/app/core/hooks/useNavModel.ts index 047654109cc..a872f1a59f1 100644 --- a/public/app/core/hooks/useNavModel.ts +++ b/public/app/core/hooks/useNavModel.ts @@ -1,5 +1,5 @@ import { NavModel } from '@grafana/data'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { getNavModel } from '../selectors/navModel'; diff --git a/public/app/core/navigation/kiosk.ts b/public/app/core/navigation/kiosk.ts index 36c66c7eeeb..0e7ddfb3f3c 100644 --- a/public/app/core/navigation/kiosk.ts +++ b/public/app/core/navigation/kiosk.ts @@ -1,6 +1,5 @@ import { UrlQueryMap } from '@grafana/data'; - -import { KioskMode } from '../../types'; +import { KioskMode } from 'app/types/dashboard'; // TODO Remove after topnav feature toggle is permanent and old NavBar is removed export function getKioskMode(queryParams: UrlQueryMap): KioskMode | null { diff --git a/public/app/core/reducers/appNotification.test.ts b/public/app/core/reducers/appNotification.test.ts index 6094e1ac7bc..f9bb79baab0 100644 --- a/public/app/core/reducers/appNotification.test.ts +++ b/public/app/core/reducers/appNotification.test.ts @@ -1,4 +1,4 @@ -import { AppNotificationSeverity, AppNotificationsState } from 'app/types/'; +import { AppNotificationSeverity, AppNotificationsState } from 'app/types/appNotifications'; import { appNotificationsReducer, clearNotification, notifyApp } from './appNotification'; diff --git a/public/app/core/reducers/appNotification.ts b/public/app/core/reducers/appNotification.ts index d9f563d364e..aedb2d0a0fc 100644 --- a/public/app/core/reducers/appNotification.ts +++ b/public/app/core/reducers/appNotification.ts @@ -1,6 +1,6 @@ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { AppNotification, AppNotificationSeverity, AppNotificationsState } from 'app/types/'; +import { AppNotification, AppNotificationSeverity, AppNotificationsState } from 'app/types/appNotifications'; const MAX_STORED_NOTIFICATIONS = 25; export const STORAGE_KEY = 'notifications'; diff --git a/public/app/core/reducers/root.test.ts b/public/app/core/reducers/root.test.ts index 6b584776233..c9f5d5068bf 100644 --- a/public/app/core/reducers/root.test.ts +++ b/public/app/core/reducers/root.test.ts @@ -1,6 +1,7 @@ +import { Team } from 'app/types/teams'; + import { reducerTester } from '../../../test/core/redux/reducerTester'; import { initialTeamsState, teamsLoaded } from '../../features/teams/state/reducers'; -import { Team } from '../../types'; import { StoreState } from '../../types/store'; import { cleanUpAction } from '../actions/cleanUp'; diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index ac0d13d1051..f518dc83df7 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -25,7 +25,8 @@ import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardSearchItem } from 'app/features/search/types'; import { TokenRevokedModal } from 'app/features/users/TokenRevokedModal'; -import { DashboardDTO, FolderDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; +import { FolderDTO } from 'app/types/folders'; import { ShowModalReactEvent } from '../../types/events'; import { isContentTypeJson, parseInitFromOptions, parseResponseBody, parseUrlFromOptions } from '../utils/fetch'; diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 513b5a97699..8374d4b7bf1 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -11,7 +11,7 @@ import { } from '@grafana/data'; import { featureEnabled, getBackendSrv } from '@grafana/runtime'; import { getSessionExpiry } from 'app/core/utils/auth'; -import { AccessControlAction, UserPermission } from 'app/types'; +import { UserPermission, AccessControlAction } from 'app/types/accessControl'; import { CurrentUserInternal } from 'app/types/config'; import config from '../../core/config'; diff --git a/public/app/core/utils/richHistory.test.ts b/public/app/core/utils/richHistory.test.ts index 0c6a95b981d..0fba9ac1352 100644 --- a/public/app/core/utils/richHistory.test.ts +++ b/public/app/core/utils/richHistory.test.ts @@ -1,7 +1,7 @@ import { dateTime, DataQuery } from '@grafana/data'; import store from 'app/core/store'; +import { RichHistoryQuery } from 'app/types/explore'; -import { RichHistoryQuery } from '../../types'; import RichHistoryStorage, { RichHistoryStorageWarning } from '../history/RichHistoryStorage'; import { diff --git a/public/app/core/utils/shortLinks.test.ts b/public/app/core/utils/shortLinks.test.ts index 38d459b906a..406aa73e99f 100644 --- a/public/app/core/utils/shortLinks.test.ts +++ b/public/app/core/utils/shortLinks.test.ts @@ -15,6 +15,17 @@ jest.mock('@grafana/runtime', () => ({ }, })); +beforeEach(() => { + Object.assign(navigator, { + clipboard: { + write: jest.fn().mockResolvedValue(undefined), + writeText: jest.fn().mockResolvedValue(undefined), + }, + }); + + document.execCommand = jest.fn(); +}); + describe('createShortLink', () => { it('creates short link', async () => { const shortUrl = await createShortLink('www.verylonglinkwehavehere.com'); @@ -23,11 +34,34 @@ describe('createShortLink', () => { }); describe('createAndCopyShortLink', () => { - it('copies short link to clipboard', async () => { + it('copies short link to clipboard via document.execCommand when navigator.clipboard is undefined', async () => { + Object.assign(navigator, { + clipboard: { + write: undefined, + }, + }); document.execCommand = jest.fn(); await createAndCopyShortLink('www.verylonglinkwehavehere.com'); expect(document.execCommand).toHaveBeenCalledWith('copy'); }); + + it('copies short link to clipboard via navigator.clipboard.writeText when ClipboardItem is undefined', async () => { + window.isSecureContext = true; + await createAndCopyShortLink('www.verylonglinkwehavehere.com'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('www.short.com'); + }); + + it('copies short link to clipboard via navigator.clipboard.write and ClipboardItem when it is defined', async () => { + global.ClipboardItem = jest.fn().mockImplementation(() => ({ + type: 'text/plain', + size: 0, + slice: jest.fn(), + supports: jest.fn().mockReturnValue(true), + // eslint-disable-next-line + })) as any; + await createAndCopyShortLink('www.verylonglinkwehavehere.com'); + expect(navigator.clipboard.write).toHaveBeenCalled(); + }); }); describe('getLogsPermalinkRange', () => { diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index 3ab6556aa67..b6bfc95b006 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -35,13 +35,30 @@ export const createShortLink = memoizeOne(async function (path: string) { } }); +/** + * Creates a ClipboardItem for the shortened link. This is used due to clipboard issues in Safari after making async calls. + * See https://github.com/grafana/grafana/issues/106889 + * @param path - The long path to share. + * @returns A ClipboardItem for the shortened link. + */ +const createShortLinkClipboardItem = (path: string) => { + return new ClipboardItem({ + 'text/plain': createShortLink(path), + }); +}; + export const createAndCopyShortLink = async (path: string) => { - const shortLink = await createShortLink(path); - if (shortLink) { - copyStringToClipboard(shortLink); + if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write) { + navigator.clipboard.write([createShortLinkClipboardItem(path)]); dispatch(notifyApp(createSuccessNotification('Shortened link copied to clipboard'))); } else { - dispatch(notifyApp(createErrorNotification('Error generating shortened link'))); + const shortLink = await createShortLink(path); + if (shortLink) { + copyStringToClipboard(shortLink); + dispatch(notifyApp(createSuccessNotification('Shortened link copied to clipboard'))); + } else { + dispatch(notifyApp(createErrorNotification('Error generating shortened link'))); + } } }; diff --git a/public/app/dev-utils.ts b/public/app/dev-utils.ts index 478af4f8f64..07a656735c1 100644 --- a/public/app/dev-utils.ts +++ b/public/app/dev-utils.ts @@ -3,7 +3,7 @@ import store from 'app/core/store'; import { sendAppNotification } from './core/copy/appNotification'; import { PreferencesService } from './core/services/PreferencesService'; -import { AppNotificationSeverity } from './types'; +import { AppNotificationSeverity } from './types/appNotifications'; export const STORAGE_MOCK_API_KEY = 'grafana.dev.mockApi'; diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index 34b0d477523..fb6039792dc 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -3,12 +3,13 @@ import { useForm } from 'react-hook-form'; import { useParams } from 'react-router-dom-v5-compat'; import { useAsyncFn } from 'react-use'; -import { NavModelItem } from '@grafana/data'; +import { NavModelItem, OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Field, Input, Button, Legend, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { OrgUser, AccessControlAction, OrgRole } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { OrgUser } from 'app/types/user'; import { OrgUsersTable } from './Users/OrgUsersTable'; import { getOrg, getOrgUsers, getUsersRoles, removeOrgUser, updateOrgName, updateOrgUserRole } from './api'; diff --git a/public/app/features/admin/AdminListOrgsPage.tsx b/public/app/features/admin/AdminListOrgsPage.tsx index be3ef8f2624..48da226edaf 100644 --- a/public/app/features/admin/AdminListOrgsPage.tsx +++ b/public/app/features/admin/AdminListOrgsPage.tsx @@ -6,7 +6,8 @@ import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { LinkButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, Organization } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Organization } from 'app/types/organization'; import { AdminOrgsTable } from './AdminOrgsTable'; diff --git a/public/app/features/admin/AdminOrgsTable.tsx b/public/app/features/admin/AdminOrgsTable.tsx index 9feba68ae31..d85dcccfea5 100644 --- a/public/app/features/admin/AdminOrgsTable.tsx +++ b/public/app/features/admin/AdminOrgsTable.tsx @@ -7,7 +7,8 @@ import { Trans, t } from '@grafana/i18n'; import { Button, ConfirmModal, useStyles2 } from '@grafana/ui'; import { SkeletonComponent, attachSkeleton } from '@grafana/ui/unstable'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Organization } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Organization } from 'app/types/organization'; interface Props { orgs: Organization[]; diff --git a/public/app/features/admin/ServerStats.tsx b/public/app/features/admin/ServerStats.tsx index a7f62ec72c1..f3d8e4d0a66 100644 --- a/public/app/features/admin/ServerStats.tsx +++ b/public/app/features/admin/ServerStats.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { config, GrafanaBootConfig } from '@grafana/runtime'; import { LinkButton, Stack, useStyles2 } from '@grafana/ui'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { contextSrv } from '../../core/services/context_srv'; diff --git a/public/app/features/admin/UpgradePage.tsx b/public/app/features/admin/UpgradePage.tsx index 3a8b8ed7f02..61925407cbd 100644 --- a/public/app/features/admin/UpgradePage.tsx +++ b/public/app/features/admin/UpgradePage.tsx @@ -6,6 +6,7 @@ import { GrafanaTheme2, NavModel } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { StoreState } from 'app/types/store'; import checkmarkSvg from 'img/licensing/checkmark.svg'; import customerSupportSvg from 'img/licensing/customer_support.svg'; import handinhandSupportSvg from 'img/licensing/handinhand_support.svg'; @@ -13,7 +14,6 @@ import pluginEnterpriseSvg from 'img/licensing/plugin_enterprise.svg'; import slaSvg from 'img/licensing/sla.svg'; import { getNavModel } from '../../core/selectors/navModel'; -import { StoreState } from '../../types'; import { LicenseChrome } from './LicenseChrome'; import { ServerStats } from './ServerStats'; diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 8a2f2c84aa8..1642c3259e8 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -8,7 +8,10 @@ import { featureEnabled } from '@grafana/runtime'; import { Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { StoreState, UserDTO, UserOrg, UserSession, SyncInfo, UserAdminError, AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { SyncInfo } from 'app/types/ldap'; +import { StoreState } from 'app/types/store'; +import { UserDTO, UserOrg, UserSession, UserAdminError } from 'app/types/user'; import { UserLdapSyncInfo } from './UserLdapSyncInfo'; import { UserOrgs } from './UserOrgs'; diff --git a/public/app/features/admin/UserLdapSyncInfo.tsx b/public/app/features/admin/UserLdapSyncInfo.tsx index 387cc3eb9fa..ca1d0ec148a 100644 --- a/public/app/features/admin/UserLdapSyncInfo.tsx +++ b/public/app/features/admin/UserLdapSyncInfo.tsx @@ -4,7 +4,9 @@ import { dateTimeFormat } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, LinkButton } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, SyncInfo, UserDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { SyncInfo } from 'app/types/ldap'; +import { UserDTO } from 'app/types/user'; import { TagBadge } from '../../core/components/TagFilter/TagBadge'; diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index d4b485c3ada..215fd281f3e 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -8,8 +8,9 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, RadioButtonGroup, useStyles2, FilterInput, EmptyState } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; - -import { AccessControlAction, StoreState, UserFilter } from '../../types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState } from 'app/types/store'; +import { UserFilter } from 'app/types/user'; import { EnterpriseAuthFeaturesCard } from './EnterpriseAuthFeaturesCard'; import { UsersTable } from './Users/UsersTable'; diff --git a/public/app/features/admin/UserListAnonymousPage.tsx b/public/app/features/admin/UserListAnonymousPage.tsx index c2cd0404dd8..eec26483830 100644 --- a/public/app/features/admin/UserListAnonymousPage.tsx +++ b/public/app/features/admin/UserListAnonymousPage.tsx @@ -7,8 +7,7 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { t } from '@grafana/i18n'; import { RadioButtonGroup, useStyles2, FilterInput } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; - -import { StoreState } from '../../types'; +import { StoreState } from 'app/types/store'; import { AnonUsersDevicesTable } from './Users/AnonUsersTable'; import { fetchUsersAnonymousDevices, changeAnonUserSort, changeAnonPage, changeAnonQuery } from './state/actions'; diff --git a/public/app/features/admin/UserListPage.tsx b/public/app/features/admin/UserListPage.tsx index 97184596de4..a2357b4aecf 100644 --- a/public/app/features/admin/UserListPage.tsx +++ b/public/app/features/admin/UserListPage.tsx @@ -9,9 +9,9 @@ import { config } from '@grafana/runtime'; import { useStyles2, TabsBar, Tab } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { isEmailSharingEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; +import { AccessControlAction } from 'app/types/accessControl'; import { Page } from '../../core/components/Page/Page'; -import { AccessControlAction } from '../../types'; import { UsersListPageContent } from '../users/UsersListPage'; import { UserListAdminPageContent } from './UserListAdminPage'; diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 6aac4d1721c..201df124ec8 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -1,7 +1,7 @@ import { css, cx } from '@emotion/css'; import { createRef, PureComponent, ReactElement } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, @@ -20,7 +20,9 @@ import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Organization, OrgRole, Role, UserDTO, UserOrg } from 'app/types'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { Organization } from 'app/types/organization'; +import { UserOrg, UserDTO } from 'app/types/user'; import { OrgRolePicker } from './OrgRolePicker'; diff --git a/public/app/features/admin/UserPermissions.tsx b/public/app/features/admin/UserPermissions.tsx index ea658a48e49..4d36d628768 100644 --- a/public/app/features/admin/UserPermissions.tsx +++ b/public/app/features/admin/UserPermissions.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { ConfirmButton, RadioButtonGroup, Icon, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { ExternalUserTooltip } from 'app/features/admin/UserOrgs'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; interface Props { isGrafanaAdmin: boolean; diff --git a/public/app/features/admin/UserProfile.tsx b/public/app/features/admin/UserProfile.tsx index 9b9522a084a..1fb9da50d77 100644 --- a/public/app/features/admin/UserProfile.tsx +++ b/public/app/features/admin/UserProfile.tsx @@ -5,7 +5,8 @@ import * as React from 'react'; import { Trans, t } from '@grafana/i18n'; import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, Stack } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, UserDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { UserDTO } from 'app/types/user'; interface Props { user: UserDTO; diff --git a/public/app/features/admin/UserSessions.tsx b/public/app/features/admin/UserSessions.tsx index 2ee811676d4..f05278d7884 100644 --- a/public/app/features/admin/UserSessions.tsx +++ b/public/app/features/admin/UserSessions.tsx @@ -5,7 +5,8 @@ import { ConfirmButton, ConfirmModal, Button, Stack } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { contextSrv } from 'app/core/core'; import { formatDate } from 'app/core/internationalization/dates'; -import { AccessControlAction, UserSession } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { UserSession } from 'app/types/user'; interface Props { sessions: UserSession[]; diff --git a/public/app/features/admin/Users/AnonUsersTable.tsx b/public/app/features/admin/Users/AnonUsersTable.tsx index b4a4882314f..ba540e280a6 100644 --- a/public/app/features/admin/Users/AnonUsersTable.tsx +++ b/public/app/features/admin/Users/AnonUsersTable.tsx @@ -13,7 +13,7 @@ import { FetchDataFunc, } from '@grafana/ui'; import { EmptyArea } from 'app/features/alerting/unified/components/EmptyArea'; -import { UserAnonymousDeviceDTO } from 'app/types'; +import { UserAnonymousDeviceDTO } from 'app/types/user'; type Cell = CellProps< UserAnonymousDeviceDTO, diff --git a/public/app/features/admin/Users/OrgUnits.tsx b/public/app/features/admin/Users/OrgUnits.tsx index 5dc25622fed..b029df9e19e 100644 --- a/public/app/features/admin/Users/OrgUnits.tsx +++ b/public/app/features/admin/Users/OrgUnits.tsx @@ -2,7 +2,7 @@ import { forwardRef, PropsWithChildren } from 'react'; import { IconName } from '@grafana/data'; import { Icon, Tooltip, Box, Stack } from '@grafana/ui'; -import { Unit } from 'app/types'; +import { Unit } from 'app/types/user'; type OrgUnitProps = { units?: Unit[]; icon: IconName }; diff --git a/public/app/features/admin/Users/OrgUsersTable.test.tsx b/public/app/features/admin/Users/OrgUsersTable.test.tsx index ea64232081e..8fa183441db 100644 --- a/public/app/features/admin/Users/OrgUsersTable.test.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.test.tsx @@ -1,7 +1,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { OrgUser } from '../../../types'; +import { OrgUser } from 'app/types/user'; + import { getMockUsers } from '../../users/mocks/userMocks'; import { OrgUsersTable, Props } from './OrgUsersTable'; diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index d83485531db..42f69b89cf2 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -25,7 +25,8 @@ import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicke import { RolePickerBadges } from 'app/core/components/RolePickerDrawer/RolePickerBadges'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, OrgUser, Role } from 'app/types'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { OrgUser } from 'app/types/user'; import { OrgRolePicker } from '../OrgRolePicker'; diff --git a/public/app/features/admin/Users/UsersTable.test.tsx b/public/app/features/admin/Users/UsersTable.test.tsx index 667aca52b89..61c8210a56f 100644 --- a/public/app/features/admin/Users/UsersTable.test.tsx +++ b/public/app/features/admin/Users/UsersTable.test.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom-v5-compat'; -import { UserDTO } from '../../../types'; +import { UserDTO } from 'app/types/user'; import { UsersTable, UsersTableProps } from './UsersTable'; diff --git a/public/app/features/admin/Users/UsersTable.tsx b/public/app/features/admin/Users/UsersTable.tsx index 7deb4480fd4..a3a1f6be297 100644 --- a/public/app/features/admin/Users/UsersTable.tsx +++ b/public/app/features/admin/Users/UsersTable.tsx @@ -17,7 +17,7 @@ import { Tooltip, } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; -import { UserDTO } from 'app/types'; +import { UserDTO } from 'app/types/user'; import { OrgUnits } from './OrgUnits'; diff --git a/public/app/features/admin/api.ts b/public/app/features/admin/api.ts index 0db8de7ed13..f1e4f5419d4 100644 --- a/public/app/features/admin/api.ts +++ b/public/app/features/admin/api.ts @@ -2,7 +2,8 @@ import { UrlQueryValue } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { OrgUser, AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { OrgUser } from 'app/types/user'; const perPage = 30; diff --git a/public/app/features/admin/ldap/LdapConnectionStatus.tsx b/public/app/features/admin/ldap/LdapConnectionStatus.tsx index 59dc679d197..268ac3ea1b5 100644 --- a/public/app/features/admin/ldap/LdapConnectionStatus.tsx +++ b/public/app/features/admin/ldap/LdapConnectionStatus.tsx @@ -2,7 +2,8 @@ import { useMemo } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Alert, CellProps, Column, Icon, InteractiveTable, Stack, Text, Tooltip } from '@grafana/ui'; -import { AppNotificationSeverity, LdapConnectionInfo, LdapServerInfo } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; +import { LdapConnectionInfo, LdapServerInfo } from 'app/types/ldap'; interface Props { ldapConnectionInfo: LdapConnectionInfo; diff --git a/public/app/features/admin/ldap/LdapDrawer.tsx b/public/app/features/admin/ldap/LdapDrawer.tsx index cd3e0a1342c..159a66a9ea5 100644 --- a/public/app/features/admin/ldap/LdapDrawer.tsx +++ b/public/app/features/admin/ldap/LdapDrawer.tsx @@ -23,7 +23,7 @@ import { RadioButtonGroup, SecretInput, } from '@grafana/ui'; -import { LdapPayload, MapKeyCertConfigured } from 'app/types'; +import { MapKeyCertConfigured, LdapPayload } from 'app/types/ldap'; import { GroupMappingComponent } from './LdapGroupMapping'; diff --git a/public/app/features/admin/ldap/LdapGroupMapping.tsx b/public/app/features/admin/ldap/LdapGroupMapping.tsx index 6680294e578..1a7b8d9450a 100644 --- a/public/app/features/admin/ldap/LdapGroupMapping.tsx +++ b/public/app/features/admin/ldap/LdapGroupMapping.tsx @@ -1,10 +1,10 @@ import { useFormContext } from 'react-hook-form'; -import { SelectableValue } from '@grafana/data'; +import { OrgRole, SelectableValue } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Box, Button, Field, Input, RadioButtonGroup, Switch } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { LdapPayload, OrgRole } from 'app/types'; +import { LdapPayload } from 'app/types/ldap'; const roleOptions: Array> = Object.keys(OrgRole).map((key) => { return { label: key, value: key }; diff --git a/public/app/features/admin/ldap/LdapPage.tsx b/public/app/features/admin/ldap/LdapPage.tsx index ce1ba188936..4c6bc32f0e7 100644 --- a/public/app/features/admin/ldap/LdapPage.tsx +++ b/public/app/features/admin/ldap/LdapPage.tsx @@ -9,15 +9,10 @@ import { Alert, Button, Field, Input, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { - AppNotificationSeverity, - LdapError, - LdapUser, - StoreState, - SyncInfo, - LdapConnectionInfo, - AccessControlAction, -} from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; +import { LdapConnectionInfo, LdapUser, SyncInfo, LdapError } from 'app/types/ldap'; +import { StoreState } from 'app/types/store'; import { loadLdapState, diff --git a/public/app/features/admin/ldap/LdapSettingsPage.tsx b/public/app/features/admin/ldap/LdapSettingsPage.tsx index 7ba327c1ce0..b100c9386fb 100644 --- a/public/app/features/admin/ldap/LdapSettingsPage.tsx +++ b/public/app/features/admin/ldap/LdapSettingsPage.tsx @@ -27,7 +27,8 @@ import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; import { Page } from 'app/core/components/Page/Page'; import config from 'app/core/config'; import { Loader } from 'app/features/plugins/admin/components/Loader'; -import { LdapPayload, MapKeyCertConfigured, StoreState } from 'app/types'; +import { LdapPayload, MapKeyCertConfigured } from 'app/types/ldap'; +import { StoreState } from 'app/types/store'; import { LdapDrawerComponent } from './LdapDrawer'; diff --git a/public/app/features/admin/ldap/LdapSyncInfo.tsx b/public/app/features/admin/ldap/LdapSyncInfo.tsx index 11a1db488b5..c4aacb5a7ad 100644 --- a/public/app/features/admin/ldap/LdapSyncInfo.tsx +++ b/public/app/features/admin/ldap/LdapSyncInfo.tsx @@ -1,7 +1,7 @@ import { dateTimeFormat } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { InteractiveTable, Text } from '@grafana/ui'; -import { SyncInfo } from 'app/types'; +import { SyncInfo } from 'app/types/ldap'; interface Props { ldapSyncInfo: SyncInfo; diff --git a/public/app/features/admin/ldap/LdapUserGroups.tsx b/public/app/features/admin/ldap/LdapUserGroups.tsx index d2e82b83e3e..7f0ff635a44 100644 --- a/public/app/features/admin/ldap/LdapUserGroups.tsx +++ b/public/app/features/admin/ldap/LdapUserGroups.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Tooltip, Icon, InteractiveTable, type CellProps, Column, Stack } from '@grafana/ui'; -import { LdapRole } from 'app/types'; +import { LdapRole } from 'app/types/ldap'; interface Props { groups: LdapRole[]; diff --git a/public/app/features/admin/ldap/LdapUserInfo.tsx b/public/app/features/admin/ldap/LdapUserInfo.tsx index 0877216ca6f..73d6a79a87b 100644 --- a/public/app/features/admin/ldap/LdapUserInfo.tsx +++ b/public/app/features/admin/ldap/LdapUserInfo.tsx @@ -1,6 +1,6 @@ import { Trans } from '@grafana/i18n'; import { Box, Stack, Text } from '@grafana/ui'; -import { LdapUser } from 'app/types'; +import { LdapUser } from 'app/types/ldap'; import { LdapUserGroups } from './LdapUserGroups'; import { LdapUserMappingInfo } from './LdapUserMappingInfo'; diff --git a/public/app/features/admin/ldap/LdapUserMappingInfo.tsx b/public/app/features/admin/ldap/LdapUserMappingInfo.tsx index 534ea3cf0a7..e4485a9d3c2 100644 --- a/public/app/features/admin/ldap/LdapUserMappingInfo.tsx +++ b/public/app/features/admin/ldap/LdapUserMappingInfo.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { InteractiveTable } from '@grafana/ui'; -import { LdapUserInfo } from 'app/types'; +import { LdapUserInfo } from 'app/types/ldap'; interface Props { info: LdapUserInfo; diff --git a/public/app/features/admin/ldap/LdapUserPermissions.tsx b/public/app/features/admin/ldap/LdapUserPermissions.tsx index e426a4e090b..57dc65f4c96 100644 --- a/public/app/features/admin/ldap/LdapUserPermissions.tsx +++ b/public/app/features/admin/ldap/LdapUserPermissions.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Trans } from '@grafana/i18n'; import { Column, Icon, InteractiveTable } from '@grafana/ui'; -import { LdapPermissions } from 'app/types'; +import { LdapPermissions } from 'app/types/ldap'; interface Props { permissions: LdapPermissions; diff --git a/public/app/features/admin/ldap/LdapUserTeams.tsx b/public/app/features/admin/ldap/LdapUserTeams.tsx index 39f9f779710..b37cad73034 100644 --- a/public/app/features/admin/ldap/LdapUserTeams.tsx +++ b/public/app/features/admin/ldap/LdapUserTeams.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { Column, InteractiveTable, CellProps } from '@grafana/ui'; -import { LdapTeam } from 'app/types'; +import { LdapTeam } from 'app/types/ldap'; interface Props { teams: LdapTeam[]; diff --git a/public/app/features/admin/state/actions.ts b/public/app/features/admin/state/actions.ts index ede0ed15dd6..7dd57ea1e72 100644 --- a/public/app/features/admin/state/actions.ts +++ b/public/app/features/admin/state/actions.ts @@ -6,15 +6,10 @@ import { FetchDataArgs } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { - ThunkResult, - LdapUser, - UserSession, - UserDTO, - AccessControlAction, - UserFilter, - AnonUserFilter, -} from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { LdapUser } from 'app/types/ldap'; +import { ThunkResult } from 'app/types/store'; +import { UserDTO, UserSession, UserFilter, AnonUserFilter } from 'app/types/user'; import { userAdminPageLoadedAction, diff --git a/public/app/features/admin/state/reducers.test.ts b/public/app/features/admin/state/reducers.test.ts index 8b504a62935..67652835859 100644 --- a/public/app/features/admin/state/reducers.test.ts +++ b/public/app/features/admin/state/reducers.test.ts @@ -1,6 +1,7 @@ import { reducerTester } from 'test/core/redux/reducerTester'; -import { LdapState, LdapUser, UserAdminState, UserDTO, UserListAdminState } from 'app/types'; +import { LdapState, LdapUser } from 'app/types/ldap'; +import { UserAdminState, UserListAdminState, UserDTO } from 'app/types/user'; import { clearUserMappingInfoAction, diff --git a/public/app/features/admin/state/reducers.ts b/public/app/features/admin/state/reducers.ts index 550e3de57b6..67f71dec9f3 100644 --- a/public/app/features/admin/state/reducers.ts +++ b/public/app/features/admin/state/reducers.ts @@ -1,22 +1,18 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { LdapState, LdapConnectionInfo, LdapError, SyncInfo, LdapUser } from 'app/types/ldap'; import { - LdapConnectionInfo, - LdapError, - LdapState, - LdapUser, - SyncInfo, - UserAdminError, UserAdminState, UserDTO, UserOrg, UserSession, + UserAdminError, UserListAdminState, UserFilter, UserListAnonymousDevicesState, UserAnonymousDeviceDTO, AnonUserFilter, -} from 'app/types'; +} from 'app/types/user'; const initialLdapState: LdapState = { connectionInfo: [], diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 12a8dbfbadf..e51fd7cc27e 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -3,7 +3,7 @@ import { Navigate } from 'react-router-dom-v5-compat'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { config } from 'app/core/config'; import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PERMISSIONS_CONTACT_POINTS } from './unified/components/contact-points/permissions'; import { @@ -314,8 +314,16 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { ), }, { + // This route is for backward compatibility + // Previously we had a single admin page containing only the alertmanager settings + // We now have a separate route for the alertmanager settings and other settings can be added as extensions path: '/alerting/admin', roles: () => ['Admin'], + component: () => , + }, + { + path: '/alerting/admin/alertmanager', + roles: () => ['Admin'], component: importAlertingComponent( () => import(/* webpackChunkName: "AlertingSettings" */ 'app/features/alerting/unified/Settings') ), diff --git a/public/app/features/alerting/state/reducers.test.ts b/public/app/features/alerting/state/reducers.test.ts index ac9b9acda67..7afd5eee5e7 100644 --- a/public/app/features/alerting/state/reducers.test.ts +++ b/public/app/features/alerting/state/reducers.test.ts @@ -1,5 +1,5 @@ import { dateTime } from '@grafana/data'; -import { AlertRuleDTO, AlertRulesState, NotificationChannelState, NotifierDTO } from 'app/types'; +import { AlertRuleDTO, AlertRulesState, NotificationChannelState, NotifierDTO } from 'app/types/alerting'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index f4c9b798bc2..ffba9aa8c89 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -8,7 +8,7 @@ import { NotificationChannelOption, NotificationChannelState, NotifierDTO, -} from 'app/types'; +} from 'app/types/alerting'; import unifiedAlertingReducer from '../unified/state/reducers'; diff --git a/public/app/features/alerting/state/selectors.test.ts b/public/app/features/alerting/state/selectors.test.ts index b279c1ebd41..0463a99be8e 100644 --- a/public/app/features/alerting/state/selectors.test.ts +++ b/public/app/features/alerting/state/selectors.test.ts @@ -1,4 +1,5 @@ -import { AlertRulesState, StoreState } from 'app/types'; +import { AlertRulesState } from 'app/types/alerting'; +import { StoreState } from 'app/types/store'; import { getAlertRuleItems, getSearchQuery } from './selectors'; diff --git a/public/app/features/alerting/state/selectors.ts b/public/app/features/alerting/state/selectors.ts index f51e66ce8a4..0e2c897e7bb 100644 --- a/public/app/features/alerting/state/selectors.ts +++ b/public/app/features/alerting/state/selectors.ts @@ -1,4 +1,5 @@ -import { AlertRule, AlertRulesState, NotificationChannelState, StoreState } from 'app/types'; +import { AlertRule, AlertRulesState, NotificationChannelState } from 'app/types/alerting'; +import { StoreState } from 'app/types/store'; export const getSearchQuery = (state: AlertRulesState) => state.searchQuery; diff --git a/public/app/features/alerting/unified/AlertGroups.test.tsx b/public/app/features/alerting/unified/AlertGroups.test.tsx index a39b0348f15..dbf38615d33 100644 --- a/public/app/features/alerting/unified/AlertGroups.test.tsx +++ b/public/app/features/alerting/unified/AlertGroups.test.tsx @@ -3,7 +3,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { selectors } from '@grafana/e2e-selectors'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import AlertGroups from './AlertGroups'; import { fetchAlertGroups } from './api/alertmanager'; diff --git a/public/app/features/alerting/unified/AlertGroups.tsx b/public/app/features/alerting/unified/AlertGroups.tsx index a72d973645b..0ca50b2c7c9 100644 --- a/public/app/features/alerting/unified/AlertGroups.tsx +++ b/public/app/features/alerting/unified/AlertGroups.tsx @@ -3,7 +3,7 @@ import { Fragment, useEffect } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Alert, Box, LoadingPlaceholder, Text } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { AlertmanagerChoice } from '../../../plugins/datasource/alertmanager/types'; diff --git a/public/app/features/alerting/unified/AlertsFolderView.tsx b/public/app/features/alerting/unified/AlertsFolderView.tsx index 8e13f93435a..ec39dcdde18 100644 --- a/public/app/features/alerting/unified/AlertsFolderView.tsx +++ b/public/app/features/alerting/unified/AlertsFolderView.tsx @@ -8,7 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { Card, FilterInput, Icon, Pagination, Select, Stack, TagList, useStyles2 } from '@grafana/ui'; import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants'; import { getQueryParamValue } from 'app/core/utils/query'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; import { usePagination } from './hooks/usePagination'; diff --git a/public/app/features/alerting/unified/MuteTimings.test.tsx b/public/app/features/alerting/unified/MuteTimings.test.tsx index 0b69e95653e..fc44b90a59f 100644 --- a/public/app/features/alerting/unified/MuteTimings.test.tsx +++ b/public/app/features/alerting/unified/MuteTimings.test.tsx @@ -14,7 +14,7 @@ import { } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import EditMuteTimingPage from './components/mute-timings/EditMuteTiming'; import NewMuteTimingPage from './components/mute-timings/NewMuteTiming'; diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx index f48f3eb16fe..90bfe88ad4b 100644 --- a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx +++ b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx @@ -29,7 +29,7 @@ import { MatcherOperator, RouteWithID, } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import NotificationPolicies from './NotificationPoliciesPage'; import { findRoutesMatchingFilters } from './components/notification-policies/NotificationPoliciesList'; diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index 9e01d77d060..dfe3549eeee 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -7,7 +7,7 @@ import config from 'app/core/config'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertQuery, PromRulesResponse } from 'app/types/unified-alerting-dto'; import { PanelAlertTabContent } from './PanelAlertTabContent'; diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index 0363e772509..f831a707bb3 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -10,7 +10,7 @@ import { import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import ContactPoints from './components/contact-points/ContactPoints'; import EditContactPoint from './components/contact-points/EditContactPoint'; diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx index 196ee8b0583..fcc74629ca1 100644 --- a/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx @@ -9,7 +9,7 @@ import { FeatureToggles } from '@grafana/data'; import { contextSrv } from 'app/core/services/context_srv'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions, mockDataSource } from './mocks'; import { grafanaRulerGroup } from './mocks/grafanaRulerApi'; diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 5868e5b4c13..905920cbf1d 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -12,7 +12,7 @@ import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/serv import * as actions from 'app/features/alerting/unified/state/actions'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto'; import RuleList from './RuleList'; diff --git a/public/app/features/alerting/unified/Settings.test.tsx b/public/app/features/alerting/unified/Settings.test.tsx index 0d362c74b45..cba6f0a0308 100644 --- a/public/app/features/alerting/unified/Settings.test.tsx +++ b/public/app/features/alerting/unified/Settings.test.tsx @@ -7,6 +7,7 @@ import DataSourcesResponse from './components/settings/mocks/api/datasources.jso import { setupGrafanaManagedServer, withExternalOnlySetting } from './components/settings/mocks/server'; import { setupMswServer } from './mockApi'; import { grantUserRole } from './mocks'; +import { addSettingsSection, clearSettingsExtensions } from './settings/extensions'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -35,12 +36,23 @@ const ui = { versionsTab: byRole('tab', { name: /versions/i }), provisionedBadge: byText(/^Provisioned$/), + + // New selectors for extension tabs + alertmanagerTab: byRole('tab', { name: 'Alert managers' }), + enrichmentTab: byRole('tab', { name: 'Enrichment' }), + notificationsTab: byRole('tab', { name: 'Notifications' }), + customTab: (name: string) => byRole('tab', { name }), }; describe('Alerting settings', () => { beforeEach(() => { grantUserRole('ServerAdmin'); setupGrafanaManagedServer(server); + clearSettingsExtensions(); + }); + + afterEach(() => { + clearSettingsExtensions(); }); it('should render the page with Built-in only enabled, others disabled', async () => { @@ -131,4 +143,118 @@ describe('Alerting settings', () => { expect(enableButton).not.toBeInTheDocument(); expect(disableButton).not.toBeInTheDocument(); }); + + it('should display additional tabs when settings extensions are registered', async () => { + // Register extensions before rendering + addSettingsSection({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + icon: 'star', + }); + + addSettingsSection({ + id: 'notifications', + text: 'Notifications', + url: '/alerting/admin/notifications', + icon: 'bell', + }); + + render(, { + historyOptions: { + initialEntries: ['/alerting/admin/alertmanager'], + }, + }); + + // Wait for the page to load + await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + + // Check that the extension tabs are visible + expect(ui.enrichmentTab.get()).toBeInTheDocument(); + expect(ui.notificationsTab.get()).toBeInTheDocument(); + }); + + it('should correctly show active state for extension tabs based on route', async () => { + // Register an extension + addSettingsSection({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + icon: 'star', + }); + + // Render with the extension route as active + render(, { + historyOptions: { + initialEntries: ['/alerting/admin/enrichment'], + }, + }); + + // Wait for the page to load + await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + + // Check that the extension tab is visible and active + const enrichmentTab = ui.enrichmentTab.get(); + expect(enrichmentTab).toBeInTheDocument(); + expect(enrichmentTab).toHaveAttribute('aria-selected', 'true'); + + // Check that the default alertmanager tab is not active + const alertmanagerTab = ui.alertmanagerTab.get(); + expect(alertmanagerTab).toHaveAttribute('aria-selected', 'false'); + }); + + it('should handle multiple extensions correctly', async () => { + // Register multiple extensions + addSettingsSection({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + icon: 'star', + }); + + addSettingsSection({ + id: 'notifications', + text: 'Notifications', + url: '/alerting/admin/notifications', + icon: 'bell', + }); + + addSettingsSection({ + id: 'custom-settings', + text: 'Custom Settings', + url: '/alerting/admin/custom', + icon: 'cog', + }); + + render(, { + historyOptions: { + initialEntries: ['/alerting/admin/alertmanager'], + }, + }); + + // Wait for the page to load + await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + + // Check that all tabs are visible + expect(ui.alertmanagerTab.get()).toBeInTheDocument(); + expect(ui.enrichmentTab.get()).toBeInTheDocument(); + expect(ui.notificationsTab.get()).toBeInTheDocument(); + expect(ui.customTab('Custom Settings').get()).toBeInTheDocument(); + }); + + it('should not show extension tabs when no extensions are registered', async () => { + render(, { + historyOptions: { + initialEntries: ['/alerting/admin/alertmanager'], + }, + }); + + // Wait for the page to load + await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + + // Check that only the default alertmanager tab is visible + expect(ui.alertmanagerTab.get()).toBeInTheDocument(); + expect(ui.enrichmentTab.query()).not.toBeInTheDocument(); + expect(ui.notificationsTab.query()).not.toBeInTheDocument(); + }); }); diff --git a/public/app/features/alerting/unified/Settings.tsx b/public/app/features/alerting/unified/Settings.tsx index e1a1eea1679..f09e9036fd3 100644 --- a/public/app/features/alerting/unified/Settings.tsx +++ b/public/app/features/alerting/unified/Settings.tsx @@ -7,24 +7,28 @@ import { useEditConfigurationDrawer } from './components/settings/ConfigurationD import { ExternalAlertmanagers } from './components/settings/ExternalAlertmanagers'; import InternalAlertmanager from './components/settings/InternalAlertmanager'; import { SettingsProvider, useSettings } from './components/settings/SettingsContext'; +import { useSettingsPageNav } from './settings/navigation'; import { withPageErrorBoundary } from './withPageErrorBoundary'; -function SettingsPage() { +function AlertmanagerSettingsPage() { return ( - + ); } -function SettingsContent() { +function AlertmanagerSettingsContent() { const [configurationDrawer, showConfiguration] = useEditConfigurationDrawer(); const { isLoading } = useSettings(); + const { navId, pageNav } = useSettingsPageNav(); + return ( { +const MenuItemPauseRule = ({ uid, isPaused, groupIdentifier, onPauseChange }: Props) => { const notifyApp = useAppNotification(); const [pauseRule, updateState] = usePauseRuleInGroup(); - const [icon, title] = isPausedRule(rule) - ? ['play' as const, 'Resume evaluation'] - : ['pause' as const, 'Pause evaluation']; + const [icon, title] = isPaused ? ['play' as const, 'Resume evaluation'] : ['pause' as const, 'Pause evaluation']; /** * Triggers API call to update the current rule to the new `is_paused` state */ const setRulePause = async (newIsPaused: boolean) => { try { - await pauseRule.execute(groupIdentifier, rule.grafana_alert.uid, newIsPaused); + await pauseRule.execute(groupIdentifier, uid, newIsPaused); } catch (error) { notifyApp.error(`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule: ${stringifyErrorLike(error)}`); return; @@ -49,7 +46,7 @@ const MenuItemPauseRule = ({ rule, groupIdentifier, onPauseChange }: Props) => { icon={icon} disabled={isLoading(updateState)} onClick={() => { - setRulePause(!rule.grafana_alert.is_paused); + setRulePause(!isPaused); }} /> ); diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx index 3177a8aa6e7..642c47a3f9d 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx @@ -5,7 +5,7 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { AlertState, AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertmanagerAction } from '../../hooks/useAbilities'; import { isGrafanaRulesSource } from '../../utils/datasource'; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index 87f1ebfff59..79cec523d69 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -7,7 +7,7 @@ import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server import { flushMicrotasks } from 'app/features/alerting/unified/test/test-utils'; import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions, mockDataSource } from '../../mocks'; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx index a9bbbf29845..6f2423e424d 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx @@ -17,7 +17,7 @@ import { import { contextSrv } from 'app/core/core'; import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { makeAMLink, stringifyErrorLike } from 'app/features/alerting/unified/utils/misc'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { usePagination } from '../../hooks/usePagination'; diff --git a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx index b43671ce874..04f6fcdb435 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx @@ -4,7 +4,7 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { render, screen, within } from 'test/test-utils'; import EditContactPoint from 'app/features/alerting/unified/components/contact-points/EditContactPoint'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; diff --git a/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx b/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx index ee5cd458b8d..479d6453c26 100644 --- a/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx @@ -1,7 +1,7 @@ import { render, screen, within } from 'test/test-utils'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; diff --git a/public/app/features/alerting/unified/components/contact-points/permissions.ts b/public/app/features/alerting/unified/components/contact-points/permissions.ts index 48ba46a10c5..48686fc5fb1 100644 --- a/public/app/features/alerting/unified/components/contact-points/permissions.ts +++ b/public/app/features/alerting/unified/components/contact-points/permissions.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; /** * List of granular permissions that allow viewing contact points diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx index 59f38e5a4fe..2ac9c04f981 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx @@ -6,7 +6,7 @@ import { disablePlugin } from 'app/features/alerting/unified/mocks/server/config import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins'; import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index f45535df9dc..034ee098894 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -11,7 +11,7 @@ import { Receiver, Route, } from 'app/plugins/datasource/alertmanager/types'; -import { NotifierDTO, NotifierStatus, ReceiversStateDTO } from 'app/types'; +import { NotifierDTO, NotifierStatus, ReceiversStateDTO } from 'app/types/alerting'; import { OnCallIntegrationDTO } from '../../api/onCallApi'; import { computeInheritedTree } from '../../utils/notification-policies'; diff --git a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx index 0f61f8c7a42..a8e3d4d3fba 100644 --- a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx +++ b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx @@ -8,7 +8,7 @@ import { Button, Field, Input, Label, Modal, Stack, useStyles2 } from '@grafana/ import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; import { useNewFolderMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { Folder } from '../../types/rule-form'; diff --git a/public/app/features/alerting/unified/components/export/GrafanaRuleFolderExporter.tsx b/public/app/features/alerting/unified/components/export/GrafanaRuleFolderExporter.tsx index cefe9fa5530..b240743f672 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaRuleFolderExporter.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaRuleFolderExporter.tsx @@ -2,8 +2,8 @@ import { useState } from 'react'; import { t } from '@grafana/i18n'; import { LoadingPlaceholder } from '@grafana/ui'; +import { FolderDTO } from 'app/types/folders'; -import { FolderDTO } from '../../../../../types'; import { alertRuleApi } from '../../api/alertRuleApi'; import { FileExportPreview } from './FileExportPreview'; diff --git a/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx b/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx index d677309fb51..14988605dbe 100644 --- a/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx +++ b/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { t } from '@grafana/i18n'; import { config, locationService } from '@grafana/runtime'; import { Dropdown, Menu } from '@grafana/ui'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { alertingFolderActionsApi } from '../../api/alertingFolderActionsApi'; import { shouldUseAlertingListViewV2, shouldUsePrometheusRulesPrimary } from '../../featureToggles'; diff --git a/public/app/features/alerting/unified/components/import-to-gma/ImportToGMARules.test.tsx b/public/app/features/alerting/unified/components/import-to-gma/ImportToGMARules.test.tsx index ad0d4feb52f..da2fbd63528 100644 --- a/public/app/features/alerting/unified/components/import-to-gma/ImportToGMARules.test.tsx +++ b/public/app/features/alerting/unified/components/import-to-gma/ImportToGMARules.test.tsx @@ -2,7 +2,7 @@ import { render, waitFor } from 'test/test-utils'; import { byLabelText, byRole } from 'testing-library-selector'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; diff --git a/public/app/features/alerting/unified/components/import-to-gma/hooks.ts b/public/app/features/alerting/unified/components/import-to-gma/hooks.ts index 0d41f413a2b..a15d6f85218 100644 --- a/public/app/features/alerting/unified/components/import-to-gma/hooks.ts +++ b/public/app/features/alerting/unified/components/import-to-gma/hooks.ts @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { lastValueFrom } from 'rxjs'; import { getBackendSrv } from '@grafana/runtime'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; import { alertRuleApi } from '../../api/alertRuleApi'; diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx index a60d1902d7b..d9d611d7a00 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx @@ -7,7 +7,7 @@ import { } from 'app/features/alerting/unified/mocks/server/configure'; import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions } from '../../mocks'; import { TIME_INTERVAL_UID_HAPPY_PATH } from '../../mocks/server/handlers/k8s/timeIntervals.k8s'; diff --git a/public/app/features/alerting/unified/components/mute-timings/permissions.ts b/public/app/features/alerting/unified/components/mute-timings/permissions.ts index 5cbf687c636..b2891d4b251 100644 --- a/public/app/features/alerting/unified/components/mute-timings/permissions.ts +++ b/public/app/features/alerting/unified/components/mute-timings/permissions.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; /** * List of granular permissions that allow viewing contact points diff --git a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx index e7ed08151a5..f426b2ad737 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx @@ -6,7 +6,7 @@ import { Button } from '@grafana/ui'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RouteWithID } from '../../../../../plugins/datasource/alertmanager/types'; import { FormAmRoute } from '../../types/amroutes'; diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.test.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.test.tsx index c83c0813cf1..171e4349bf3 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.test.tsx @@ -6,7 +6,7 @@ import { Button } from '@grafana/ui'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RouteWithID } from '../../../../../plugins/datasource/alertmanager/types'; import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index 346a9de008e..ff037ca21f2 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -32,7 +32,7 @@ import { Receiver, RouteWithID, } from 'app/plugins/datasource/alertmanager/types'; -import { ReceiversState } from 'app/types'; +import { ReceiversState } from 'app/types/alerting'; import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { getAmMatcherFormatter } from '../../utils/alertmanager'; diff --git a/public/app/features/alerting/unified/components/notification-policies/permissions.ts b/public/app/features/alerting/unified/components/notification-policies/permissions.ts index c685357df3e..bcbcae681ab 100644 --- a/public/app/features/alerting/unified/components/notification-policies/permissions.ts +++ b/public/app/features/alerting/unified/components/notification-policies/permissions.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; /** * List of granular permissions that allow viewing notification policies diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx index 08fb0f52c8e..89b8886fb4e 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx @@ -9,8 +9,8 @@ import * as analytics from '../../Analytics'; import { NewRuleFromPanelButton } from './NewRuleFromPanelButton'; -jest.mock('app/types', () => { - const original = jest.requireActual('app/types'); +jest.mock('app/types/store', () => { + const original = jest.requireActual('app/types/store'); return { ...original, useSelector: jest.fn(), diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx index 714f42a9a8a..ccd057e4526 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { Alert, Button, LinkButton } from '@grafana/ui'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { LogMessages, logInfo } from '../../Analytics'; import { panelToRuleFormValues } from '../../utils/rule-form'; diff --git a/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx b/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx index 12fefb5a4d3..9ec9d6f9b03 100644 --- a/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx @@ -4,7 +4,7 @@ import { Trans, t } from '@grafana/i18n'; import { Alert, Button, LinkButton, Stack } from '@grafana/ui'; import { useCleanup } from 'app/core/hooks/useCleanup'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { updateAlertManagerConfigAction } from '../../state/actions'; diff --git a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx index 3929c478053..e1a1aa5aeba 100644 --- a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx index e402c4f8875..e65dd9f0f38 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { DeepMap, FieldError, FieldErrors, useFormContext } from 'react-hook-form'; import { Field, SecretInput } from '@grafana/ui'; -import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types'; +import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types/alerting'; import { ChannelValues, diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx index 7e47f08b5a2..93dfa812d85 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -7,7 +7,7 @@ import { Controller, FieldErrors, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Alert, Button, Field, Select, Stack, Text, useStyles2 } from '@grafana/ui'; -import { NotificationChannelOption } from 'app/types'; +import { NotificationChannelOption } from 'app/types/alerting'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx index 937c64f4afb..af23e5dc423 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx @@ -13,7 +13,7 @@ import { import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertmanagerConfigBuilder, setupMswServer } from '../../../mockApi'; import { grantUserPermissions } from '../../../mocks'; diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index 5a7a4d72d4b..44da5c3588b 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -15,7 +15,7 @@ import { GrafanaManagedReceiverConfig, TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { alertmanagerApi } from '../../../api/alertmanagerApi'; import { testReceiversAction } from '../../../state/actions'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx index 62db9294dc3..4bad7a8639f 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx @@ -14,7 +14,7 @@ import { TextArea, useStyles2, } from '@grafana/ui'; -import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types'; +import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types/alerting'; import { KeyValueMapInput } from './KeyValueMapInput'; import { StringArrayInput } from './StringArrayInput'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx index 00533179299..02177df1bae 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx @@ -3,7 +3,7 @@ import { DeepMap, FieldError, useFormContext } from 'react-hook-form'; import { Trans, t } from '@grafana/i18n'; import { Button, useStyles2 } from '@grafana/ui'; import { useControlledFieldArray } from 'app/features/alerting/unified/hooks/useControlledFieldArray'; -import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types'; +import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types/alerting'; import { ActionIcon } from '../../../rules/ActionIcon'; import { CollapsibleSection } from '../CollapsibleSection'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx index e836f4522f8..32118e6bc4c 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx @@ -3,7 +3,7 @@ import { DeepMap, FieldError, useFormContext } from 'react-hook-form'; import { Trans, t } from '@grafana/i18n'; import { Button, useStyles2 } from '@grafana/ui'; -import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types'; +import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types/alerting'; import { ActionIcon } from '../../../rules/ActionIcon'; 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 e8a58495887..74f574ac549 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 @@ -9,7 +9,8 @@ import { AlertmanagerProvider } from 'app/features/alerting/unified/state/Alertm import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { DEFAULT_TEMPLATES } from 'app/features/alerting/unified/utils/template-constants'; -import { AccessControlAction, NotificationChannelOption } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { NotificationChannelOption } from 'app/types/alerting'; import { TemplatesPicker, getTemplateOptions } from './TemplateSelector'; import { parseTemplates } from './utils'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx index de3cd0e3503..6cfea24cd83 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx @@ -28,7 +28,7 @@ import { useNotificationTemplates, } from 'app/features/alerting/unified/components/contact-points/useNotificationTemplates'; import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; -import { NotificationChannelOption } from 'app/types'; +import { NotificationChannelOption } from 'app/types/alerting'; import { defaultPayloadString } from '../../TemplateForm'; diff --git a/public/app/features/alerting/unified/components/receivers/form/notifiers.ts b/public/app/features/alerting/unified/components/receivers/form/notifiers.ts index 15baf45aabe..d84e8bc2f9d 100644 --- a/public/app/features/alerting/unified/components/receivers/form/notifiers.ts +++ b/public/app/features/alerting/unified/components/receivers/form/notifiers.ts @@ -1,6 +1,6 @@ import { ReactElement } from 'react'; -import { NotifierDTO } from '../../../../../../types'; +import { NotifierDTO } from 'app/types/alerting'; export interface NotifierMetadata { enabled: boolean; diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx index b30545e604b..2f84d394b9f 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx @@ -6,10 +6,10 @@ import { t } from '@grafana/i18n'; import { isFetchError } from '@grafana/runtime'; import { Badge } from '@grafana/ui'; import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config'; +import { NotifierDTO } from 'app/types/alerting'; import { useAppNotification } from '../../../../../../../core/copy/appNotification'; import { Receiver } from '../../../../../../../plugins/datasource/alertmanager/types'; -import { NotifierDTO } from '../../../../../../../types'; import { ONCALL_INTEGRATION_V2_FEATURE, onCallApi } from '../../../../api/onCallApi'; import { usePluginBridge } from '../../../../hooks/usePluginBridge'; import { option } from '../../../../utils/notifier-types'; diff --git a/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx b/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx index ee9992d4f1b..26d32af3104 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { Icon, Text, useStyles2 } from '@grafana/ui'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { makeDashboardLink, makePanelLink } from '../../utils/misc'; diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx index c2d7fd5b240..d56b5b5ea01 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -17,7 +17,7 @@ import { setupDataSources } from 'app/features/alerting/unified/testSetup/dataso import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; import { MANUAL_ROUTING_KEY, SIMPLIFIED_QUERY_EDITOR_KEY } from 'app/features/alerting/unified/utils/rule-form'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grafanaRulerGroup } from '../../../../mocks/grafanaRulerApi'; diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/utils.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/utils.ts index 6ad5dac7d73..138db1979bf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/utils.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/utils.ts @@ -2,7 +2,7 @@ import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceJsonData } from '@grafana/schema/dist/esm/index'; import { contextSrv } from 'app/core/core'; import { ExpressionDatasourceUID } from 'app/features/expressions/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { RuleFormType } from '../../../types/rule-form'; diff --git a/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts b/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts index 27bfb98c6a8..bf84e8eb110 100644 --- a/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts +++ b/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts @@ -1,6 +1,7 @@ import memoizeOne from 'memoize-one'; -import { DashboardDTO } from '../../../../../types'; +import { DashboardDTO } from 'app/types/dashboard'; + import { DashboardModel } from '../../../../dashboard/state/DashboardModel'; import { dashboardApi } from '../../api/dashboardApi'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx index 77e751238cf..0ad498ebc65 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx @@ -7,7 +7,7 @@ import appEvents from 'app/core/app_events'; import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule'; import MoreButton from 'app/features/alerting/unified/components/MoreButton'; import { useRulePluginLinkExtension } from 'app/features/alerting/unified/plugins/useRulePluginLinkExtensions'; -import { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting'; +import { EditableRuleIdentifier, Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting'; import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { @@ -18,7 +18,13 @@ import { } from '../../hooks/useAbilities'; import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc'; import * as ruleId from '../../utils/rule-id'; -import { prometheusRuleType, rulerRuleType } from '../../utils/rules'; +import { + getRuleUID, + isEditableRuleIdentifier, + isPausedRule, + prometheusRuleType, + rulerRuleType, +} from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton'; @@ -28,7 +34,7 @@ interface Props { identifier: RuleIdentifier; groupIdentifier: RuleGroupIdentifierV2; handleSilence: () => void; - handleDelete: (rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifierV2) => void; + handleDelete: (identifier: EditableRuleIdentifier, groupIdentifier: RuleGroupIdentifierV2) => void; handleDuplicateRule: (identifier: RuleIdentifier) => void; onPauseChange?: () => void; buttonSize?: ComponentSize; @@ -118,10 +124,22 @@ const AlertRuleMenu = ({ const showDivider = [canPause, canSilence, shouldShowDeclareIncidentButton, canDuplicate].some(Boolean) && [canExport].some(Boolean); + // grab the UID from either rulerRule or promRule + const ruleUid = getRuleUID(rulerRule ?? promRule); + + const isPaused = + (rulerRuleType.grafana.rule(rulerRule) && isPausedRule(rulerRule)) || + (prometheusRuleType.grafana.rule(promRule) && promRule.isPaused); + const menuItems = ( <> - {canPause && rulerRuleType.grafana.rule(rulerRule) && groupIdentifier.groupOrigin === 'grafana' && ( - + {canPause && ruleUid && groupIdentifier.groupOrigin === 'grafana' && ( + )} {canSilence && ( )} - {canDelete && rulerRule && ( + {canDelete && ( <> handleDelete(rulerRule, groupIdentifier)} + onClick={() => { + // if the identifier is not for a editable rule I wonder how you even got here. + if (isEditableRuleIdentifier(identifier)) { + handleDelete(identifier, groupIdentifier); + } + }} /> )} diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx index a3125ccca53..cf642620c14 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx @@ -5,7 +5,7 @@ import { byLabelText, byRole, byText } from 'testing-library-selector'; import { setPluginLinksHook } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; import { diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index 5a4b4da2178..7a3f89a2526 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -40,10 +40,10 @@ import { useRuleGroupConsistencyCheck } from '../../hooks/usePrometheusConsisten import { useReturnTo } from '../../hooks/useReturnTo'; import { PluginOriginBadge } from '../../plugins/PluginOriginBadge'; import { Annotation } from '../../utils/constants'; -import { ruleIdentifierToRuleSourceIdentifier } from '../../utils/datasource'; +import { getRulesSourceUid, ruleIdentifierToRuleSourceIdentifier } from '../../utils/datasource'; import { labelsSize } from '../../utils/labels'; import { makeDashboardLink, makePanelLink, stringifyErrorLike } from '../../utils/misc'; -import { createListFilterLink } from '../../utils/navigation'; +import { createListFilterLink, groups } from '../../utils/navigation'; import { RulePluginOrigin, getRulePluginOrigin, @@ -422,6 +422,11 @@ function usePageNav(rule: CombinedRule) { const isGrafanaRecordingRule = rulerRuleType.grafana.recordingRule(rulerRule); const isRecordingRuleType = prometheusRuleType.recordingRule(promRule); + const dataSourceUID = getRulesSourceUid(rule.namespace.rulesSource); + const namespaceString = getNamespaceString(rule); + + const groupDetailsUrl = groups.detailsPageLink(dataSourceUID, namespaceString, groupName); + const pageNav: NavModelItem = { ...defaultPageNav, text: rule.name, @@ -470,10 +475,7 @@ function usePageNav(rule: CombinedRule) { ], parentItem: { text: groupName, - url: createListFilterLink([ - ['namespace', namespaceName], - ['group', groupName], - ]), + url: groupDetailsUrl, // @TODO support nested folders here parentItem: { text: namespaceName, @@ -523,4 +525,33 @@ function isValidRunbookURL(url: string) { return isRelative || isAbsolute; } +function getNamespaceString(rule: CombinedRule): string { + // try rule.namespace.uid + if (rule.namespace.uid) { + return rule.namespace.uid; + } + + // if datasource managed, use rule.namespace.name; + const isDataSourceManagedRulerRule = rulerRuleType.dataSource.rule(rule.rulerRule); + const isDataSourceManagedPromRule = + prometheusRuleType.rule(rule.promRule) && !prometheusRuleType.grafana.rule(rule.promRule); + + if (isDataSourceManagedRulerRule || isDataSourceManagedPromRule) { + return rule.namespace.name; + } + + // try rulerRule definition if grafana ruler rule; + if (rulerRuleType.grafana.rule(rule.rulerRule)) { + return rule.rulerRule?.grafana_alert.namespace_uid; + } + + // try promRule definition if grafana prom rule; + if (prometheusRuleType.grafana.rule(rule.promRule)) { + return rule.promRule.folderUid; + } + + // fell back to whatever the name is of the namespace assigned to the combined rule + return rule.namespace.name; +} + export default RuleViewer; diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx index 61dc5f119e7..ca70ddaa984 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx @@ -8,7 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Field, Icon, Input, Label, RadioButtonGroup, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; import { diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx index 57725b1975b..5de673ccb14 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx @@ -15,7 +15,7 @@ import { mockPromAlertingRule, } from 'app/features/alerting/unified/mocks'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { setupDataSources } from '../../testSetup/datasources'; diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index 9e4d8c25a46..c6bcd640657 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -1,3 +1,4 @@ +import { isString } from 'lodash'; import { useState } from 'react'; import { Trans, t } from '@grafana/i18n'; @@ -7,7 +8,7 @@ import { useDeleteModal } from 'app/features/alerting/unified/components/rule-vi import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails'; import SilenceGrafanaRuleDrawer from 'app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer'; import { useRulesFilter } from 'app/features/alerting/unified/hooks/useFilteredRules'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { CombinedRule, RuleIdentifier, RulesSource } from 'app/types/unified-alerting'; import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; @@ -16,7 +17,7 @@ import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datas import { groupIdentifier } from '../../utils/groupIdentifier'; import { createViewLink } from '../../utils/misc'; import * as ruleId from '../../utils/rule-id'; -import { rulerRuleType } from '../../utils/rules'; +import { getRuleUID, prometheusRuleType, rulerRuleType } from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; import { RedirectToCloneRule } from './CloneRule'; @@ -104,6 +105,12 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource return null; } + // determine if this rule can be silenced by checking for Grafana Alert rule type and extracting the UID + const ruleUid = getRuleUID(rule.rulerRule ?? rule.promRule); + const silenceableRule = + isString(ruleUid) && + (rulerRuleType.grafana.alertingRule(rule.rulerRule) || prometheusRuleType.grafana.alertingRule(rule.promRule)); + return ( {buttons} @@ -132,8 +139,8 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource buttonSize={buttonSize} /> {deleteModal} - {rulerRuleType.grafana.alertingRule(rule.rulerRule) && showSilenceDrawer && ( - setShowSilenceDrawer(false)} /> + {silenceableRule && showSilenceDrawer && ( + setShowSilenceDrawer(false)} /> )} {redirectToClone?.identifier && ( void; }; @@ -17,32 +16,26 @@ type Props = { * For a given Grafana managed rule, renders a drawer containing silences editor and Alertmanager selection */ const SilenceGrafanaRuleDrawer = React.memo( - ({ rulerRule, onClose }: Props) => { - const { uid } = rulerRule.grafana_alert; - - return ( - - - - - - - - ); - }, - (prevProps, nextProps) => { - return prevProps.rulerRule.grafana_alert.uid === nextProps.rulerRule.grafana_alert.uid; - } + ({ ruleUid, onClose }: Props) => ( + + + + + + + + ), + (prevProps, nextProps) => prevProps.ruleUid === nextProps.ruleUid ); export default SilenceGrafanaRuleDrawer; diff --git a/public/app/features/alerting/unified/components/templates/permissions.ts b/public/app/features/alerting/unified/components/templates/permissions.ts index a92758f1a1d..82bef452e94 100644 --- a/public/app/features/alerting/unified/components/templates/permissions.ts +++ b/public/app/features/alerting/unified/components/templates/permissions.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; /** * List of all permissions that allow templates read/write functionality diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx index a1606965fd2..90485290325 100644 --- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx +++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx @@ -1,13 +1,16 @@ -import { HttpResponse } from 'msw'; +import { uniqueId } from 'lodash'; +import { HttpResponse, http } from 'msw'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { Props } from 'react-virtualized-auto-sizer'; import { render, screen, waitFor, within } from 'test/test-utils'; import { byRole, byTestId } from 'testing-library-selector'; -import { AccessControlAction } from 'app/types'; +import { setPluginLinksHook } from '@grafana/runtime'; +import { AccessControlAction } from 'app/types/accessControl'; +import { GrafanaPromRuleGroupDTO, GrafanaPromRulesResponse } from 'app/types/unified-alerting-dto'; import { setupMswServer } from '../mockApi'; -import { grantUserPermissions, mockRulerGrafanaRule, mockRulerRuleGroup } from '../mocks'; +import { grantUserPermissions, mockGrafanaPromAlertingRule, mockRulerGrafanaRule, mockRulerRuleGroup } from '../mocks'; import { mimirDataSource, setFolderResponse, @@ -38,8 +41,7 @@ const ui = { header: byRole('heading', { level: 1 }), editLink: byRole('link', { name: 'Edit' }), exportButton: byRole('button', { name: 'Export' }), - tableRow: byTestId('row'), - rowsTable: byTestId('dynamic-table'), + ruleItem: byRole('treeitem'), export: { dialog: byRole('dialog', { name: /Drawer title Export .* rules/ }), jsonTab: byRole('tab', { name: /JSON/ }), @@ -50,10 +52,16 @@ const ui = { }, }; -setupMswServer(); +const server = setupMswServer(); describe('GroupDetailsPage', () => { beforeEach(() => { + // mock this... + setPluginLinksHook(() => ({ + links: [], + isLoading: false, + })); + grantUserPermissions([ AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate, @@ -63,25 +71,71 @@ describe('GroupDetailsPage', () => { }); describe('Grafana managed rules', () => { - const rule1 = mockRulerGrafanaRule({ for: '10m' }, { title: 'High CPU Usage' }); - const rule2 = mockRulerGrafanaRule({ for: '5m' }, { title: 'Memory Pressure' }); - const provisionedRule = mockRulerGrafanaRule({ for: '10m' }, { title: 'Provisioned Rule', provenance: 'api' }); + const folder = { uid: 'test-folder-uid', canSave: true, title: 'test-folder-title' } as const; + + const rule1 = mockRulerGrafanaRule({ for: '10m' }, { title: 'High CPU Usage', uid: uniqueId() }); + const rule2 = mockRulerGrafanaRule({ for: '5m' }, { title: 'Memory Pressure', uid: uniqueId() }); + + const provisionedRule = mockRulerGrafanaRule( + { for: '10m' }, + { uid: uniqueId(), title: 'Provisioned Rule', provenance: 'api' } + ); const group = mockRulerRuleGroup({ name: 'test-group-cpu', interval: '3m', rules: [rule1, rule2], }); + const promGroup: GrafanaPromRuleGroupDTO = { + name: group.name, + rules: [ + mockGrafanaPromAlertingRule({ + uid: rule1.grafana_alert.uid, + name: rule1.grafana_alert.title, + }), + mockGrafanaPromAlertingRule({ + uid: rule2.grafana_alert.uid, + name: rule2.grafana_alert.title, + }), + ], + interval: 180, + folderUid: folder.uid, + file: folder.title, + }; const provisionedGroup = mockRulerRuleGroup({ name: 'provisioned-group-cpu', interval: '15m', rules: [provisionedRule], }); + const promProvisionedGroup: GrafanaPromRuleGroupDTO = { + name: provisionedGroup.name, + interval: 900, + rules: [ + mockGrafanaPromAlertingRule({ + uid: provisionedRule.grafana_alert.uid, + name: provisionedRule.grafana_alert.title, + provenance: provisionedRule.grafana_alert.provenance, + }), + ], + folderUid: folder.uid, + file: folder.title, + }; beforeEach(() => { setRulerRuleGroupHandler({ response: HttpResponse.json(group) }); - setFolderResponse({ uid: 'test-folder-uid', canSave: true, title: 'test-folder-title' }); + server.use( + http.get('/api/prometheus/grafana/api/v1/rules', () => + HttpResponse.json({ + status: 'success', + data: { + groups: [promGroup], + }, + }) + ) + ); + + setFolderResponse(folder); setGrafanaRuleGroupExportResolver(({ request }) => { const url = new URL(request.url); return HttpResponse.text( @@ -106,22 +160,24 @@ describe('GroupDetailsPage', () => { '/alerting/grafana/namespaces/test-folder-uid/groups/test-group-cpu/edit?returnTo=%2Falerting%2Fgrafana%2Fnamespaces%2Ftest-folder-uid%2Fgroups%2Ftest-group-cpu%2Fview' ); - const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); - expect(tableRows).toHaveLength(2); + expect(await screen.findByRole('treeitem', { name: rule1.grafana_alert.title })).toBeInTheDocument(); - expect(within(tableRows[0]).getByRole('link', { name: rule1.grafana_alert.title })).toHaveAttribute( + const alertRuleItems = await ui.ruleItem.findAll(); + expect(alertRuleItems).toHaveLength(2); + + // assert rule 1 + expect(within(alertRuleItems[0]).getByRole('link', { name: rule1.grafana_alert.title })).toHaveAttribute( 'href', `/alerting/grafana/${rule1.grafana_alert.uid}/view` ); - expect(tableRows[0]).toHaveTextContent(String(rule1.for)); - expect(tableRows[0]).toHaveTextContent('5'); + expect(alertRuleItems[0]).toHaveTextContent(rule1.grafana_alert.title); - expect(within(tableRows[1]).getByRole('link', { name: rule2.grafana_alert.title })).toHaveAttribute( + // assert rule 2 + expect(within(alertRuleItems[1]).getByRole('link', { name: rule2.grafana_alert.title })).toHaveAttribute( 'href', `/alerting/grafana/${rule2.grafana_alert.uid}/view` ); - expect(tableRows[1]).toHaveTextContent(String(rule2.for)); - expect(tableRows[1]).toHaveTextContent('3'); + expect(alertRuleItems[1]).toHaveTextContent(rule2.grafana_alert.title); }); it('should render error alert when API returns an error', async () => { @@ -164,10 +220,12 @@ describe('GroupDetailsPage', () => { // Act renderGroupDetailsPage('grafana', 'test-folder-uid', group.name); - const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); + // Wait until rule items are rendered + expect(await screen.findByRole('treeitem', { name: rule1.grafana_alert.title })).toBeInTheDocument(); + const alertRuleItems = ui.ruleItem.getAll(); // Assert - expect(tableRows).toHaveLength(2); + expect(alertRuleItems).toHaveLength(2); expect(ui.editLink.query()).not.toBeInTheDocument(); // Edit button should not be present }); @@ -177,24 +235,39 @@ describe('GroupDetailsPage', () => { // Act renderGroupDetailsPage('grafana', 'test-folder-uid', group.name); - const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); + // wait for rule items to render + expect(await screen.findByRole('treeitem', { name: rule1.grafana_alert.title })).toBeInTheDocument(); + + const alertRuleItems = await ui.ruleItem.findAll(); // Assert - expect(tableRows).toHaveLength(2); + expect(alertRuleItems).toHaveLength(2); expect(ui.editLink.query()).not.toBeInTheDocument(); // Edit button should not be present }); it('should not allow editing if the group is provisioned', async () => { setRulerRuleGroupHandler({ response: HttpResponse.json(provisionedGroup) }); + server.use( + http.get('/api/prometheus/grafana/api/v1/rules', () => + HttpResponse.json({ + status: 'success', + data: { + groups: [promProvisionedGroup], + }, + }) + ) + ); // Act renderGroupDetailsPage('grafana', 'test-folder-uid', provisionedGroup.name); + // wait for rule items to render + expect(await screen.findByRole('treeitem', { name: provisionedRule.grafana_alert.title })).toBeInTheDocument(); - const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); + const alertRuleItems = await ui.ruleItem.findAll(); // Assert - expect(tableRows).toHaveLength(1); - expect(tableRows[0]).toHaveTextContent('Provisioned Rule'); + expect(alertRuleItems).toHaveLength(1); + expect(alertRuleItems[0]).toHaveTextContent('Provisioned Rule'); expect(ui.editLink.query()).not.toBeInTheDocument(); expect(ui.exportButton.query()).toBeInTheDocument(); }); diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx index f05e9c1a19d..997b1bd44b7 100644 --- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx +++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx @@ -1,35 +1,28 @@ import { skipToken } from '@reduxjs/toolkit/query'; -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; import { Trans, t } from '@grafana/i18n'; -import { Alert, Badge, Button, LinkButton, Text, TextLink, withErrorBoundary } from '@grafana/ui'; +import { Alert, Button, Dropdown, Icon, LinkButton, Menu, TextLink, withErrorBoundary } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; -import { FolderDTO } from 'app/types'; -import { GrafanaRulesSourceSymbol, RuleGroup } from 'app/types/unified-alerting'; -import { PromRuleType, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; +import { FolderDTO } from 'app/types/folders'; +import { GrafanaRulesSourceSymbol } from 'app/types/unified-alerting'; +import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { alertRuleApi } from '../api/alertRuleApi'; import { RulesSourceFeatures, featureDiscoveryApi } from '../api/featureDiscoveryApi'; import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; -import { DynamicTable, DynamicTableColumnProps } from '../components/DynamicTable'; import { GrafanaRuleGroupExporter } from '../components/export/GrafanaRuleGroupExporter'; import { useFolder } from '../hooks/useFolder'; import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../rule-editor/formDefaults'; -import { createViewLinkFromIdentifier } from '../rule-list/DataSourceRuleListItem'; +import { DataSourceGroupLoader } from '../rule-list/DataSourceGroupLoader'; +import { GrafanaGroupLoader } from '../rule-list/GrafanaGroupLoader'; import { useRulesAccess } from '../utils/accessControlHooks'; import { GRAFANA_RULES_SOURCE_NAME, getDataSourceByUid } from '../utils/datasource'; import { makeFolderLink, stringifyErrorLike } from '../utils/misc'; import { createListFilterLink, groups } from '../utils/navigation'; -import { fromRule, fromRulerRule } from '../utils/rule-id'; -import { - calcRuleEvalsToStartAlerting, - getRuleName, - isFederatedRuleGroup, - isProvisionedRuleGroup, - rulerRuleType, -} from '../utils/rules'; -import { formatPrometheusDuration, safeParsePrometheusDuration } from '../utils/time'; +import { isFederatedRuleGroup, isProvisionedRuleGroup } from '../utils/rules'; +import { formatPrometheusDuration } from '../utils/time'; import { Title } from './Title'; @@ -111,6 +104,7 @@ function GroupDetailsPage() { }, }} renderTitle={(title) => } + subTitle={t('alerting.titles.group-view.subtitle', 'Manage alert rules, recording rules and evaluation interval')} info={[ { label: namespaceLabel, value: namespaceValue }, { label: t('alerting.group-details.interval', 'Interval'), value: groupInterval }, @@ -150,13 +144,33 @@ function GroupDetailsPage() { <div>{stringifyErrorLike(ruleNamespacesError || ruleGroupError)}</div> </Alert> )} - {promGroup && ruleSourceName && ( - <GroupDetails group={promRuleGroupToRuleGroupDetails(ruleSourceName, namespaceName, promGroup)} /> - )} - {rulerGroup && ruleSourceName && ( - <GroupDetails group={rulerRuleGroupToRuleGroupDetails(ruleSourceName, namespaceName, rulerGroup)} /> - )} {!promGroup && !rulerGroup && <EntityNotFound entity={`${namespaceId}/${groupName}`} />} + + {ruleSourceName && ( + <ul role="tree"> + {isGrafanaRuleGroup ? ( + <GrafanaGroupLoader + groupIdentifier={{ groupName, groupOrigin: 'grafana', namespace: { uid: namespaceId } }} + namespaceName={namespaceName} + /> + ) : ( + <DataSourceGroupLoader + groupIdentifier={{ + groupName, + groupOrigin: 'datasource', + namespace: { + name: namespaceName, + }, + rulesSource: { + name: ruleSourceName, + uid: dataSourceUid, + ruleSourceType: 'datasource', + }, + }} + /> + )} + </ul> + )} </> </AlertingPageWrapper> ); @@ -195,13 +209,39 @@ function GroupActions({ dsFeatures, namespaceId, groupName, folder, rulerGroup } </Button> )} {canEdit && ( - <LinkButton - icon="pen" - href={groups.editPageLink(dsFeatures.uid, namespaceId, groupName, { includeReturnTo: true })} - variant="secondary" - > - <Trans i18nKey="alerting.group-details.edit">Edit</Trans> - </LinkButton> + <> + <LinkButton + icon="pen" + href={groups.editPageLink(dsFeatures.uid, namespaceId, groupName, { includeReturnTo: true })} + variant="secondary" + > + <Trans i18nKey="alerting.group-details.edit">Edit</Trans> + </LinkButton> + {/* Data source managed requires different URLs, a hassle to implement for now */} + {isGrafanaSource && ( + <Dropdown + overlay={ + <Menu> + <Menu.Item + icon="bell" + url={groups.newAlertRuleLink(folder?.title, folder?.uid, groupName)} + label={t('alerting.alert-rule.term', 'Alert rule')} + /> + <Menu.Item + icon="record-audio" + url={groups.newRecordingRuleLink(folder?.title, folder?.uid, groupName)} + label={t('alerting.recording-rule.term', 'Recording rule')} + /> + </Menu> + } + > + <Button variant="primary"> + {t('alerting.group-details.new', 'New')} + <Icon name="angle-down" /> + </Button> + </Dropdown> + )} + </> )} {folder && isExporting && ( <GrafanaRuleGroupExporter folderUid={folder.uid} groupName={groupName} onClose={() => setIsExporting(false)} /> @@ -210,158 +250,4 @@ function GroupActions({ dsFeatures, namespaceId, groupName, folder, rulerGroup } ); } -/** An common interface for both Prometheus and Ruler rule groups */ -interface RuleGroupDetails { - name: string; - interval: string; - rules: RuleDetails[]; -} - -interface AlertingRuleDetails { - name: string; - href?: string; - type: 'alerting'; - pendingPeriod: string; - evaluationsToFire: number; -} -interface RecordingRuleDetails { - name: string; - href?: string; - type: 'recording'; -} - -type RuleDetails = AlertingRuleDetails | RecordingRuleDetails; - -interface GroupDetailsProps { - group: RuleGroupDetails; -} - -function GroupDetails({ group }: GroupDetailsProps) { - return ( - <div> - <RulesTable rules={group.rules} /> - </div> - ); -} - -function RulesTable({ rules }: { rules: RuleDetails[] }) { - const rows = rules.map((rule: RuleDetails, index) => ({ - id: index, - data: rule, - })); - - const columns: Array<DynamicTableColumnProps<RuleDetails>> = useMemo(() => { - return [ - { - id: 'alertName', - label: t('alerting.group-details.rule-name', 'Rule name'), - renderCell: ({ data: { name, href } }) => { - if (href) { - return ( - <TextLink href={href} inline={false} color="primary"> - {name} - </TextLink> - ); - } - - return <Text truncate>{name}</Text>; - }, - size: 0.4, - }, - { - id: 'for', - label: t('alerting.group-details.pending-period', 'Pending period'), - renderCell: ({ data }) => { - switch (data.type) { - case 'alerting': - return <>{data.pendingPeriod}</>; - case 'recording': - return <Badge text={t('alerting.group-details.recording', 'Recording')} color="purple" />; - } - }, - size: 0.3, - }, - { - id: 'numberEvaluations', - label: t('alerting.group-details.evaluations-to-fire', 'Evaluation cycles to fire'), - renderCell: ({ data }) => { - switch (data.type) { - case 'alerting': - return <>{data.evaluationsToFire}</>; - case 'recording': - return null; - } - }, - size: 0.3, - }, - ]; - }, []); - - return <DynamicTable items={rows} cols={columns} />; -} - -function promRuleGroupToRuleGroupDetails( - ruleSourceName: string, - namespaceName: string, - group: RuleGroup -): RuleGroupDetails { - const groupIntervalMs = group.interval * 1000; - - return { - name: group.name, - interval: formatPrometheusDuration(group.interval * 1000), - rules: group.rules.map<RuleDetails>((rule) => { - const ruleIdentifier = fromRule(ruleSourceName, namespaceName, group.name, rule); - const href = ruleIdentifier ? createViewLinkFromIdentifier(ruleIdentifier) : undefined; - - switch (rule.type) { - case PromRuleType.Alerting: - return { - name: rule.name, - href, - type: 'alerting', - pendingPeriod: formatPrometheusDuration(rule.duration ? rule.duration * 1000 : 0), - evaluationsToFire: calcRuleEvalsToStartAlerting(rule.duration ? rule.duration * 1000 : 0, groupIntervalMs), - }; - case PromRuleType.Recording: - return { name: rule.name, href, type: 'recording' }; - } - }), - }; -} - -function rulerRuleGroupToRuleGroupDetails( - ruleSourceName: string, - namespaceName: string, - group: RulerRuleGroupDTO -): RuleGroupDetails { - const groupIntervalMs = safeParsePrometheusDuration(group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); - - return { - name: group.name, - interval: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, - rules: group.rules.map<RuleDetails>((rule) => { - const name = getRuleName(rule); - - const ruleIdentifier = fromRulerRule(ruleSourceName, namespaceName, group.name, rule); - const href = createViewLinkFromIdentifier(ruleIdentifier); - - if (rulerRuleType.any.alertingRule(rule)) { - return { - name, - href, - type: 'alerting', - pendingPeriod: rule.for ?? '0s', - evaluationsToFire: calcRuleEvalsToStartAlerting( - rule.for ? safeParsePrometheusDuration(rule.for) : 0, - groupIntervalMs - ), - }; - } - - return { name, href, type: 'recording' }; - }), - }; -} - export default withErrorBoundary(GroupDetailsPage, { style: 'page' }); diff --git a/public/app/features/alerting/unified/group-details/GroupEditPage.test.tsx b/public/app/features/alerting/unified/group-details/GroupEditPage.test.tsx index 2240d26dc55..bafd72ddedd 100644 --- a/public/app/features/alerting/unified/group-details/GroupEditPage.test.tsx +++ b/public/app/features/alerting/unified/group-details/GroupEditPage.test.tsx @@ -5,7 +5,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { locationService } from '@grafana/runtime'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { setupMswServer } from '../mockApi'; diff --git a/public/app/features/alerting/unified/group-details/GroupEditPage.tsx b/public/app/features/alerting/unified/group-details/GroupEditPage.tsx index 1a2ece20bc4..a68f230d8cb 100644 --- a/public/app/features/alerting/unified/group-details/GroupEditPage.tsx +++ b/public/app/features/alerting/unified/group-details/GroupEditPage.tsx @@ -20,7 +20,7 @@ import { } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { GrafanaRulesSourceSymbol, RuleGroupIdentifierV2, RulerDataSourceConfig } from 'app/types/unified-alerting'; import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useDeleteRuleFromGroup.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/useDeleteRuleFromGroup.test.tsx index 538d02d1a38..552c401b6d0 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useDeleteRuleFromGroup.test.tsx +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useDeleteRuleFromGroup.test.tsx @@ -3,7 +3,7 @@ import { render } from 'test/test-utils'; import { byRole, byText } from 'testing-library-selector'; import server from '@grafana/test-utils/server'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule } from 'app/types/unified-alerting'; import { setupMswServer } from '../../mockApi'; diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx index 1b7f1f89db3..df57efb9ac5 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx +++ b/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx @@ -3,7 +3,7 @@ import { HttpResponse } from 'msw'; import { render } from 'test/test-utils'; import { byRole, byText } from 'testing-library-selector'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; import { setupMswServer } from '../../mockApi'; diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleGroup.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleGroup.test.tsx index 4d5fa6d556f..e74178297fb 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleGroup.test.tsx +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleGroup.test.tsx @@ -1,7 +1,7 @@ import { render } from 'test/test-utils'; import { byRole, byText } from 'testing-library-selector'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RuleGroupIdentifier } from 'app/types/unified-alerting'; import { setupMswServer } from '../../mockApi'; diff --git a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx index a7b550e0b68..3fdd05e6c63 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx +++ b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx @@ -5,7 +5,7 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule } from 'app/types/unified-alerting'; import { getCloudRule, getGrafanaRule, grantUserPermissions, mockDataSource } from '../mocks'; diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 91fbe9cc603..5dd43d6db63 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -12,7 +12,7 @@ import { } from 'app/features/alerting/unified/components/notification-policies/permissions'; import { useFolder } from 'app/features/alerting/unified/hooks/useFolder'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule, RuleGroupIdentifierV2 } from 'app/types/unified-alerting'; import { GrafanaPromRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/hooks/useFolder.ts b/public/app/features/alerting/unified/hooks/useFolder.ts index 7b06bf92020..804c9c12b25 100644 --- a/public/app/features/alerting/unified/hooks/useFolder.ts +++ b/public/app/features/alerting/unified/hooks/useFolder.ts @@ -1,7 +1,7 @@ import { skipToken } from '@reduxjs/toolkit/query/react'; import { useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; interface ReturnBag { folder?: FolderDTO; diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx index 36d2d6f06d1..ff0c9b61a1c 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx @@ -5,7 +5,8 @@ import { Provider } from 'react-redux'; import { contextSrv } from 'app/core/services/context_srv'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { configureStore } from 'app/store/configureStore'; -import { AccessControlAction, FolderDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { FolderDTO } from 'app/types/folders'; import { setupMswServer } from '../mockApi'; import { mockDataSource, mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks'; diff --git a/public/app/features/alerting/unified/hooks/useManagedAlertStateHistory.ts b/public/app/features/alerting/unified/hooks/useManagedAlertStateHistory.ts index e7902fcf507..8230954063c 100644 --- a/public/app/features/alerting/unified/hooks/useManagedAlertStateHistory.ts +++ b/public/app/features/alerting/unified/hooks/useManagedAlertStateHistory.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { StateHistoryItem } from 'app/types/unified-alerting'; import { fetchGrafanaAnnotationsAction } from '../state/actions'; diff --git a/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts b/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts index 5e6daa81e8b..ed65c6ea4a9 100644 --- a/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts +++ b/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; import { UnifiedAlertingState } from '../state/reducers'; diff --git a/public/app/features/alerting/unified/mockApi.ts b/public/app/features/alerting/unified/mockApi.ts index ae0e9c385b0..614c8235dd0 100644 --- a/public/app/features/alerting/unified/mockApi.ts +++ b/public/app/features/alerting/unified/mockApi.ts @@ -9,7 +9,8 @@ import { setupAlertmanagerStatusMapDefaultState, } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { resetRoutingTreeMap } from 'app/features/alerting/unified/mocks/server/entities/k8s/routingtrees'; -import { DashboardDTO, FolderDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; +import { FolderDTO } from 'app/types/folders'; import { PromRulesResponse, RulerGrafanaRuleDTO, diff --git a/public/app/features/alerting/unified/mockGrafanaNotifiers.ts b/public/app/features/alerting/unified/mockGrafanaNotifiers.ts index 08bc5d4d90d..fd058176867 100644 --- a/public/app/features/alerting/unified/mockGrafanaNotifiers.ts +++ b/public/app/features/alerting/unified/mockGrafanaNotifiers.ts @@ -1,4 +1,4 @@ -import { GrafanaNotifierType, NotifierDTO } from 'app/types'; +import { GrafanaNotifierType, NotifierDTO } from 'app/types/alerting'; export const grafanaAlertNotifiers: Record<GrafanaNotifierType, NotifierDTO> = { dingding: { diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index e8d3bc2e2ab..0202406bf8e 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -26,7 +26,11 @@ import { SilenceState, } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; -import { AccessControlAction, DashboardDTO, FolderDTO, NotifiersState, ReceiversState, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { NotifiersState, ReceiversState } from 'app/types/alerting'; +import { DashboardDTO } from 'app/types/dashboard'; +import { FolderDTO } from 'app/types/folders'; +import { StoreState } from 'app/types/store'; import { Alert, AlertingRule, diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts index f21833e4866..3e3043d9331 100644 --- a/public/app/features/alerting/unified/mocks/server/configure.ts +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -22,7 +22,7 @@ import { import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { RulerDataSourceConfig } from 'app/types/unified-alerting'; import { GrafanaPromRuleGroupDTO, PromRuleGroupDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/mocks/server/db.ts b/public/app/features/alerting/unified/mocks/server/db.ts index a100519b4b4..05cbf225981 100644 --- a/public/app/features/alerting/unified/mocks/server/db.ts +++ b/public/app/features/alerting/unified/mocks/server/db.ts @@ -4,7 +4,7 @@ import { uniqueId } from 'lodash'; import { DataSourceInstanceSettings, PluginType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { GrafanaManagedContactPoint, GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { GrafanaAlertStateDecision, GrafanaAlertingRuleDefinition, diff --git a/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts b/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts index 9e73c77f671..3a79aa4824d 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts @@ -1,7 +1,7 @@ import { HttpResponse, http } from 'msw'; import { Description, ResourcePermission } from 'app/core/components/AccessControl/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; // TODO: Expand this out to more realistic use cases as we work on RBAC for contact points const resourceDescriptionsMap: Record<string, Description> = { diff --git a/public/app/features/alerting/unified/mocks/server/handlers/folders.ts b/public/app/features/alerting/unified/mocks/server/handlers/folders.ts index a1599b815e1..7a295222077 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/folders.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/folders.ts @@ -2,7 +2,7 @@ import { HttpResponse, http } from 'msw'; import { mockFolder } from 'app/features/alerting/unified/mocks'; import { grafanaRulerRule } from 'app/features/alerting/unified/mocks/grafanaRulerApi'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; export const DEFAULT_FOLDERS: FolderDTO[] = [ mockFolder({ diff --git a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx index fd04f5c3b8e..8bd3b28e933 100644 --- a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx @@ -5,7 +5,7 @@ import { byRole, byTestId } from 'testing-library-selector'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { RulerAlertingRuleDTO, diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx index 86d92dac4fe..d21d660cc66 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx @@ -3,7 +3,7 @@ import { screen } from 'test/test-utils'; import { byText } from 'testing-library-selector'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromApiFeatures, PromApplication } from 'app/types/unified-alerting-dto'; import { discoverFeaturesByUid } from '../api/buildInfo'; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx index ac0668721cb..7e823431f87 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx @@ -3,7 +3,7 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { screen } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; import { setupMswServer } from '../mockApi'; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx index 9a99b587bcd..eab7a703499 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx @@ -6,7 +6,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/configure'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx index fedb898f955..632904914b1 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx @@ -8,7 +8,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks'; import { grafanaRulerGroup, grafanaRulerGroup2, grafanaRulerRule } from '../mocks/grafanaRulerApi'; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx index 19166c01ea4..cd1f40deb43 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx @@ -5,7 +5,7 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { byText } from 'testing-library-selector'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RecordingRuleEditorProps } from '../components/rule-editor/RecordingRuleEditor'; import { grantUserPermissions } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx index c1fe5eeec29..2c8411e09a9 100644 --- a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx @@ -3,7 +3,7 @@ import { byRole } from 'testing-library-selector'; import { DataSourceInstanceSettings } from '@grafana/data'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { DataSourceRuleGroupIdentifier, DataSourceRulesSourceIdentifier } from 'app/types/unified-alerting'; import { PromRuleGroupDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx index b9120e57eb1..b37e08dda81 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -2,7 +2,7 @@ import { mockIntersectionObserver } from 'jsdom-testing-mocks'; import { act, render, screen, waitForElementToBeRemoved } from 'test/test-utils'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx index 68913940e8e..3169996b88c 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx @@ -2,7 +2,7 @@ import { render } from 'test/test-utils'; import { byRole, byTitle } from 'testing-library-selector'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; import { GrafanaPromRuleDTO, @@ -40,6 +40,7 @@ const ui = { copyLink: () => byRole('menuitem', { name: /copy link/i }), export: () => byRole('menuitem', { name: /export/i }), delete: () => byRole('menuitem', { name: /delete/i }), + pause: () => byRole('menuitem', { name: /pause/i }), }, }; @@ -188,7 +189,7 @@ describe('GrafanaGroupLoader', () => { const menu = byRole('menu').get(); expect(menu).toBeInTheDocument(); - // With proper permissions, all 4 menu actions should be available: + // With proper permissions, all 6 menu actions should be available: // 1. Silence notifications - available for alerting rules (AlertingSilenceCreate permission) expect(ui.menuItems.silence().get()).toBeInTheDocument(); @@ -202,9 +203,15 @@ describe('GrafanaGroupLoader', () => { // 4. Export - should be available for Grafana alerting rules (AlertingRuleRead permission) expect(ui.menuItems.export().get()).toBeInTheDocument(); - // Verify that the menu contains all 4 expected menu items + // 5. Delete - should be available for Grafana alerting rules (AlertingRuleDelete permission) + expect(ui.menuItems.delete().get()).toBeInTheDocument(); + + // 6. Pause - should be available for Grafana alerting rules (AlertingRuleUpdate permission) + expect(ui.menuItems.pause().get()).toBeInTheDocument(); + + // Verify that the menu contains all 6 expected menu items const menuItems = byRole('menuitem').getAll(); - expect(menuItems.length).toBe(4); + expect(menuItems.length).toBe(6); }); }); diff --git a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx index 361d017b4d2..63723b06bdd 100644 --- a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor, within } from 'test/test-utils'; import { byRole } from 'testing-library-selector'; import { setPluginComponentsHook, setPluginLinksHook, setReturnToPreviousHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx index d9df8e007d4..8e80dd0eff3 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx @@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n'; import { logInfo } from '@grafana/runtime'; import { Button, LinkButton, Stack } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; import { LogMessages, trackRuleListNavigation } from '../Analytics'; diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx index e29d4c2f65b..1f2f4c0b02e 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx @@ -2,7 +2,7 @@ import { render } from 'test/test-utils'; import { byRole, byTestId } from 'testing-library-selector'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx index db56bb9af9f..e8abce41ed8 100644 --- a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx @@ -17,6 +17,7 @@ export function AlertRuleListItemSkeleton() { description={<Skeleton width={256} />} actions={<RuleActionsSkeleton />} data-testid="alert-rule-list-item-loader" + aria-disabled={true} /> ); } diff --git a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx index 37e56db4058..4570b3dd8e9 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx @@ -1,3 +1,4 @@ +import { isString } from 'lodash'; import { useState } from 'react'; import { RequireAtLeastOne } from 'type-fest'; @@ -19,7 +20,13 @@ import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { logWarning } from '../../Analytics'; import { AlertRuleAction, skipToken, useGrafanaPromRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities'; import * as ruleId from '../../utils/rule-id'; -import { isProvisionedPromRule, isProvisionedRule, prometheusRuleType, rulerRuleType } from '../../utils/rules'; +import { + getRuleUID, + isProvisionedPromRule, + isProvisionedRule, + prometheusRuleType, + rulerRuleType, +} from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; type RuleProps = RequireAtLeastOne<{ @@ -69,6 +76,12 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }: return null; } + // determine if this rule can be silenced by checking for Grafana Alert rule type and extracting the UID + const ruleUid = getRuleUID(rule ?? promRule); + const silenceableRule = + isString(ruleUid) && + (rulerRuleType.grafana.alertingRule(rule) || prometheusRuleType.grafana.alertingRule(promRule)); + if (canEditRule) { const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`); @@ -96,13 +109,13 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }: promRule={promRule} groupIdentifier={groupIdentifier} identifier={identifier} - handleDelete={() => showDeleteModal(identifier, groupIdentifier)} + handleDelete={(identifier, groupIdentifier) => showDeleteModal(identifier, groupIdentifier)} handleSilence={() => setShowSilenceDrawer(true)} handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })} /> {deleteModal} - {rulerRuleType.grafana.alertingRule(rule) && showSilenceDrawer && ( - <SilenceGrafanaRuleDrawer rulerRule={rule} onClose={() => setShowSilenceDrawer(false)} /> + {silenceableRule && showSilenceDrawer && ( + <SilenceGrafanaRuleDrawer ruleUid={ruleUid} onClose={() => setShowSilenceDrawer(false)} /> )} {redirectToClone?.identifier && ( <RedirectToCloneRule diff --git a/public/app/features/alerting/unified/settings/extensions.ts b/public/app/features/alerting/unified/settings/extensions.ts new file mode 100644 index 00000000000..c6cb202849f --- /dev/null +++ b/public/app/features/alerting/unified/settings/extensions.ts @@ -0,0 +1,50 @@ +import { useLocation } from 'react-router-dom-v5-compat'; + +import { NavModelItem } from '@grafana/data'; +import { useSelector } from 'app/types/store'; + +type SettingsSectionUrl = `/alerting/admin/${string}`; +type SettingsSectionNav = Pick<NavModelItem, 'id' | 'text' | 'icon'> & { + url: SettingsSectionUrl; +}; + +const settingsExtensions: Map<SettingsSectionUrl, { nav: SettingsSectionNav }> = new Map(); + +/** + * Registers a new settings section that will appear as a tab in the alerting settings page. + * @param pageNav - The navigation configuration for the settings section + */ +export function addSettingsSection(pageNav: SettingsSectionNav) { + if (settingsExtensions.has(pageNav.url)) { + console.warn('Unable to add settings page, PageNav must have an unique url'); + return; + } + settingsExtensions.set(pageNav.url, { nav: pageNav }); +} + +/** + * Returns the navigation configuration for all settings extensions. + */ +export function useSettingsExtensionsNav(): NavModelItem[] { + const location = useLocation(); + + const navIndex = useSelector((state) => state.navIndex); + const settingsNav = navIndex['alerting-admin']; + + // Build extension tabs from settingsExtensions + const extensionTabs: NavModelItem[] = Array.from(settingsExtensions.entries()).map(([url, { nav }]) => ({ + ...nav, + active: location.pathname === url, + url: url, + parentItem: settingsNav, + })); + + return extensionTabs; +} + +/** + * ONLY USE FOR TESTING. Clears all settings extensions. + */ +export function clearSettingsExtensions() { + settingsExtensions.clear(); +} diff --git a/public/app/features/alerting/unified/settings/navigation.test.ts b/public/app/features/alerting/unified/settings/navigation.test.ts new file mode 100644 index 00000000000..c4b4c4678ef --- /dev/null +++ b/public/app/features/alerting/unified/settings/navigation.test.ts @@ -0,0 +1,111 @@ +import { renderHook } from '@testing-library/react'; +import { getWrapper } from 'test/test-utils'; + +import { addSettingsSection, clearSettingsExtensions } from './extensions'; +import { useSettingsPageNav } from './navigation'; + +describe('useSettingsPageNav', () => { + const mockNavIndex = { + 'alerting-admin': { + id: 'alerting-admin', + text: 'Settings', + url: '/alerting/admin', + }, + }; + + const defaultPreloadedState = { + navIndex: mockNavIndex, + }; + + it('should return settings page nav with alertmanager child when no extensions are present', () => { + const wrapper = getWrapper({ + preloadedState: defaultPreloadedState, + renderWithRouter: true, + historyOptions: { + initialEntries: ['/alerting/admin/alertmanager'], + }, + }); + + const { result } = renderHook(() => useSettingsPageNav(), { wrapper }); + + expect(result.current.navId).toBe('alerting'); + + // Check the structure + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav.children).toHaveLength(1); + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav.children).toEqual([ + expect.objectContaining({ + id: 'alertmanager', + text: 'Alert managers', + url: '/alerting/admin/alertmanager', + active: true, + icon: 'cloud', + parentItem: undefined, + }), + ]); + }); + + it('should include extensions when added via addSettingsSection', () => { + // Clear any existing extensions + clearSettingsExtensions(); + + // Add two extensions + addSettingsSection({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + icon: 'star', + }); + + addSettingsSection({ + id: 'notifications', + text: 'Notifications', + url: '/alerting/admin/notifications', + icon: 'bell', + }); + + const wrapper = getWrapper({ + preloadedState: defaultPreloadedState, + renderWithRouter: true, + historyOptions: { + initialEntries: ['/alerting/admin/enrichment'], + }, + }); + + const { result } = renderHook(() => useSettingsPageNav(), { wrapper }); + + expect(result.current.navId).toBe('alerting'); + + // Should have 3 children: alertmanager + 2 extensions + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav.children).toHaveLength(3); + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav.children).toEqual([ + expect.objectContaining({ + id: 'alertmanager', + text: 'Alert managers', + url: '/alerting/admin/alertmanager', + active: false, + icon: 'cloud', + parentItem: undefined, + }), + expect.objectContaining({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + active: true, + icon: 'star', + parentItem: undefined, + }), + expect.objectContaining({ + id: 'notifications', + text: 'Notifications', + url: '/alerting/admin/notifications', + active: false, + icon: 'bell', + parentItem: undefined, + }), + ]); + }); +}); diff --git a/public/app/features/alerting/unified/settings/navigation.ts b/public/app/features/alerting/unified/settings/navigation.ts new file mode 100644 index 00000000000..85513269e1f --- /dev/null +++ b/public/app/features/alerting/unified/settings/navigation.ts @@ -0,0 +1,40 @@ +import { useLocation } from 'react-router-dom-v5-compat'; + +import { NavModelItem } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { useSelector } from 'app/types/store'; + +import { useSettingsExtensionsNav } from './extensions'; + +export function useSettingsPageNav() { + const location = useLocation(); + + const navIndex = useSelector((state) => state.navIndex); + const settingsNav = navIndex['alerting-admin']; + + const extensionTabs = useSettingsExtensionsNav(); + + // All available tabs including the main alertmanager tab + const allTabs: NavModelItem[] = [ + { + id: 'alertmanager', + text: t('alerting.settings.tabs.alert-managers', 'Alert managers'), + url: '/alerting/admin/alertmanager', + active: location.pathname === '/alerting/admin/alertmanager', + icon: 'cloud', + parentItem: settingsNav, + }, + ...extensionTabs, + ]; + + // Create pageNav that represents the Settings page with tabs as children + const pageNav: NavModelItem = { + ...settingsNav, + children: allTabs, + }; + + return { + navId: 'alerting', + pageNav, + }; +} diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index f45b7ff1dde..90ccc17db20 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -9,7 +9,7 @@ import { Receiver, TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { RuleIdentifier, RuleNamespace, StateHistoryItem } from 'app/types/unified-alerting'; import { RulerRuleDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/test/test-utils.ts b/public/app/features/alerting/unified/test/test-utils.ts index 66598dea083..0dea7f9eb58 100644 --- a/public/app/features/alerting/unified/test/test-utils.ts +++ b/public/app/features/alerting/unified/test/test-utils.ts @@ -2,7 +2,7 @@ import { act } from '@testing-library/react'; import { FeatureToggles } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions } from '../mocks'; import { setFolderAccessControl } from '../mocks/server/configure'; diff --git a/public/app/features/alerting/unified/types/receiver-form.ts b/public/app/features/alerting/unified/types/receiver-form.ts index e17aa9d05f0..d2045a2c2a6 100644 --- a/public/app/features/alerting/unified/types/receiver-form.ts +++ b/public/app/features/alerting/unified/types/receiver-form.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types'; -import { CloudNotifierType, NotifierType } from 'app/types'; +import { CloudNotifierType, NotifierType } from 'app/types/alerting'; import { ControlledField } from '../hooks/useControlledFieldArray'; diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts index edda81712ae..0821c92963b 100644 --- a/public/app/features/alerting/unified/utils/access-control.ts +++ b/public/app/features/alerting/unified/utils/access-control.ts @@ -1,6 +1,6 @@ import { getConfig } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from './datasource'; diff --git a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts index 96bc19b4aff..2475248dc34 100644 --- a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts +++ b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts @@ -1,5 +1,5 @@ import { config } from '@grafana/runtime'; -import { CloudNotifierType, NotificationChannelOption, NotifierDTO } from 'app/types'; +import { CloudNotifierType, NotificationChannelOption, NotifierDTO } from 'app/types/alerting'; import { option } from './notifier-types'; diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index a50eddcfa50..4bcf3757d2c 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -8,7 +8,7 @@ import { AlertManagerImplementation, AlertmanagerChoice, } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { DataSourceRulesSourceIdentifier as DataSourceRulesSourceIdentifier, GrafanaRulesSourceIdentifier, diff --git a/public/app/features/alerting/unified/utils/navigation.ts b/public/app/features/alerting/unified/utils/navigation.ts index 15081fb8b4e..259207f41b7 100644 --- a/public/app/features/alerting/unified/utils/navigation.ts +++ b/public/app/features/alerting/unified/utils/navigation.ts @@ -45,6 +45,32 @@ export const groups = { { skipSubPath: options?.skipSubPath } ); }, + newAlertRuleLink: (folderName?: string, folderUid?: string, groupName?: string) => { + const returnTo = createReturnTo(); + + const defaults = JSON.stringify({ + folder: { + title: folderName, + uid: folderUid, + }, + group: groupName, + }); + + return createRelativeUrl('/alerting/new', { defaults, returnTo }); + }, + newRecordingRuleLink: (folderName?: string, folderUid?: string, groupName?: string) => { + const returnTo = createReturnTo(); + + const defaults = JSON.stringify({ + folder: { + title: folderName, + uid: folderUid, + }, + group: groupName, + }); + + return createRelativeUrl('/alerting/new/grafana-recording', { defaults, returnTo }); + }, }; export const rulesNav = { diff --git a/public/app/features/alerting/unified/utils/notifier-types.ts b/public/app/features/alerting/unified/utils/notifier-types.ts index f7c9e0054bc..eeb2f30dae9 100644 --- a/public/app/features/alerting/unified/utils/notifier-types.ts +++ b/public/app/features/alerting/unified/utils/notifier-types.ts @@ -1,4 +1,4 @@ -import { NotificationChannelOption } from '../../../../types'; +import { NotificationChannelOption } from 'app/types/alerting'; export function option( propertyName: string, diff --git a/public/app/features/alerting/unified/utils/receiver-form.ts b/public/app/features/alerting/unified/utils/receiver-form.ts index c5ae3b55263..689bfe07c26 100644 --- a/public/app/features/alerting/unified/utils/receiver-form.ts +++ b/public/app/features/alerting/unified/utils/receiver-form.ts @@ -7,7 +7,7 @@ import { GrafanaManagedReceiverSecureFields, Receiver, } from 'app/plugins/datasource/alertmanager/types'; -import { CloudNotifierType, NotificationChannelOption, NotifierDTO, NotifierType } from 'app/types'; +import { CloudNotifierType, NotificationChannelOption, NotifierDTO, NotifierType } from 'app/types/alerting'; import { ChannelValues, diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index 91ca9915e34..033be738e49 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -533,3 +533,22 @@ export function isRecordingRuleByType(type?: RuleFormType) { export function isDataSourceManagedRuleByType(type?: RuleFormType) { return isCloudAlertingRuleByType(type) || isCloudRecordingRuleByType(type); } + +/* + * Grab the UID from either a rulerRule definition or a Prometheus rule definition, only Grafana-managed rules will have a UID. + */ +export function getRuleUID(rule?: RulerRuleDTO | Rule) { + if (!rule) { + return; + } + + let ruleUid: string | undefined; + + if ('grafana_alert' in rule && rulerRuleType.grafana.rule(rule)) { + ruleUid = rule.grafana_alert.uid; + } else if ('uid' in rule && prometheusRuleType.grafana.rule(rule)) { + ruleUid = rule.uid; + } + + return ruleUid; +} diff --git a/public/app/features/auth-config/AuthDrawer.tsx b/public/app/features/auth-config/AuthDrawer.tsx index 06d1ebc948c..e6a3892bc1c 100644 --- a/public/app/features/auth-config/AuthDrawer.tsx +++ b/public/app/features/auth-config/AuthDrawer.tsx @@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, Drawer, Text, TextLink, Switch, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { loadSettings, saveSettings } from './state/actions'; diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index d0ae5e73ab7..37fdf7c5cfe 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -7,7 +7,7 @@ import { reportInteraction } from '@grafana/runtime'; import { Grid, TextLink, ToolbarButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { config } from 'app/core/config'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import AuthDrawer from './AuthDrawer'; import ConfigureAuthCTA from './components/ConfigureAuthCTA'; diff --git a/public/app/features/auth-config/ErrorContainer.tsx b/public/app/features/auth-config/ErrorContainer.tsx index 79cf6514569..336dde9716a 100644 --- a/public/app/features/auth-config/ErrorContainer.tsx +++ b/public/app/features/auth-config/ErrorContainer.tsx @@ -1,7 +1,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { Alert } from '@grafana/ui'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { resetError, resetWarning } from './state/reducers'; diff --git a/public/app/features/auth-config/ProviderConfigPage.tsx b/public/app/features/auth-config/ProviderConfigPage.tsx index 617626660fa..34614ddb746 100644 --- a/public/app/features/auth-config/ProviderConfigPage.tsx +++ b/public/app/features/auth-config/ProviderConfigPage.tsx @@ -6,7 +6,7 @@ import { t } from '@grafana/i18n'; import { Badge, Stack, Text } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { ProviderConfigForm } from './ProviderConfigForm'; import { UIMap } from './constants'; diff --git a/public/app/features/auth-config/index.ts b/public/app/features/auth-config/index.ts index c927b22e701..69113401661 100644 --- a/public/app/features/auth-config/index.ts +++ b/public/app/features/auth-config/index.ts @@ -1,7 +1,8 @@ import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { AccessControlAction, Settings, SettingsSection } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Settings, SettingsSection } from 'app/types/settings'; import { AuthProviderInfo, GetStatusHook, AuthProviderStatus } from './types'; diff --git a/public/app/features/auth-config/state/actions.ts b/public/app/features/auth-config/state/actions.ts index 074cb6b894f..cf1cdd8d7f7 100644 --- a/public/app/features/auth-config/state/actions.ts +++ b/public/app/features/auth-config/state/actions.ts @@ -2,7 +2,9 @@ import { lastValueFrom } from 'rxjs'; import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Settings, ThunkResult, UpdateSettingsQuery } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Settings, UpdateSettingsQuery } from 'app/types/settings'; +import { ThunkResult } from 'app/types/store'; import { getAuthProviderStatus, getRegisteredAuthProviders, SSOProvider } from '..'; import { AuthProviderStatus, SettingsError } from '../types'; diff --git a/public/app/features/auth-config/state/reducers.ts b/public/app/features/auth-config/state/reducers.ts index 0194ed538d0..0dac91d70a6 100644 --- a/public/app/features/auth-config/state/reducers.ts +++ b/public/app/features/auth-config/state/reducers.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Settings } from 'app/types'; +import { Settings } from 'app/types/settings'; import { SettingsError, AuthProviderStatus, AuthConfigState, SSOProvider } from '../types'; diff --git a/public/app/features/auth-config/types.ts b/public/app/features/auth-config/types.ts index b14336f06ab..6b6fd5c5a4e 100644 --- a/public/app/features/auth-config/types.ts +++ b/public/app/features/auth-config/types.ts @@ -2,7 +2,7 @@ import { ReactElement } from 'react'; import { Validate, UseFormSetValue } from 'react-hook-form'; import { IconName, SelectableValue } from '@grafana/data'; -import { Settings } from 'app/types'; +import { Settings } from 'app/types/settings'; export interface AuthProviderInfo { id: string; type: string; diff --git a/public/app/features/bookmarks/BookmarksPage.tsx b/public/app/features/bookmarks/BookmarksPage.tsx index 6a4c1b704df..6b7f9ab08ae 100644 --- a/public/app/features/bookmarks/BookmarksPage.tsx +++ b/public/app/features/bookmarks/BookmarksPage.tsx @@ -7,7 +7,7 @@ import { usePinnedItems } from 'app/core/components/AppChrome/MegaMenu/hooks'; import { findByUrl } from 'app/core/components/AppChrome/MegaMenu/utils'; import { NavLandingPageCard } from 'app/core/components/NavLandingPage/NavLandingPageCard'; import { Page } from 'app/core/components/Page/Page'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; export function BookmarksPage() { const styles = useStyles2(getStyles); diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 7a58ec579ae..e392e6a3f53 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -10,7 +10,7 @@ import { config, reportInteraction } from '@grafana/runtime'; import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { FolderRepo } from '../../core/components/NestedFolderPicker/FolderRepo'; import { contextSrv } from '../../core/services/context_srv'; diff --git a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx index 06fc910a683..3e18b71d399 100644 --- a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx +++ b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx @@ -8,8 +8,7 @@ import { FilterInput, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { ActionRow } from 'app/features/search/page/components/ActionRow'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; - -import { useDispatch } from '../../types'; +import { useDispatch } from 'app/types/store'; import { useRecentlyDeletedStateManager } from './api/useRecentlyDeletedStateManager'; import { RecentlyDeletedActions } from './components/RecentlyDeletedActions'; diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index fdd091aecc3..b9f5f4c9323 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -15,15 +15,9 @@ import { isDashboardV2Resource, isV1DashboardCommand, isV2DashboardCommand } fro import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dispatch } from 'app/store/store'; -import { - DescendantCount, - DescendantCountDTO, - FolderDTO, - FolderListItemDTO, - ImportDashboardResponseDTO, - PermissionLevelString, - SaveDashboardResponseDTO, -} from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; +import { SaveDashboardResponseDTO, ImportDashboardResponseDTO } from 'app/types/dashboard'; +import { FolderListItemDTO, FolderDTO, DescendantCount, DescendantCountDTO } from 'app/types/folders'; import { refetchChildren, refreshParents } from '../state/actions'; import { DashboardTreeSelection } from '../types'; diff --git a/public/app/features/browse-dashboards/api/isProvisioned.ts b/public/app/features/browse-dashboards/api/isProvisioned.ts index 7df88e77de3..f0b948d4d5e 100644 --- a/public/app/features/browse-dashboards/api/isProvisioned.ts +++ b/public/app/features/browse-dashboards/api/isProvisioned.ts @@ -1,6 +1,6 @@ import { Folder } from 'app/api/clients/folder/v1beta1'; import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { DashboardWithAccessInfo } from '../../dashboard/api/types'; diff --git a/public/app/features/browse-dashboards/api/services.ts b/public/app/features/browse-dashboards/api/services.ts index c5eb2375b65..a6a511ba0f7 100644 --- a/public/app/features/browse-dashboards/api/services.ts +++ b/public/app/features/browse-dashboards/api/services.ts @@ -4,9 +4,9 @@ import { getGrafanaSearcher } from 'app/features/search/service/searcher'; import { NestedFolderDTO } from 'app/features/search/service/types'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { contextSrv } from '../../../core/core'; -import { AccessControlAction } from '../../../types'; import { getFolderURL, isSharedWithMe } from '../components/utils'; export const PAGE_SIZE = 50; diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index 931c66cfd74..3dd60457c77 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -5,8 +5,8 @@ import { config, reportInteraction } from '@grafana/runtime'; import { Button, Stack, Tooltip } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; -import { useDispatch } from 'app/types'; import { ShowModalReactEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDashboardsAPI'; import { useActionSelectionState } from '../../state/hooks'; diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx index 2a6c55a4ed3..b20fa522fc7 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { Trans, t } from '@grafana/i18n'; import { CallToActionCard, EmptyState, LinkButton, TextLink } from '@grafana/ui'; import { DashboardViewItem } from 'app/features/search/types'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { PAGE_SIZE } from '../api/services'; import { fetchNextChildrenPage } from '../state/actions'; diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx index 902f3b0ef2a..e5635cdfd4d 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx @@ -2,7 +2,7 @@ import { render as rtlRender, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { mockFolderDTO } from '../fixtures/folder.fixture'; diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx index bb00a254d31..d1c487da4f8 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx @@ -12,7 +12,7 @@ import { getNewFolderPhrase, getNewPhrase, } from 'app/features/search/tempI18nPhrases'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { ManagerKind } from '../../apiserver/types'; import { useNewFolderMutation } from '../api/browseDashboardsAPI'; diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx index f56e50bf023..0412efd0c18 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { ProvisionedFolderFormDataResult, useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx index 995f68bbd23..2b4a6ebb92f 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx @@ -10,7 +10,7 @@ import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/ap import { AnnoKeySourcePath } from 'app/features/apiserver/types'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields'; import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx index d5cef0b3aa1..9640314aec9 100644 --- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx @@ -5,8 +5,8 @@ import { config, locationService, reportInteraction } from '@grafana/runtime'; import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui'; import { Permissions } from 'app/core/components/AccessControl'; import { appEvents } from 'app/core/core'; -import { FolderDTO } from 'app/types'; import { ShowModalReactEvent } from 'app/types/events'; +import { FolderDTO } from 'app/types/folders'; import { ManagerKind } from '../../apiserver/types'; import { useDeleteFolderMutation, useMoveFolderMutation } from '../api/browseDashboardsAPI'; diff --git a/public/app/features/browse-dashboards/components/NewFolderForm.tsx b/public/app/features/browse-dashboards/components/NewFolderForm.tsx index 25ee5b5282f..a5c8fdb1043 100644 --- a/public/app/features/browse-dashboards/components/NewFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewFolderForm.tsx @@ -28,18 +28,6 @@ export function NewFolderForm({ onCancel, onConfirm }: Props) { 'browse-dashboards.action.new-folder-name-required-phrase', 'Folder name is required.' ); - const validateFolderName = async (folderName: string) => { - try { - await validationSrv.validateNewFolderName(folderName); - return true; - } catch (e) { - if (e instanceof Error) { - return e.message; - } else { - throw e; - } - } - }; const fieldNameLabel = t('browse-dashboards.new-folder-form.name-label', 'Folder name'); @@ -75,3 +63,16 @@ export function NewFolderForm({ onCancel, onConfirm }: Props) { </form> ); } + +export async function validateFolderName(folderName: string) { + try { + await validationSrv.validateNewFolderName(folderName); + return true; + } catch (e) { + if (e instanceof Error) { + return e.message; + } else { + throw e; + } + } +} diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx index e4c7527c079..a983a66ada0 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx @@ -6,8 +6,8 @@ import { getAppEvents } from '@grafana/runtime'; import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; +import { FolderDTO } from 'app/types/folders'; -import { FolderDTO } from '../../../types'; import { ProvisionedFolderFormDataResult, useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; import { NewProvisionedFolderForm } from './NewProvisionedFolderForm'; diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx index 10a9e429099..f7f141fac01 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx @@ -1,22 +1,26 @@ +import { css } from '@emotion/css'; import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { AppEvents } from '@grafana/data'; +import { AppEvents, GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { getAppEvents } from '@grafana/runtime'; -import { Alert, Button, Field, Input, Stack } from '@grafana/ui'; +import { Alert, Text, Button, Field, Icon, Input, Stack, useStyles2 } from '@grafana/ui'; import { Folder } from 'app/api/clients/folder/v1beta1'; import { RepositoryView, useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { AnnoKeySourcePath, Resource } from 'app/features/apiserver/types'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields'; import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared'; -import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; + +import { validateFolderName } from './NewFolderForm'; +import { formatFolderName, hasFolderNameCharactersToReplace } from './utils'; + interface FormProps extends Props { initialValues: BaseProvisionedFormData; repository?: RepositoryView; @@ -40,7 +44,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit }); const { handleSubmit, watch, register, formState } = methods; - const [workflow, ref] = watch(['workflow', 'ref']); + const [workflow, ref, title] = watch(['workflow', 'ref', 'title']); // TODO: replace with useProvisionedRequestHandler hook useEffect(() => { @@ -82,18 +86,6 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit } }, [request.isSuccess, request.isError, request.error, ref, request.data, workflow, navigate, repository, onDismiss]); - const validateFolderName = async (folderName: string) => { - try { - await validationSrv.validateNewFolderName(folderName); - return true; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return t('browse-dashboards.new-provisioned-folder-form.error-invalid-folder-name', 'Invalid folder name'); - } - }; - const doSave = async ({ ref, title, workflow, comment }: BaseProvisionedFormData) => { const repoName = repository?.name; if (!title || !repoName) { @@ -102,10 +94,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit const basePath = folder?.metadata?.annotations?.[AnnoKeySourcePath] ?? ''; // Convert folder title to filename format (lowercase, replace spaces with hyphens) - const titleInFilenameFormat = title - .toLowerCase() - .replace(/\s+/g, '-') - .replace(/[^a-z0-9-]/g, ''); + const titleInFilenameFormat = formatFolderName(title); // TODO: this is currently not working, issue created https://github.com/grafana/git-ui-sync-project/issues/314 const prefix = basePath ? `${basePath}/` : ''; const path = `${prefix}${titleInFilenameFormat}/`; @@ -163,6 +152,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit id="folder-name-input" /> </Field> + <FolderNamePreviewMessage folderName={title} /> <ResourceEditFormSharedFields resourceType="folder" @@ -210,7 +200,7 @@ export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) { const { workflowOptions, isGitHub, repository, folder, initialValues } = useProvisionedFolderFormData({ folderUid: parentFolder?.uid, action: 'create', - title: parentFolder?.title, + title: '', // Empty title for new folders }); if (!initialValues) { @@ -229,3 +219,39 @@ export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) { /> ); } + +function FolderNamePreviewMessage({ folderName }: { folderName: string }) { + const styles = useStyles2(getStyles); + const isValidFolderName = + folderName.length && hasFolderNameCharactersToReplace(folderName) && validateFolderName(folderName); + + if (!isValidFolderName) { + return null; + } + + return ( + <div className={styles.folderNameMessage}> + <Icon name="check-circle" type="solid" /> + <Text color="success"> + {t( + 'browse-dashboards.new-provisioned-folder-form.text-your-folder-will-be-created-as', + 'Your folder will be created as {{folderName}}', + { + folderName: formatFolderName(folderName), + } + )} + </Text> + </div> + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + folderNameMessage: css({ + display: 'flex', + alignItems: 'center', + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.success.text, + }), + }; +}; diff --git a/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx b/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx index 57443939fa6..32068d49077 100644 --- a/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx +++ b/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx @@ -12,11 +12,11 @@ export function ProvisionedFolderPreviewBanner({ queryParams }: CommonBannerProp } if (prURL) { - return <PreviewBannerViewPR prParam={prURL} isFolder />; + return <PreviewBannerViewPR prParam={prURL} />; } if (newPrURL) { - return <PreviewBannerViewPR prParam={newPrURL} isFolder isNewPr />; + return <PreviewBannerViewPR prParam={newPrURL} isNewPr />; } return null; diff --git a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx index 158c8d9fa71..18cc800213e 100644 --- a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx +++ b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx @@ -6,8 +6,8 @@ import { Button, Stack } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { AnnoKeyFolder } from 'app/features/apiserver/types'; import { GENERAL_FOLDER_UID } from 'app/features/search/constants'; -import { useDispatch } from 'app/types'; import { ShowModalReactEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { deletedDashboardsCache } from '../../search/service/deletedDashboardsCache'; import { useListDeletedDashboardsQuery, useRestoreDashboardMutation } from '../api/browseDashboardsAPI'; diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx index 203d46a4307..281455f140e 100644 --- a/public/app/features/browse-dashboards/components/SearchView.tsx +++ b/public/app/features/browse-dashboards/components/SearchView.tsx @@ -7,7 +7,7 @@ import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKey import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable'; import { SearchStateManager } from 'app/features/search/state/SearchStateManager'; import { DashboardViewItemKind, SearchState } from 'app/features/search/types'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { useHasSelection } from '../state/hooks'; import { setAllSelection, setItemSelectionState } from '../state/slice'; diff --git a/public/app/features/browse-dashboards/components/utils.test.ts b/public/app/features/browse-dashboards/components/utils.test.ts new file mode 100644 index 00000000000..2bf48e3d329 --- /dev/null +++ b/public/app/features/browse-dashboards/components/utils.test.ts @@ -0,0 +1,124 @@ +import { formatFolderName, hasFolderNameCharactersToReplace } from './utils'; + +describe('formatFolderName', () => { + it('should handle empty string', () => { + expect(formatFolderName('')).toBe(''); + }); + + it('should convert uppercase to lowercase', () => { + expect(formatFolderName('MyFolder')).toBe('myfolder'); + expect(formatFolderName('UPPERCASE')).toBe('uppercase'); + expect(formatFolderName('MiXeD cAsE')).toBe('mixed-case'); + }); + + it('should replace whitespace with hyphens', () => { + expect(formatFolderName('folder name')).toBe('folder-name'); + expect(formatFolderName('folder name')).toBe('folder-name'); // multiple spaces + expect(formatFolderName('folder\tname')).toBe('folder-name'); // tab + expect(formatFolderName('folder\nname')).toBe('folder-name'); // newline + expect(formatFolderName(' folder name ')).toBe('folder-name'); // leading/trailing spaces + }); + + it('should remove special characters', () => { + expect(formatFolderName('folder@name')).toBe('foldername'); + expect(formatFolderName('folder!@#$%^&*()name')).toBe('foldername'); + expect(formatFolderName('folder_name')).toBe('foldername'); + expect(formatFolderName('folder.name')).toBe('foldername'); + expect(formatFolderName('folder/name')).toBe('foldername'); + }); + + it('should preserve numbers and hyphens', () => { + expect(formatFolderName('folder-123')).toBe('folder-123'); + expect(formatFolderName('folder123')).toBe('folder123'); + expect(formatFolderName('123-folder')).toBe('123-folder'); + expect(formatFolderName('folder-name-123')).toBe('folder-name-123'); + }); + + it('should handle complex mixed cases', () => { + expect(formatFolderName('My Folder @2023!')).toBe('my-folder-2023'); + expect(formatFolderName(' FOLDER_NAME with-123 ')).toBe('foldername-with-123'); + expect(formatFolderName('Test@Folder#Name$123')).toBe('testfoldername123'); + expect(formatFolderName('Multiple Spaces Between')).toBe('multiple-spaces-between'); + }); + + it('should handle strings with only special characters', () => { + expect(formatFolderName('!@#$%^&*()')).toBe(''); + expect(formatFolderName('___')).toBe(''); + expect(formatFolderName('...')).toBe(''); + }); + + it('should handle strings with only whitespace', () => { + expect(formatFolderName(' ')).toBe(''); + expect(formatFolderName('\t\n\r')).toBe(''); + }); + + it('should handle already formatted names', () => { + expect(formatFolderName('already-formatted')).toBe('already-formatted'); + expect(formatFolderName('folder123')).toBe('folder123'); + expect(formatFolderName('test-folder-name-123')).toBe('test-folder-name-123'); + }); +}); + +describe('hasFolderNameCharactersToReplace', () => { + it('should return false for non-string inputs', () => { + // @ts-expect-error + expect(hasFolderNameCharactersToReplace(null)).toBe(false); + // @ts-expect-error + expect(hasFolderNameCharactersToReplace(undefined)).toBe(false); + // @ts-expect-error + expect(hasFolderNameCharactersToReplace(123)).toBe(false); + // @ts-expect-error + expect(hasFolderNameCharactersToReplace({})).toBe(false); + // @ts-expect-error + expect(hasFolderNameCharactersToReplace([])).toBe(false); + }); + + it('should return false for empty string', () => { + expect(hasFolderNameCharactersToReplace('')).toBe(false); + }); + + it('should return false for valid folder names', () => { + expect(hasFolderNameCharactersToReplace('validname')).toBe(false); + expect(hasFolderNameCharactersToReplace('folder123')).toBe(false); + expect(hasFolderNameCharactersToReplace('test-folder-name')).toBe(false); + expect(hasFolderNameCharactersToReplace('folder-123')).toBe(false); + expect(hasFolderNameCharactersToReplace('123-folder')).toBe(false); + expect(hasFolderNameCharactersToReplace('a')).toBe(false); + expect(hasFolderNameCharactersToReplace('1')).toBe(false); + }); + + it('should return true for names with whitespace', () => { + expect(hasFolderNameCharactersToReplace('folder name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder\tname')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder\nname')).toBe(true); + expect(hasFolderNameCharactersToReplace(' folder')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder ')).toBe(true); + expect(hasFolderNameCharactersToReplace(' ')).toBe(true); + }); + + it('should return true for names with uppercase letters', () => { + expect(hasFolderNameCharactersToReplace('FolderName')).toBe(true); + expect(hasFolderNameCharactersToReplace('UPPERCASE')).toBe(true); + expect(hasFolderNameCharactersToReplace('MiXeD')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder-Name')).toBe(true); + }); + + it('should return true for names with special characters', () => { + expect(hasFolderNameCharactersToReplace('folder@name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder!name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder_name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder.name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder/name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder#name')).toBe(true); + }); + + it('should return true for mixed cases with multiple issues', () => { + expect(hasFolderNameCharactersToReplace('Test@Folder#Name$123')).toBe(true); + expect(hasFolderNameCharactersToReplace('Multiple Spaces Between')).toBe(true); + }); + + it('should return true for strings with only special characters', () => { + expect(hasFolderNameCharactersToReplace('!@#$%^&*()')).toBe(true); + }); +}); diff --git a/public/app/features/browse-dashboards/components/utils.ts b/public/app/features/browse-dashboards/components/utils.ts index af40b5e2f0f..8f29702bda7 100644 --- a/public/app/features/browse-dashboards/components/utils.ts +++ b/public/app/features/browse-dashboards/components/utils.ts @@ -22,3 +22,38 @@ export function getFolderURL(uid: string) { } return url; } + +export function hasFolderNameCharactersToReplace(folderName: string): boolean { + if (typeof folderName !== 'string') { + return false; + } + + // whitespace that needs to be replaced with hyphens + const hasWhitespace = /\s+/.test(folderName); + + // characters that are not lowercase letters, numbers, or hyphens + const hasInvalidCharacters = /[^a-z0-9-]/.test(folderName); + + return hasWhitespace || hasInvalidCharacters; +} + +export function formatFolderName(folderName?: string): string { + if (typeof folderName !== 'string') { + console.error('Invalid folder name type:', typeof folderName); + return ''; + } + + const result = folderName + .trim() // Remove leading/trailing whitespace first + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, '') + .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens + + // If the result is empty, return empty string + if (result === '') { + return ''; + } + + return result; +} diff --git a/public/app/features/browse-dashboards/fixtures/folder.fixture.ts b/public/app/features/browse-dashboards/fixtures/folder.fixture.ts index 14cb3a83652..bab54a40975 100644 --- a/public/app/features/browse-dashboards/fixtures/folder.fixture.ts +++ b/public/app/features/browse-dashboards/fixtures/folder.fixture.ts @@ -1,6 +1,6 @@ import { Chance } from 'chance'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; export function mockFolderDTO(seed = 1, partial?: Partial<FolderDTO>): FolderDTO { const random = Chance(seed); diff --git a/public/app/features/browse-dashboards/permissions.ts b/public/app/features/browse-dashboards/permissions.ts index 49eff8d69ff..d589af9480a 100644 --- a/public/app/features/browse-dashboards/permissions.ts +++ b/public/app/features/browse-dashboards/permissions.ts @@ -1,6 +1,7 @@ import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, FolderDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { FolderDTO } from 'app/types/folders'; function checkFolderPermission(action: AccessControlAction, folderDTO?: FolderDTO) { return folderDTO ? contextSrv.hasPermissionInMetadata(action, folderDTO) : contextSrv.hasPermission(action); diff --git a/public/app/features/browse-dashboards/state/actions.ts b/public/app/features/browse-dashboards/state/actions.ts index 88073d0e518..f738493b342 100644 --- a/public/app/features/browse-dashboards/state/actions.ts +++ b/public/app/features/browse-dashboards/state/actions.ts @@ -1,6 +1,6 @@ import { GENERAL_FOLDER_UID } from 'app/features/search/constants'; import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types'; -import { createAsyncThunk } from 'app/types'; +import { createAsyncThunk } from 'app/types/store'; import { listDashboards, listFolders, PAGE_SIZE } from '../api/services'; import { DashboardViewItemWithUIItems, UIDashboardViewItem } from '../types'; diff --git a/public/app/features/browse-dashboards/state/hooks.test.ts b/public/app/features/browse-dashboards/state/hooks.test.ts index 373a6c0a0f7..d5fd74b4bb0 100644 --- a/public/app/features/browse-dashboards/state/hooks.test.ts +++ b/public/app/features/browse-dashboards/state/hooks.test.ts @@ -1,13 +1,13 @@ import { configureStore } from 'app/store/configureStore'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { fullyLoadedViewItemCollection } from '../fixtures/state.fixtures'; import { BrowseDashboardsState } from '../types'; import { useBrowseLoadingStatus } from './hooks'; -jest.mock('app/types', () => { - const original = jest.requireActual('app/types'); +jest.mock('app/types/store', () => { + const original = jest.requireActual('app/types/store'); return { ...original, useSelector: jest.fn(), diff --git a/public/app/features/browse-dashboards/state/hooks.ts b/public/app/features/browse-dashboards/state/hooks.ts index d46f0b44708..2c0bee62a98 100644 --- a/public/app/features/browse-dashboards/state/hooks.ts +++ b/public/app/features/browse-dashboards/state/hooks.ts @@ -2,7 +2,7 @@ import { useCallback, useRef } from 'react'; import { createSelector } from 'reselect'; import { DashboardViewItem } from 'app/features/search/types'; -import { useSelector, StoreState, useDispatch } from 'app/types'; +import { useSelector, StoreState, useDispatch } from 'app/types/store'; import { PAGE_SIZE } from '../api/services'; import { isSharedWithMe } from '../components/utils'; diff --git a/public/app/features/commandPalette/actions/staticActions.ts b/public/app/features/commandPalette/actions/staticActions.ts index d3304f0db78..e7e8c8d6734 100644 --- a/public/app/features/commandPalette/actions/staticActions.ts +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -6,8 +6,8 @@ import { enrichHelpItem } from 'app/core/components/AppChrome/MegaMenu/utils'; import { performInviteUserClick, shouldRenderInviteUserButton } from 'app/core/components/InviteUserButton/utils'; import { changeTheme } from 'app/core/services/theme'; import { currentMockApiState, toggleMockApiAndReload, togglePseudoLocale } from 'app/dev-utils'; +import { useSelector } from 'app/types/store'; -import { useSelector } from '../../../types'; import { CommandPaletteAction } from '../types'; import { ACTIONS_PRIORITY, DEFAULT_PRIORITY, PREFERENCES_PRIORITY } from '../values'; diff --git a/public/app/features/connections/Connections.tsx b/public/app/features/connections/Connections.tsx index 5515e9bf96b..e4119a6bf3f 100644 --- a/public/app/features/connections/Connections.tsx +++ b/public/app/features/connections/Connections.tsx @@ -1,6 +1,6 @@ import { Navigate, Routes, Route, useLocation } from 'react-router-dom-v5-compat'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; import { ROUTES } from './constants'; import { AddNewConnectionPage } from './pages/AddNewConnectionPage'; diff --git a/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx b/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx index f9bb04c72de..438868f7751 100644 --- a/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx +++ b/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx @@ -4,9 +4,9 @@ import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Alert, LinkButton, useStyles2 } from '@grafana/ui'; +import { AccessControlAction } from 'app/types/accessControl'; import { contextSrv } from '../../../../core/core'; -import { AccessControlAction } from '../../../../types'; import { ROUTES } from '../../constants'; const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/features/connections/hooks/useDataSourceSettingsNav.ts b/public/app/features/connections/hooks/useDataSourceSettingsNav.ts index d5898d370d2..72c4a09059a 100644 --- a/public/app/features/connections/hooks/useDataSourceSettingsNav.ts +++ b/public/app/features/connections/hooks/useDataSourceSettingsNav.ts @@ -7,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel'; import { useDataSource, useDataSourceMeta, useDataSourceSettings } from 'app/features/datasources/state/hooks'; import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from 'app/features/datasources/state/navModel'; import { useGetSingle } from 'app/features/plugins/admin/state/hooks'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; export function useDataSourceSettingsNav(pageIdParam?: string) { const { uid = '' } = useParams<{ uid: string }>(); diff --git a/public/app/features/connections/pages/DataSourceDetailsPage.tsx b/public/app/features/connections/pages/DataSourceDetailsPage.tsx index be345ee77ed..f7223abc418 100644 --- a/public/app/features/connections/pages/DataSourceDetailsPage.tsx +++ b/public/app/features/connections/pages/DataSourceDetailsPage.tsx @@ -3,7 +3,8 @@ import { useParams } from 'react-router-dom-v5-compat'; import { Trans, t } from '@grafana/i18n'; import { Alert, Badge, TextLink } from '@grafana/ui'; import { PluginDetailsPage } from 'app/features/plugins/admin/components/PluginDetailsPage'; -import { StoreState, useSelector, AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; +import { StoreState, useSelector } from 'app/types/store'; import { ROUTES } from '../constants'; diff --git a/public/app/features/connections/pages/DataSourcesListPage.tsx b/public/app/features/connections/pages/DataSourcesListPage.tsx index 5c67b07da24..7ec9062b1d7 100644 --- a/public/app/features/connections/pages/DataSourcesListPage.tsx +++ b/public/app/features/connections/pages/DataSourcesListPage.tsx @@ -3,7 +3,7 @@ import { AdvisorRedirectNotice } from 'app/features/connections/components/Advis import { DataSourceAddButton } from 'app/features/datasources/components/DataSourceAddButton'; import { DataSourcesList } from 'app/features/datasources/components/DataSourcesList'; import { getDataSourcesCount } from 'app/features/datasources/state/selectors'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; export function DataSourcesListPage() { const dataSourcesCount = useSelector(({ dataSources }: StoreState) => getDataSourcesCount(dataSources)); diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx index 0215b463ca9..a4dabd2bbf9 100644 --- a/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx +++ b/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx @@ -7,7 +7,7 @@ import { locationService } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { getCatalogPluginMock, getPluginsStateMock } from 'app/features/plugins/admin/mocks/mockHelpers'; import { CatalogPlugin } from 'app/features/plugins/admin/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AddNewConnection } from './ConnectData'; diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx index 30f0464b7ce..3668047af9f 100644 --- a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx +++ b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx @@ -14,7 +14,7 @@ import { SearchField } from 'app/features/plugins/admin/components/SearchField'; import { Sorters } from 'app/features/plugins/admin/helpers'; import { useHistory } from 'app/features/plugins/admin/hooks/useHistory'; import { useGetAll, useIsRemotePluginsAvailable } from 'app/features/plugins/admin/state/hooks'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ROUTES } from '../../constants'; diff --git a/public/app/features/correlations/CorrelationsPage.tsx b/public/app/features/correlations/CorrelationsPage.tsx index bab13964ae6..60ce5a5b166 100644 --- a/public/app/features/correlations/CorrelationsPage.tsx +++ b/public/app/features/correlations/CorrelationsPage.tsx @@ -22,7 +22,7 @@ import { import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { useNavModel } from 'app/core/hooks/useNavModel'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AddCorrelationForm } from './Forms/AddCorrelationForm'; import { EditCorrelationForm } from './Forms/EditCorrelationForm'; diff --git a/public/app/features/correlations/utils.ts b/public/app/features/correlations/utils.ts index 1a6f97157da..37c81fc6728 100644 --- a/public/app/features/correlations/utils.ts +++ b/public/app/features/correlations/utils.ts @@ -9,7 +9,7 @@ import { getBackendSrv, getDataSourceSrv, } from '@grafana/runtime'; -import { ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; import { formatValueName } from '../explore/PrometheusListView/ItemLabels'; import { parseLogsFrame } from '../logs/logsFrame'; diff --git a/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx b/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx index d437e1308ce..17ecf87eb8f 100644 --- a/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx +++ b/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx @@ -9,7 +9,7 @@ import { Panel } from '@grafana/schema'; import { Alert, Button, Field, Modal, RadioButtonGroup } from '@grafana/ui'; import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { addToDashboard, SubmissionError } from './addToDashboard'; diff --git a/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts b/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts index 7cad2fc7c3d..90afde16cd4 100644 --- a/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts +++ b/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts @@ -1,7 +1,7 @@ import { SceneTimeRange } from '@grafana/scenes'; import store from 'app/core/store'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; -import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts b/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts index e0f929c6f2e..e22feb9a502 100644 --- a/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts +++ b/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts @@ -4,7 +4,7 @@ import { config, locationService } from '@grafana/runtime'; import { Panel } from '@grafana/schema'; import store from 'app/core/store'; import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; -import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types/dashboard'; export enum GenericError { UNKNOWN = 'unknown-error', diff --git a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx index 90b1524c1d2..ae55db117d0 100644 --- a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx +++ b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx @@ -7,7 +7,7 @@ import { EmbeddedDashboardProps } from '@grafana/runtime'; import { SceneObjectStateChangedEvent, sceneUtils } from '@grafana/scenes'; import { Spinner, Alert, useStyles2 } from '@grafana/ui'; import { getMessageFromError } from 'app/core/utils/errors'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx index e6a1c1ffbf4..cab5eaf608c 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx @@ -24,7 +24,7 @@ import { TextLink, } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowMessage, SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index b2776dc9414..016e72010bf 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -15,7 +15,7 @@ import { getRouteComponentProps } from 'app/core/navigation/mocks/routeProps'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import store from 'app/core/store'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; -import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types/dashboard'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { setupLoadDashboardMockReject, setupLoadDashboardRuntimeErrorMock } from '../utils/test-utils'; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index bcdcff100ec..ef1ec38b653 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -11,7 +11,7 @@ import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardPageError } from 'app/features/dashboard/containers/DashboardPageError'; import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { DashboardPrompt } from '../saving/DashboardPrompt'; import { DashboardPreviewBanner } from '../saving/provisioned/DashboardPreviewBanner'; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index a38fe4fb66f..1b971c981ee 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -14,7 +14,7 @@ import { setDashboardLoaderSrv, } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getDashboardSnapshotSrv } from 'app/features/dashboard/services/SnapshotSrv'; -import { DASHBOARD_FROM_LS_KEY, DashboardDataDTO, DashboardDTO, DashboardRoutes } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardDataDTO, DashboardDTO, DashboardRoutes } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; import { setupLoadDashboardMock, setupLoadDashboardMockReject } from '../utils/test-utils'; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index e6c2cb3e0a8..1039767ed95 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -27,7 +27,7 @@ import { DashboardRoutes, HomeDashboardRedirectDTO, isRedirectResponse, -} from 'app/types'; +} from 'app/types/dashboard'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx index f7c88b2815a..07bcf478336 100644 --- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx @@ -16,7 +16,7 @@ import { PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams, } from 'app/features/dashboard/containers/types'; -import { AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; import { DashboardRoutes } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index 7f2b930b208..06573cc06ef 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -25,7 +25,8 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { configureStore } from 'app/store/configureStore'; -import { AccessControlAction, DashboardDataDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { AlertQuery, PromRulesResponse } from 'app/types/unified-alerting-dto'; import { createDashboardSceneFromDashboardModel } from '../../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx index c76896c4afd..241b37165b5 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx @@ -22,7 +22,7 @@ import { config, locationService } from '@grafana/runtime'; import { PANEL_EDIT_LAST_USED_DATASOURCE } from 'app/features/dashboard/utils/dashboard'; import { InspectTab } from 'app/features/inspector/types'; import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { PanelTimeRange, PanelTimeRangeState } from '../../scene/PanelTimeRange'; import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index f1660b56772..ce2784758be 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -18,12 +18,15 @@ import { addQuery } from 'app/core/utils/query'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; +import { ExpressionTypeDropdown } from 'app/features/expressions/components/ExpressionTypeDropdown'; +import { ExpressionQueryType } from 'app/features/expressions/types'; +import { getDefaults } from 'app/features/expressions/utils/expressionTypes'; import { GroupActionComponents } from 'app/features/query/components/QueryActionComponent'; import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows'; import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup'; import { updateQueries } from 'app/features/query/state/updateQueries'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest'; -import { QueryGroupOptions } from 'app/types'; +import { QueryGroupOptions } from 'app/types/query'; import { MIXED_DATASOURCE_NAME } from '../../../../plugins/datasource/mixed/MixedDataSource'; import { useQueryLibraryContext } from '../../../explore/QueryLibrary/QueryLibraryContext'; @@ -286,9 +289,15 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat return (dsSettings.meta.backend || dsSettings.meta.alerting || dsSettings.meta.mixed) === true; } - public onAddExpressionClick = () => { + public onAddExpressionOfType = (type: ExpressionQueryType) => { const queries = this.getQueries(); - this.onQueriesChange(addQuery(queries, expressionDatasource.newQuery())); + // Create base expression query with the specified type + const baseQuery = expressionDatasource.newQuery(); + const queryWithType = { ...baseQuery, type }; + // Apply defaults specific to the expression type + const queryWithDefaults = getDefaults(queryWithType); + + this.onQueriesChange(addQuery(queries, queryWithDefaults)); }; public renderExtraActions() { @@ -316,6 +325,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel if (!datasource || !dsSettings || !data) { return null; } + const showAddButton = !isSharedDashboardQuery(dsSettings.name); const onSelectQueryFromLibrary = async (query: DataQuery) => { // ensure all queries explicitly define a datasource @@ -394,16 +404,11 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel </> )} {config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && ( - <Button - icon="plus" - onClick={model.onAddExpressionClick} - variant="secondary" - data-testid={selectors.components.QueryTab.addExpression} - > - <span> + <ExpressionTypeDropdown handleOnSelect={model.onAddExpressionOfType}> + <Button icon="plus" variant="secondary" data-testid={selectors.components.QueryTab.addExpression}> <Trans i18nKey="dashboard-scene.panel-data-queries-tab-rendered.expression">Expression </Trans> - </span> - </Button> + </Button> + </ExpressionTypeDropdown> )} {model.renderExtraActions()} </Stack> diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx index 7ff6453297a..e06b9fc6da2 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx @@ -14,7 +14,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { SceneDataTransformer, SceneQueryRunner } from '@grafana/scenes'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene'; import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx index fa88f3b5251..c00f4233fc1 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx @@ -52,10 +52,13 @@ export function TransformationsDrawer(props: TransformationsDrawerProps) { ) { return false; } - return ( - t.name.toLocaleLowerCase().includes(drawerState.search.toLocaleLowerCase()) || - t.description?.toLocaleLowerCase().includes(drawerState.search.toLocaleLowerCase()) - ); + const searchLower = drawerState.search.toLocaleLowerCase(); + const textMatch = + t.name.toLocaleLowerCase().includes(searchLower) || t.description?.toLocaleLowerCase().includes(searchLower); + const tagMatch = t.tags?.size + ? Array.from(t.tags).some((tag) => tag.toLocaleLowerCase().includes(searchLower)) + : false; + return textMatch || tagMatch; }); const searchBoxSuffix = ( diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx index d78b4971bfe..dd4d6734919 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx @@ -6,7 +6,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { config } from '@grafana/runtime'; import { sceneGraph, SceneRefreshPicker } from '@grafana/scenes'; import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; import { DashboardSceneState } from '../scene/DashboardScene'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts index b47c88473ef..17ef35baa60 100644 --- a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts +++ b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts @@ -10,7 +10,7 @@ import { } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { ResponseTransformers } from 'app/features/dashboard/api/ResponseTransformers'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { validateFiltersOrigin } from '../serialization/sceneVariablesSetToVariables'; import { jsonDiff } from '../settings/version-history/utils'; diff --git a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx index ee306bad2f9..f44552cbd69 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx @@ -4,7 +4,7 @@ import { Alert } from '@grafana/ui'; import { useGetRepositoryFilesWithPathQuery } from 'app/api/clients/provisioning/v0alpha1'; import { DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { PreviewBannerViewPR } from './PreviewBannerViewPR'; diff --git a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx index de716143ac0..94964f8004a 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx @@ -60,14 +60,14 @@ describe('PreviewBannerViewPR', () => { setup({ prParam: 'test-url', isFolder: false, isNewPr: true }); expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('This dashboard is loaded from a branch in GitHub.')).toBeInTheDocument(); + expect(screen.getByText('A new resource has been created in a branch in GitHub.')).toBeInTheDocument(); }); it('should render correct text for existing PR dashboard', () => { setup({ prParam: 'test-url', isFolder: false, isNewPr: false }); expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('This dashboard is loaded from a pull request in GitHub.')).toBeInTheDocument(); + expect(screen.getByText('This resource is loaded from a pull request in GitHub.')).toBeInTheDocument(); }); it('should render correct button text for new PR dashboard', () => { @@ -88,14 +88,14 @@ describe('PreviewBannerViewPR', () => { setup({ prParam: 'test-url', isFolder: true, isNewPr: true }); expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('A new folder has been created in a branch in GitHub.')).toBeInTheDocument(); + expect(screen.getByText('A new resource has been created in a branch in GitHub.')).toBeInTheDocument(); }); it('should render correct text for existing PR folder', () => { setup({ prParam: 'test-url', isFolder: true, isNewPr: false }); expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('A new folder has been created in a pull request in GitHub.')).toBeInTheDocument(); + expect(screen.getByText('This resource is loaded from a pull request in GitHub.')).toBeInTheDocument(); }); it('should render correct button text for new PR folder', () => { diff --git a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx index 2188c4029a0..e1da054147f 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx @@ -8,33 +8,22 @@ import { commonAlertProps } from './DashboardPreviewBanner'; interface Props { prParam: string; - isFolder?: boolean; isNewPr?: boolean; } /** * @description This component is used to display a banner when a provisioned dashboard/folder is created or loaded from a new branch in Github. */ -export function PreviewBannerViewPR({ prParam, isFolder = false, isNewPr }: Props) { - const titleText = isFolder - ? isNewPr - ? t( - 'provisioned-resource-preview-banner.title-folder-created-branch-git-hub', - 'A new folder has been created in a branch in GitHub.' - ) - : t( - 'provisioned-resource-preview-banner.title-folder-created-pull-request-git-hub', - 'A new folder has been created in a pull request in GitHub.' - ) - : isNewPr - ? t( - 'provisioned-resource-preview-banner.title-dashboard-loaded-branch-git-hub', - 'This dashboard is loaded from a branch in GitHub.' - ) - : t( - 'provisioned-resource-preview-banner.title-dashboard-loaded-pull-request-git-hub', - 'This dashboard is loaded from a pull request in GitHub.' - ); +export function PreviewBannerViewPR({ prParam, isNewPr }: Props) { + const titleText = isNewPr + ? t( + 'provisioned-resource-preview-banner.title-created-branch-git-hub', + 'A new resource has been created in a branch in GitHub.' + ) + : t( + 'provisioned-resource-preview-banner.title-loaded-pull-request-git-hub', + 'This resource is loaded from a pull request in GitHub.' + ); return ( <Alert diff --git a/public/app/features/dashboard-scene/saving/provisioned/hooks.ts b/public/app/features/dashboard-scene/saving/provisioned/hooks.ts index a274c9c7f88..e82ebcee548 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/hooks.ts +++ b/public/app/features/dashboard-scene/saving/provisioned/hooks.ts @@ -4,7 +4,7 @@ import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; import { useUrlParams } from 'app/core/navigation/hooks'; import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, AnnoKeySourcePath } from 'app/features/apiserver/types'; import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { DashboardScene } from '../../scene/DashboardScene'; import { ProvisionedDashboardFormData } from '../shared'; diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index 97a50aa3621..8f75ecf0751 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -10,8 +10,8 @@ import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { SaveDashboardAsOptions, SaveDashboardOptions } from 'app/features/dashboard/components/SaveDashboard/types'; -import { useDispatch } from 'app/types'; import { DashboardSavedEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { updateDashboardUidLastUsedDatasource } from '../../dashboard/utils/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts b/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts index 643ea0ac56f..f939f4a2a24 100644 --- a/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts +++ b/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts @@ -19,7 +19,7 @@ import { Annotation } from 'app/features/alerting/unified/utils/constants'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { prometheusRuleType } from 'app/features/alerting/unified/utils/rules'; import { dispatch } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RuleNamespace } from 'app/types/unified-alerting'; import { PromAlertingRuleState, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 2d5a2901b06..46e67142648 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -31,7 +31,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DashboardJson } from 'app/features/manage-dashboards/types'; import { VariablesChanged } from 'app/features/variables/types'; -import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardMeta, KioskMode, SaveDashboardResponseDTO, DashboardDTO } from 'app/types/dashboard'; import { ShowConfirmModalEvent } from 'app/types/events'; import { diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index bd300a5bbd3..19a6916552c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -6,7 +6,7 @@ import { ScopesContext } from '@grafana/runtime'; import { SceneComponentProps } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { DashboardEditPaneSplitter } from '../edit-pane/DashboardEditPaneSplitter'; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts index 4a7e9fca190..9dbd47cf919 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts @@ -1,7 +1,7 @@ import { AppEvents } from '@grafana/data'; import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; -import { KioskMode } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { getCloneKey } from '../utils/clone'; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 1740fe87c8a..3d9acceed0b 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -5,7 +5,7 @@ import { config, locationService } from '@grafana/runtime'; import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/core'; -import { KioskMode } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; diff --git a/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx b/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx index 3e6761dfed0..fe025098988 100644 --- a/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx +++ b/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx @@ -1,6 +1,6 @@ import { Badge } from '@grafana/ui'; import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; export default function ManagedDashboardNavBarBadge({ meta }: { meta: DashboardMeta }) { const obj = meta.k8s; diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index abca5175425..3a6e2f7935c 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { LocationServiceProvider, config, locationService } from '@grafana/runtime'; import { SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 2e36a248586..49b4a3c9f53 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -13,7 +13,7 @@ import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { contextSrv } from 'app/core/core'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { shareDashboardType } from '../../dashboard/components/ShareModal/utils'; import { selectFolderRepository } from '../../provisioning/utils/selectors'; diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 998541edf80..10191b32a58 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -23,7 +23,7 @@ import { GetExploreUrlArguments } from 'app/core/utils/explore'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form'; import * as storeModule from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 8b07ede1cb8..d10b2ab5b65 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -29,7 +29,7 @@ import { pluginExtensionRegistries } from 'app/features/plugins/extensions/regis import { GetPluginExtensions } from 'app/features/plugins/extensions/types'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; import { dispatch } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowConfirmModalEvent } from 'app/types/events'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index cde032a413e..0d7ad8f85a8 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -4,7 +4,7 @@ import { sceneGraph, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { KeybindingSet } from 'app/core/services/KeybindingSet'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { shareDashboardType } from '../../dashboard/components/ShareModal/utils'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx index 00c09366d37..66eec343ea3 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx @@ -15,7 +15,7 @@ const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButt export const ShareDashboardButton = ({ dashboard }: ToolbarActionProps) => { const [_, buildUrl] = useAsyncFn(async () => { DashboardInteractions.toolbarShareClick(); - return await buildShareUrl(dashboard); + await buildShareUrl(dashboard); }, [dashboard]); return ( diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 4157352dd3a..ab99200417d 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -14,9 +14,7 @@ import { defaultPanelSpec, defaultTimeSettingsSpec, GridLayoutKind, - PanelKind, PanelSpec, - QueryVariableKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types'; @@ -50,13 +48,11 @@ jest.mock('@grafana/runtime', () => ({ name: 'Grafana', meta: { id: 'grafana' }, type: 'datasource', - uid: 'grafana', }, prometheus: { name: 'prometheus', meta: { id: 'prometheus' }, type: 'datasource', - uid: 'prometheus-uid', }, }, }, @@ -894,226 +890,6 @@ describe('DashboardSceneSerializer', () => { }, }); }); - - describe('data source references persistence', () => { - it('should not fill data source references for annotations when input did not contain it', () => { - const dashboard = setupV2({ - annotations: [ - { - kind: 'AnnotationQuery', - spec: { - builtIn: false, - enable: true, - hide: false, - iconColor: 'blue', - name: 'prom-annotations', - query: { - group: 'prometheus', - kind: 'DataQuery', - spec: { - refId: 'Anno', - }, - version: 'v0', - }, - }, - }, - ], - }); - const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); - // referencing index 1 as transformation adds built in annotation query - expect(saveAsModel.annotations[1].spec.query.datasource).toBeUndefined(); - }); - - it('should fill data source references for annotations when input did contain it', () => { - const dashboard = setupV2({ - annotations: [ - { - kind: 'AnnotationQuery', - spec: { - builtIn: false, - enable: true, - hide: false, - iconColor: 'blue', - name: 'prom-annotations', - query: { - group: 'prometheus', - kind: 'DataQuery', - datasource: { - name: 'prometheus-uid', - }, - spec: { - refId: 'Anno', - }, - version: 'v0', - }, - }, - }, - ], - }); - const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); - // referencing index 1 as transformation adds built in annotation query - expect(saveAsModel.annotations[1].spec.query.datasource).toEqual({ - name: 'prometheus-uid', - }); - }); - it('should not fill data source references for panel queries when input did not contain it', () => { - const dashboard = setupV2({ - elements: { - 'panel-1': { - kind: 'Panel', - spec: { - ...defaultPanelSpec(), - data: { - kind: 'QueryGroup', - spec: { - transformations: [], - queryOptions: {}, - queries: [ - { - kind: 'PanelQuery', - spec: { - refId: 'A', - hidden: false, - query: { - kind: 'DataQuery', - group: 'prometheus', - version: 'v0', - spec: {}, - }, - }, - }, - ], - }, - }, - }, - }, - }, - }); - const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); - expect( - (saveAsModel.elements['panel-1'] as PanelKind).spec.data.spec.queries[0].spec.query.datasource - ).toBeUndefined(); - }); - - it('should fill data source references for panel queries when input did contain it', () => { - const dashboard = setupV2({ - elements: { - 'panel-1': { - kind: 'Panel', - spec: { - ...defaultPanelSpec(), - data: { - kind: 'QueryGroup', - spec: { - transformations: [], - queryOptions: {}, - queries: [ - { - kind: 'PanelQuery', - spec: { - refId: 'A', - hidden: false, - query: { - kind: 'DataQuery', - group: 'prometheus', - version: 'v0', - datasource: { - name: 'prometheus-uid', - }, - spec: {}, - }, - }, - }, - ], - }, - }, - }, - }, - }, - }); - const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); - expect( - (saveAsModel.elements['panel-1'] as PanelKind).spec.data.spec.queries[0].spec.query.datasource - ).toEqual({ - name: 'prometheus-uid', - }); - }); - - it('should not fill data source references for query variables when input did contain it', () => { - const queryVariable: QueryVariableKind = { - kind: 'QueryVariable', - spec: { - name: 'app', - current: { - text: 'app1', - value: 'app1', - }, - hide: 'dontHide', - includeAll: false, - label: 'Query Variable', - skipUrlSync: false, - regex: '', - definition: '', - options: [], - refresh: 'never', - sort: 'alphabeticalAsc', - multi: false, - allowCustomValue: true, - query: { - kind: 'DataQuery', - group: 'prometheus', - version: 'v0', - spec: {}, - }, - }, - }; - const dashboard = setupV2({ - variables: [queryVariable], - }); - const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); - expect((saveAsModel.variables[0] as QueryVariableKind).spec.query.datasource).toBeUndefined(); - }); - - it('should fill data source references for query variables when input did contain it', () => { - const queryVariable: QueryVariableKind = { - kind: 'QueryVariable', - spec: { - name: 'app', - current: { - text: 'app1', - value: 'app1', - }, - hide: 'dontHide', - includeAll: false, - label: 'Query Variable', - skipUrlSync: false, - regex: '', - definition: '', - options: [], - refresh: 'never', - sort: 'alphabeticalAsc', - multi: false, - allowCustomValue: true, - query: { - kind: 'DataQuery', - group: 'prometheus', - version: 'v0', - datasource: { - name: 'prometheus-uid', - }, - spec: {}, - }, - }, - }; - const dashboard = setupV2({ - variables: [queryVariable], - }); - const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); - expect((saveAsModel.variables[0] as QueryVariableKind).spec.query.datasource).toEqual({ - name: 'prometheus-uid', - }); - }); - }); }); describe('panel mapping methods', () => { diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index e96284c1008..9e73bed659e 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -12,7 +12,7 @@ import { getV2SchemaVariables, } from 'app/features/dashboard/utils/tracking'; import { DashboardJson } from 'app/features/manage-dashboards/types'; -import { DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardMeta, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { getRawDashboardChanges, getRawDashboardV2Changes } from '../saving/getDashboardChanges'; import { DashboardChangeInfo } from '../saving/shared'; diff --git a/public/app/features/dashboard-scene/serialization/annotations.test.ts b/public/app/features/dashboard-scene/serialization/annotations.test.ts deleted file mode 100644 index f054c7a22a3..00000000000 --- a/public/app/features/dashboard-scene/serialization/annotations.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { AnnotationQuery } from '@grafana/data'; -import { AnnotationQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; - -import { transformV1ToV2AnnotationQuery, transformV2ToV1AnnotationQuery } from './annotations'; - -describe('V1<->V2 annotation convertions', () => { - test('given grafana-built in annotations', () => { - // test case - const annotationDefinition: AnnotationQuery = { - builtIn: 1, - datasource: { - type: 'grafana', - uid: 'grafana', - }, - enable: true, - hide: false, - iconColor: 'yellow', - name: 'Annotations \u0026 Alerts', - target: { - // @ts-expect-error - limit: 100, - matchAny: false, - tags: [], - type: 'dashboard', - }, - type: 'dashboard', - }; - - const expectedV2: AnnotationQueryKind = { - kind: 'AnnotationQuery', - spec: { - builtIn: true, - enable: true, - hide: false, - iconColor: 'yellow', - name: 'Annotations \u0026 Alerts', - query: { - kind: 'DataQuery', - group: 'grafana', - version: 'v0', - datasource: { - name: 'grafana', - }, - spec: { - limit: 100, - matchAny: false, - tags: [], - type: 'dashboard', - }, - }, - }, - }; - - const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'grafana', 'grafana'); - - expect(resultV2).toEqual(expectedV2); - - const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2); - expect(resultV1).toEqual(annotationDefinition); - }); - - test('given annotations with datasource', () => { - const annotationDefinition = { - datasource: { - type: 'grafana-testdata-datasource', - uid: 'uid', - }, - enable: true, - hide: false, - iconColor: 'blue', - name: 'testdata-annos', - target: { - lines: 10, - refId: 'Anno', - scenarioId: 'annotations', - }, - }; - - const expectedV2: AnnotationQueryKind = { - kind: 'AnnotationQuery', - spec: { - enable: true, - hide: false, - iconColor: 'blue', - name: 'testdata-annos', - builtIn: false, - query: { - kind: 'DataQuery', - group: 'grafana-testdata-datasource', - version: 'v0', - datasource: { - name: 'uid', - }, - spec: { - lines: 10, - refId: 'Anno', - scenarioId: 'annotations', - }, - }, - }, - }; - - const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery( - annotationDefinition, - 'grafana-testdata-datasource', - 'uid' - ); - - expect(resultV2).toEqual(expectedV2); - - const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2); - expect(resultV1).toEqual(annotationDefinition); - }); - - test('given annotations with target', () => { - const annotationDefinition = { - datasource: { - type: 'prometheus', - uid: 'uid', - }, - enable: true, - hide: false, - iconColor: 'yellow', - name: 'prom-annos', - target: { - expr: '{action="add_client"}', - interval: '', - lines: 10, - refId: 'Anno', - scenarioId: 'annotations', - }, - }; - - const expectedV2: AnnotationQueryKind = { - kind: 'AnnotationQuery', - spec: { - enable: true, - hide: false, - iconColor: 'yellow', - name: 'prom-annos', - builtIn: false, - query: { - kind: 'DataQuery', - group: 'prometheus', - version: 'v0', - datasource: { - name: 'uid', - }, - spec: { - expr: '{action="add_client"}', - interval: '', - lines: 10, - refId: 'Anno', - scenarioId: 'annotations', - }, - }, - }, - }; - - const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'prometheus', 'uid'); - expect(resultV2).toEqual(expectedV2); - - const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2); - expect(resultV1).toEqual(annotationDefinition); - }); - - test('given annotations with non-schematised options / legacyOptions', () => { - const annotationDefinition = { - datasource: { - type: 'elasticsearch', - uid: 'uid', - }, - enable: true, - hide: false, - iconColor: 'red', - name: 'elastic - annos', - tagsField: 'asd', - target: { - lines: 10, - query: 'test query', - refId: 'Anno', - scenarioId: 'annotations', - }, - textField: 'asd', - timeEndField: 'asdas', - timeField: 'asd', - }; - - const expectedV2: AnnotationQueryKind = { - kind: 'AnnotationQuery', - spec: { - enable: true, - hide: false, - iconColor: 'red', - name: 'elastic - annos', - builtIn: false, - query: { - kind: 'DataQuery', - group: 'elasticsearch', - version: 'v0', - datasource: { - name: 'uid', - }, - spec: { - lines: 10, - query: 'test query', - refId: 'Anno', - scenarioId: 'annotations', - }, - }, - legacyOptions: { - tagsField: 'asd', - textField: 'asd', - timeEndField: 'asdas', - timeField: 'asd', - }, - }, - }; - - const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'elasticsearch', 'uid'); - expect(resultV2).toEqual(expectedV2); - - const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2); - expect(resultV1).toEqual(annotationDefinition); - }); -}); diff --git a/public/app/features/dashboard-scene/serialization/annotations.ts b/public/app/features/dashboard-scene/serialization/annotations.ts deleted file mode 100644 index 7fba058df19..00000000000 --- a/public/app/features/dashboard-scene/serialization/annotations.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { AnnotationQuery } from '@grafana/data'; -import { - AnnotationQueryKind, - defaultDataQueryKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; - -import { getRuntimePanelDataSource } from './layoutSerializers/utils'; - -export function transformV1ToV2AnnotationQuery( - annotation: AnnotationQuery, - - dsType: string, - dsUID?: string, - // Overrides are used to provide properties based on scene's annotations data layer object state - override?: Partial<AnnotationQuery> -): AnnotationQueryKind { - const group = annotation.builtIn ? 'grafana' : dsType; - - const { - // known properties documented in v1 schema - enable, - hide, - iconColor, - name, - builtIn, - filter, - mappings, - datasource, - target, - snapshotData, - type, - - // unknown properties that are still available for configuration through API - ...legacyOptions - } = annotation; - - const result: AnnotationQueryKind = { - kind: 'AnnotationQuery', - spec: { - builtIn: Boolean(annotation.builtIn), - name: annotation.name, - enable: Boolean(override?.enable) || Boolean(annotation.enable), - hide: Boolean(override?.hide) || Boolean(annotation.hide), - iconColor: annotation.iconColor, - - query: { - kind: 'DataQuery', - version: defaultDataQueryKind().version, - group, // Annotation layer has a datasource type provided in runtime. - spec: target || {}, - }, - }, - }; - - if (dsUID) { - result.spec.query.datasource = { - name: dsUID, - }; - } - - // if legacy options is not an empty object, add it to the result - if (Object.keys(legacyOptions).length > 0) { - result.spec.legacyOptions = legacyOptions; - } - - if (annotation.filter?.ids?.length) { - result.spec.filter = annotation.filter; - } - - // TODO: add mappings - - return result; -} - -export function transformV2ToV1AnnotationQuery(annotation: AnnotationQueryKind): AnnotationQuery { - let { query: dataQuery, ...annotationQuery } = annotation.spec; - - // Mapping from AnnotationQueryKind to AnnotationQuery used by scenes. - let annoQuerySpec: AnnotationQuery = { - enable: annotation.spec.enable, - hide: annotation.spec.hide, - iconColor: annotation.spec.iconColor, - name: annotation.spec.name, - // TOOO: mappings - }; - - if (Object.keys(dataQuery.spec).length > 0) { - // @ts-expect-error DataQueryKind spec should be typed as DataQuery interface - annoQuerySpec.target = { - ...dataQuery?.spec, - }; - } - - if (annotation.spec.builtIn) { - annoQuerySpec.type = 'dashboard'; - annoQuerySpec.builtIn = 1; - } - - if (annotation.spec.filter) { - annoQuerySpec.filter = annotation.spec.filter; - } - - // some annotations will contain in the legacyOptions properties that need to be - // added to the root level AnnotationQuery - if (annotationQuery.legacyOptions) { - annoQuerySpec = { - ...annoQuerySpec, - ...annotationQuery.legacyOptions, - }; - } - - // get data source from annotation query - const datasource = getRuntimePanelDataSource(dataQuery); - - annoQuerySpec.datasource = datasource; - - return annoQuerySpec; -} diff --git a/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts b/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts index 705ab15ddee..436563c8379 100644 --- a/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts @@ -13,7 +13,7 @@ import { import { AnnoKeyFolder } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; export async function buildNewDashboardSaveModel(urlFolderUid?: string): Promise<DashboardDTO> { let variablesList = defaultDashboard.templating?.list; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 07f1359bc7a..c78cbd4c480 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -781,8 +781,8 @@ describe('transformSaveModelSchemaV2ToScene', () => { enable: true, iconColor: 'rgba(0, 211, 255, 1)', name: 'Annotations & Alerts', + filter: undefined, hide: true, - type: 'dashboard', }); const annotationLayer = dataLayerSet.state.annotationLayers[1] as DashboardAnnotationsDataLayer; @@ -794,6 +794,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { type: 'prometheus', }, name: 'Annotation with legacy options', + builtIn: 0, enable: true, hide: false, iconColor: 'purple', @@ -803,6 +804,15 @@ describe('transformSaveModelSchemaV2ToScene', () => { useValueAsTime: true, step: '1m', }); + + // Verify the original legacyOptions object is also preserved + expect(annotationLayer.state.query.legacyOptions).toMatchObject({ + expr: 'rate(http_requests_total[5m])', + queryType: 'range', + legendFormat: '{{method}} {{endpoint}}', + useValueAsTime: true, + step: '1m', + }); }); }); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index f37598bb45e..f0ed00957f2 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -1,5 +1,6 @@ import { uniqueId } from 'lodash'; +import { AnnotationQuery } from '@grafana/data'; import { config, getDataSourceSrv } from '@grafana/runtime'; import { AdHocFiltersVariable, @@ -51,7 +52,7 @@ import { DeprecatedInternalId, } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; @@ -63,10 +64,9 @@ import { DashboardScene } from '../scene/DashboardScene'; import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { getIntervalsFromQueryString } from '../utils/utils'; -import { transformV2ToV1AnnotationQuery } from './annotations'; import { SnapshotVariable } from './custom-variables/SnapshotVariable'; import { layoutDeserializerRegistry } from './layoutSerializers/layoutSerializerRegistry'; -import { getRuntimeVariableDataSource } from './layoutSerializers/utils'; +import { getRuntimePanelDataSource, getRuntimeVariableDataSource } from './layoutSerializers/utils'; import { registerPanelInteractionsReporter } from './transformSaveModelToScene'; import { transformCursorSyncV2ToV1, @@ -92,17 +92,47 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D const { spec: dashboard, metadata, apiVersion } = dto; // annotations might not come with the builtIn Grafana annotation, we need to add it + const grafanaBuiltAnnotation = getGrafanaBuiltInAnnotationDataLayer(dashboard); if (grafanaBuiltAnnotation) { dashboard.annotations.unshift(grafanaBuiltAnnotation); } const annotationLayers = dashboard.annotations.map((annotation) => { - const annotationQuerySpec = transformV2ToV1AnnotationQuery(annotation); + let { query: dataQuery, ...annotationQuery } = annotation.spec; + + // Mapping from AnnotationQueryKind to AnnotationQuery used by scenes. + let annoQuerySpec: AnnotationQuery = { + builtIn: annotation.spec.builtIn ? 1 : 0, + enable: annotation.spec.enable, + iconColor: annotation.spec.iconColor, + name: annotation.spec.name, + filter: annotation.spec.filter, + hide: annotation.spec.hide, + ...dataQuery?.spec, + }; + + // some annotations will contain in the legacyOptions properties that need to be + // added to the root level annotation spec + if (annotationQuery.legacyOptions) { + annoQuerySpec = { + ...annoQuerySpec, + ...annotationQuery.legacyOptions, + legacyOptions: { + ...annotationQuery.legacyOptions, + }, + }; + } + + // get data source from annotation query + const datasource = getRuntimePanelDataSource(dataQuery); const layerState = { key: uniqueId('annotations-'), - query: annotationQuerySpec, + query: { + ...annoQuerySpec, + datasource, + }, name: annotation.spec.name, isEnabled: Boolean(annotation.spec.enable), isHidden: Boolean(annotation.spec.hide), diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index cc1e2ee2ec1..ef03bdd17ee 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -25,7 +25,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { createPanelSaveModel } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 2c51bee4427..2936ccfcf49 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -25,7 +25,7 @@ import { contextSrv } from 'app/core/core'; import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { DashboardDTO, DashboardDataDTO } from 'app/types'; +import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 9b6ec578234..9d4fbea9aa2 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -21,7 +21,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; import { reduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 94c7aae659a..ad6137da1e5 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -561,8 +561,16 @@ describe('transformSceneToSaveModelSchemaV2', () => { name: 'annotation-with-options', enable: true, iconColor: 'red', - customProp1: true, - customProp2: 'test', + legacyOptions: { + expr: 'rate(http_requests_total[5m])', + queryType: 'range', + legendFormat: '{{method}} {{endpoint}}', + useValueAsTime: true, + }, + // Some other properties that aren't in the annotation spec + // and should be moved to options + customProp1: 'value1', + customProp2: 'value2', }, name: 'layerWithOptions', isEnabled: true, @@ -584,11 +592,19 @@ describe('transformSceneToSaveModelSchemaV2', () => { expect(result.annotations.length).toBe(1); expect(result.annotations[0].spec.legacyOptions).toBeDefined(); expect(result.annotations[0].spec.legacyOptions).toEqual({ - customProp1: true, - customProp2: 'test', + expr: 'rate(http_requests_total[5m])', + queryType: 'range', + legendFormat: '{{method}} {{endpoint}}', + useValueAsTime: true, + customProp1: 'value1', + customProp2: 'value2', }); // Ensure these properties are not at the root level + expect(result).not.toHaveProperty('annotations[0].spec.expr'); + expect(result).not.toHaveProperty('annotations[0].spec.queryType'); + expect(result).not.toHaveProperty('annotations[0].spec.legendFormat'); + expect(result).not.toHaveProperty('annotations[0].spec.useValueAsTime'); expect(result).not.toHaveProperty('annotations[0].spec.customProp1'); expect(result).not.toHaveProperty('annotations[0].spec.customProp2'); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 0dae10574b6..a7682c4e559 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -52,7 +52,6 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils'; import { DSReferencesMapping } from './DashboardSceneSerializer'; -import { transformV1ToV2AnnotationQuery } from './annotations'; import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables'; import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils'; @@ -419,46 +418,114 @@ function getAnnotations(state: DashboardSceneState, dsReferencesMapping?: DSRefe if (!(layer instanceof dataLayers.AnnotationsDataLayer)) { continue; } - const datasource = getElementDatasource(layer, layer.state.query, 'annotation', undefined, dsReferencesMapping); - let layerDs = layer.state.query.datasource; - + const layerDs = layer.state.query.datasource; if (!layerDs) { - // This can happen only if we are transforming a scene that was created - // from a v1 spec. In v1 annotation layer can contain no datasource ref, which is guaranteed - // for layers created for v2 schema. See transform transformSaveModelSchemaV2ToScene.ts. - // In this case we will resolve default data source - layerDs = getDefaultDataSourceRef(); - console.error( - 'Misconfigured AnnotationsDataLayer: Data source is required for annotations. Resolving default data source', - layer, - layerDs - ); + throw new Error('Misconfigured AnnotationsDataLayer: Datasource is required for annotations'); } - const result = transformV1ToV2AnnotationQuery(layer.state.query, layerDs.type!, layerDs.uid!, { - enable: layer.state.isEnabled, - hide: layer.state.isHidden, - }); + const result: AnnotationQueryKind = { + kind: 'AnnotationQuery', + spec: { + builtIn: Boolean(layer.state.query.builtIn), + name: layer.state.query.name, + enable: Boolean(layer.state.isEnabled), + hide: Boolean(layer.state.isHidden), + iconColor: layer.state.query.iconColor, + query: { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: layerDs.type!, // Annotation layer has a datasource type provided in runtime. + spec: {}, + }, + }, + }; - const annotationQuery = layer.state.query; + if (datasource) { + result.spec.query!.datasource = { + name: datasource.uid, + }; + } + + // Transform v1 dashboard (using target) to v2 structure + // adds extra condition to prioritize query over target + // if query is defined, use it + if (layer.state.query.target && !layer.state.query.query) { + // Handle built-in annotations + if (layer.state.query.builtIn) { + result.spec.query = { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'grafana', // built-in annotations are always of type grafana + spec: { + ...layer.state.query.target, + }, + }; + } else { + result.spec.query = { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: datasource?.type!, + spec: { + ...layer.state.query.target, + }, + }; + + if (layer.state.query.datasource?.uid) { + result.spec.query.datasource = { + name: layer.state.query.datasource?.uid, + }; + } + } + } + // For annotations without query.query defined (e.g., grafana annotations without tags) + else if (layer.state.query.query?.kind) { + result.spec.query = { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: layer.state.query.query.group || getAnnotationQueryKind(layer.state.query), + datasource: layer.state.query.query.datasource, + spec: { + ...layer.state.query.query.spec, + }, + }; + } + // Collect datasource-specific properties not in standard annotation spec + let otherProps = omit( + layer.state.query, + 'type', + 'target', + 'builtIn', + 'name', + 'datasource', + 'iconColor', + 'enable', + 'hide', + 'filter', + 'query' + ); + + // Store extra properties in the legacyOptions field instead of directly in the spec + if (Object.keys(otherProps).length > 0) { + // // Extract options property and get the rest of the properties + const { legacyOptions, ...restProps } = otherProps; + if (legacyOptions) { + // Merge options with the rest of the properties + result.spec.legacyOptions = { ...legacyOptions, ...restProps }; + } + result.spec.query!.spec = { + ...otherProps, + }; + } // If filter is an empty array, don't save it - if (annotationQuery.filter?.ids?.length) { - result.spec.filter = annotationQuery.filter; - } - - // Finally, if the datasource references mapping did not containt data source ref, - // this means that the original model that was fetched did not contain it. In such scenario we don't want to save - // the explicit data source reference, so lets remove it from the save model. - if (!datasource) { - delete result.spec.query.datasource; + if (layer.state.query.filter?.ids?.length) { + result.spec.filter = layer.state.query.filter; } annotations.push(result); } - return annotations; } diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index 228240f721b..ad47d202db9 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -31,9 +31,16 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; import { DeleteDashboardButton } from './DeleteDashboardButton'; +import { MoveProvisionedDashboardDrawer } from './MoveProvisionedDashboardDrawer'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; -export interface GeneralSettingsEditViewState extends DashboardEditViewState {} +export interface GeneralSettingsEditViewState extends DashboardEditViewState { + showMoveModal?: boolean; + moveModalProps?: { + targetFolderUID?: string; + targetFolderTitle?: string; + }; +} export class GeneralSettingsEditView extends SceneObjectBase<GeneralSettingsEditViewState> @@ -156,10 +163,40 @@ export class GeneralSettingsEditView public onDeleteDashboard = () => {}; + public onProvisionedFolderChange = async (newUID?: string, newTitle?: string) => { + if (newUID !== this._dashboard.state.meta.folderUid) { + this.setState({ + showMoveModal: true, + moveModalProps: { + targetFolderUID: newUID, + targetFolderTitle: newTitle, + }, + }); + } + }; + + public onMoveModalDismiss = () => { + this.setState({ + showMoveModal: false, + moveModalProps: undefined, + }); + }; + + private onMoveSuccess = (folderUID: string, folderTitle: string) => { + const newMeta = { + ...this._dashboard.state.meta, + folderUid: folderUID, + folderTitle: folderTitle, + }; + this._dashboard.setState({ meta: newMeta }); + this.onMoveModalDismiss(); + }; + static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => { const dashboard = model.getDashboard(); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const { title, description, tags, meta, editable } = dashboard.useState(); + const { showMoveModal, moveModalProps } = model.useState(); const { sync: graphTooltip } = model.getCursorSync()?.useState() || {}; const { timeZone, weekStart, UNSAFE_nowDelay: nowDelay } = model.getTimeRange().useState(); const { intervals } = model.getRefreshPicker().useState(); @@ -201,8 +238,9 @@ export class GeneralSettingsEditView <Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <NavToolbarActions dashboard={dashboard} /> <div style={{ maxWidth: '600px' }}> - <Box marginBottom={5}> + <Box display="flex" direction="column" gap={2} marginBottom={5}> <Field + noMargin label={ <Stack justifyContent="space-between"> <Label htmlFor="title-input"> @@ -222,6 +260,7 @@ export class GeneralSettingsEditView /> </Field> <Field + noMargin label={ <Stack justifyContent="space-between"> <Label htmlFor="description-input"> @@ -240,18 +279,18 @@ export class GeneralSettingsEditView onChange={(e: ChangeEvent<HTMLTextAreaElement>) => model.onDescriptionChange(e.target.value)} /> </Field> - <Field label={t('dashboard-settings.general.tags-label', 'Tags')}> + <Field noMargin label={t('dashboard-settings.general.tags-label', 'Tags')}> <TagsInput id="tags-input" tags={tags} onChange={model.onTagsChange} width={40} /> </Field> - <Field label={t('dashboard-settings.general.folder-label', 'Folder')}> - {dashboard.isManagedRepository() ? ( - <Input readOnly value={meta.folderTitle} /> - ) : ( - <FolderPicker value={meta.folderUid} onChange={model.onFolderChange} /> - )} + <Field noMargin label={t('dashboard-settings.general.folder-label', 'Folder')}> + <FolderPicker + value={meta.folderUid} + onChange={dashboard.isManagedRepository() ? model.onProvisionedFolderChange : model.onFolderChange} + /> </Field> <Field + noMargin label={t('dashboard-settings.general.editable-label', 'Editable')} description={t( 'dashboard-settings.general.editable-description', @@ -282,33 +321,51 @@ export class GeneralSettingsEditView label={t('dashboard-settings.general.panel-options-label', 'Panel options')} isOpen={true} > - <Field - label={t('dashboard-settings.general.panel-options-graph-tooltip-label', 'Graph tooltip')} - description={t( - 'dashboard-settings.general.panel-options-graph-tooltip-description', - 'Controls tooltip and hover highlight behavior across different panels. Reload the dashboard for changes to take effect' - )} - > - <RadioButtonGroup onChange={model.onTooltipChange} options={GRAPH_TOOLTIP_OPTIONS} value={graphTooltip} /> - </Field> + <Stack direction="column" gap={2}> + <Field + noMargin + label={t('dashboard-settings.general.panel-options-graph-tooltip-label', 'Graph tooltip')} + description={t( + 'dashboard-settings.general.panel-options-graph-tooltip-description', + 'Controls tooltip and hover highlight behavior across different panels. Reload the dashboard for changes to take effect' + )} + > + <RadioButtonGroup + onChange={model.onTooltipChange} + options={GRAPH_TOOLTIP_OPTIONS} + value={graphTooltip} + /> + </Field> - <Field - label={t('dashboard-settings.general.panels-preload-label', 'Preload panels')} - description={t( - 'dashboard-settings.general.panels-preload-description', - 'When enabled all panels will start loading as soon as the dashboard has been loaded.' - )} - > - <Switch - id="preload-panels-dashboards-toggle" - value={dashboard.state.preload} - onChange={(e) => model.onPreloadChange(e.currentTarget.checked)} - /> - </Field> + <Field + noMargin + label={t('dashboard-settings.general.panels-preload-label', 'Preload panels')} + description={t( + 'dashboard-settings.general.panels-preload-description', + 'When enabled all panels will start loading as soon as the dashboard has been loaded.' + )} + > + <Switch + id="preload-panels-dashboards-toggle" + value={dashboard.state.preload} + onChange={(e) => model.onPreloadChange(e.currentTarget.checked)} + /> + </Field> + </Stack> </CollapsableSection> <Box marginTop={3}>{meta.canDelete && <DeleteDashboardButton dashboard={dashboard} />}</Box> </div> + + {showMoveModal && moveModalProps && ( + <MoveProvisionedDashboardDrawer + dashboard={dashboard} + targetFolderUID={moveModalProps.targetFolderUID} + targetFolderTitle={moveModalProps.targetFolderTitle} + onDismiss={model.onMoveModalDismiss} + onSuccess={model.onMoveSuccess} + /> + )} </Page> ); }; diff --git a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx index 803068b979d..c6b40b81ff0 100644 --- a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx +++ b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx @@ -11,7 +11,7 @@ import { Page } from 'app/core/components/Page/Page'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; import { getPrettyJSON } from 'app/features/inspector/utils/utils'; -import { DashboardDataDTO, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardDataDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { NameAlreadyExistsError, diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx new file mode 100644 index 00000000000..b1caef27bf5 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx @@ -0,0 +1,43 @@ +import { useProvisionedDashboardData } from '../saving/provisioned/hooks'; +import { DashboardScene } from '../scene/DashboardScene'; + +import { MoveProvisionedDashboardForm } from './MoveProvisionedDashboardForm'; + +export interface Props { + dashboard: DashboardScene; + targetFolderUID?: string; + targetFolderTitle?: string; + onDismiss: () => void; + onSuccess: (folderUID: string, folderTitle: string) => void; +} + +export function MoveProvisionedDashboardDrawer({ + dashboard, + targetFolderUID, + targetFolderTitle, + onDismiss, + onSuccess, +}: Props) { + const { defaultValues, loadedFromRef, readOnly, isGitHub, workflowOptions, isNew } = + useProvisionedDashboardData(dashboard); + + if (!defaultValues) { + return null; + } + + return ( + <MoveProvisionedDashboardForm + dashboard={dashboard} + defaultValues={defaultValues} + loadedFromRef={loadedFromRef} + readOnly={readOnly} + isGitHub={isGitHub} + isNew={isNew} + workflowOptions={workflowOptions} + targetFolderUID={targetFolderUID} + targetFolderTitle={targetFolderTitle} + onDismiss={onDismiss} + onSuccess={onSuccess} + /> + ); +} diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx new file mode 100644 index 00000000000..1e6496d4229 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx @@ -0,0 +1,241 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { getAppEvents } from '@grafana/runtime'; +import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; +import { + useCreateRepositoryFilesWithPathMutation, + useDeleteRepositoryFilesWithPathMutation, + useGetRepositoryFilesWithPathQuery, +} from 'app/api/clients/provisioning/v0alpha1'; +import { AnnoKeySourcePath } from 'app/features/apiserver/types'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler'; + +import { MoveProvisionedDashboardForm, Props } from './MoveProvisionedDashboardForm'; + +jest.mock('@grafana/runtime', () => { + const actual = jest.requireActual('@grafana/runtime'); + return { + ...actual, + getAppEvents: jest.fn(), + }; +}); + +jest.mock('app/api/clients/provisioning/v0alpha1', () => ({ + useGetRepositoryFilesWithPathQuery: jest.fn(), + useCreateRepositoryFilesWithPathMutation: jest.fn(), + useDeleteRepositoryFilesWithPathMutation: jest.fn(), +})); + +jest.mock('app/api/clients/folder/v1beta1', () => ({ + useGetFolderQuery: jest.fn(), +})); + +jest.mock('../utils/useProvisionedRequestHandler', () => ({ + useProvisionedRequestHandler: jest.fn(), +})); + +jest.mock('react-router-dom-v5-compat', () => ({ + useNavigate: () => jest.fn(), +})); + +jest.mock('../components/Provisioned/ResourceEditFormSharedFields', () => ({ + ResourceEditFormSharedFields: () => <div data-testid="resource-edit-form" />, +})); + +function setup(props: Partial<Props> = {}) { + const user = userEvent.setup(); + + const mockDashboard = { + useState: jest.fn().mockReturnValue({ + editPanel: null, + }), + state: { + title: 'Test Dashboard', + }, + } as unknown as DashboardScene; + + const defaultProps: Props = { + dashboard: mockDashboard, + defaultValues: { + repo: 'test-repo', + path: 'folder1/dashboard.json', + ref: 'main', + workflow: 'write', + comment: '', + title: 'Test Dashboard', + description: '', + folder: { uid: '', title: '' }, + }, + readOnly: false, + isGitHub: true, + workflowOptions: [ + { label: 'Write', value: 'write' }, + { label: 'Branch', value: 'branch' }, + ], + targetFolderUID: 'target-folder-uid', + targetFolderTitle: 'Target Folder', + onDismiss: jest.fn(), + onSuccess: jest.fn(), + ...props, + }; + + return { + user, + ...render(<MoveProvisionedDashboardForm {...defaultProps} />), + props: defaultProps, + }; +} + +const mockCreateRequest = { + isSuccess: false, + isError: false, + isLoading: false, + error: null, +}; + +const mockDeleteRequest = { + isSuccess: false, + isError: false, + isLoading: false, + error: null, +}; + +describe('MoveProvisionedDashboardForm', () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Setup default mocks + const mockAppEvents = { + publish: jest.fn(), + }; + (getAppEvents as jest.Mock).mockReturnValue(mockAppEvents); + + // Mock hooks + (useGetRepositoryFilesWithPathQuery as jest.Mock).mockReturnValue({ + data: { + resource: { + file: { spec: { title: 'Test Dashboard' } }, + dryRun: { + metadata: { + annotations: { + [AnnoKeySourcePath]: 'folder1/dashboard.json', + }, + }, + }, + }, + }, + isLoading: false, + }); + + (useGetFolderQuery as jest.Mock).mockReturnValue({ + data: { + metadata: { + annotations: { + [AnnoKeySourcePath]: 'target-folder', + }, + }, + }, + isLoading: false, + }); + + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([jest.fn(), mockCreateRequest]); + + (useDeleteRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([jest.fn(), mockDeleteRequest]); + + (useProvisionedRequestHandler as jest.Mock).mockReturnValue(undefined); + }); + + it('should render the form with correct title and subtitle', () => { + setup(); + + expect(screen.getByText('Move Provisioned Dashboard')).toBeInTheDocument(); + expect(screen.getByText('Test Dashboard')).toBeInTheDocument(); + }); + + it('should render form even when currentFileData is not available', () => { + (useGetRepositoryFilesWithPathQuery as jest.Mock).mockReturnValue({ + data: null, + isLoading: false, + }); + + setup(); + + // Form should still render, but move button should be disabled + expect(screen.getByText('Move Provisioned Dashboard')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /move dashboard/i })).toBeDisabled(); + }); + + it('should show loading spinner when file data is loading', () => { + (useGetRepositoryFilesWithPathQuery as jest.Mock).mockReturnValue({ + data: null, + isLoading: true, + }); + + setup(); + + expect(screen.getByText('Loading dashboard data')).toBeInTheDocument(); + }); + + it('should show read-only alert when repository is read-only', () => { + setup({ readOnly: true }); + + expect(screen.getByText('This repository is read only')).toBeInTheDocument(); + expect(screen.getByText(/This dashboard cannot be moved directly from Grafana/)).toBeInTheDocument(); + }); + + it('should show target path input with calculated path', () => { + setup(); + + expect(screen.getByDisplayValue('target-folder/dashboard.json')).toBeInTheDocument(); + }); + + it('should show error alert when file data has errors', () => { + (useGetRepositoryFilesWithPathQuery as jest.Mock).mockReturnValue({ + data: { + errors: ['File not found', 'Permission denied'], + resource: null, + }, + isLoading: false, + }); + + setup(); + + expect(screen.getByText('Error loading dashboard')).toBeInTheDocument(); + expect(screen.getByText('File not found')).toBeInTheDocument(); + expect(screen.getByText('Permission denied')).toBeInTheDocument(); + }); + + it('should disable move button when form is submitting', () => { + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([ + jest.fn(), + { + ...mockCreateRequest, + isLoading: true, + }, + ]); + + setup(); + + const moveButton = screen.getByRole('button', { name: /moving/i }); + expect(moveButton).toBeDisabled(); + expect(moveButton).toHaveTextContent('Moving...'); + }); + + it('should show move dashboard button when not loading', () => { + setup(); + + expect(screen.getByRole('button', { name: /move dashboard/i })).toBeInTheDocument(); + }); + + it('should call onDismiss when cancel button is clicked', async () => { + const { user, props } = setup(); + + const cancelButton = screen.getByRole('button', { name: /cancel/i }); + await user.click(cancelButton); + + expect(props.onDismiss).toHaveBeenCalled(); + }); +}); diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx new file mode 100644 index 00000000000..6a17c8df03b --- /dev/null +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx @@ -0,0 +1,238 @@ +import { useEffect, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { AppEvents } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { getAppEvents } from '@grafana/runtime'; +import { Alert, Button, Drawer, Field, Input, Spinner, Stack } from '@grafana/ui'; +import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; +import { + useCreateRepositoryFilesWithPathMutation, + useDeleteRepositoryFilesWithPathMutation, + useGetRepositoryFilesWithPathQuery, +} from 'app/api/clients/provisioning/v0alpha1'; +import { AnnoKeySourcePath } from 'app/features/apiserver/types'; + +import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields'; +import { ProvisionedDashboardFormData } from '../saving/shared'; +import { DashboardScene } from '../scene/DashboardScene'; +import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler'; + +export interface Props { + dashboard: DashboardScene; + defaultValues: ProvisionedDashboardFormData; + readOnly: boolean; + isGitHub: boolean; + isNew?: boolean; + workflowOptions: Array<{ label: string; value: string }>; + loadedFromRef?: string; + targetFolderUID?: string; + targetFolderTitle?: string; + onDismiss: () => void; + onSuccess: (folderUID: string, folderTitle: string) => void; +} + +export function MoveProvisionedDashboardForm({ + dashboard, + defaultValues, + loadedFromRef, + readOnly, + isGitHub, + isNew, + workflowOptions, + targetFolderUID, + targetFolderTitle, + onDismiss, + onSuccess, +}: Props) { + const methods = useForm<ProvisionedDashboardFormData>({ defaultValues }); + const { editPanel: panelEditor } = dashboard.useState(); + const { handleSubmit, watch } = methods; + const appEvents = getAppEvents(); + + const [ref, workflow] = watch(['ref', 'workflow']); + + const { data: currentFileData, isLoading: isLoadingFileData } = useGetRepositoryFilesWithPathQuery({ + name: defaultValues.repo, + path: defaultValues.path, + }); + + const { data: targetFolder } = useGetFolderQuery({ name: targetFolderUID! }, { skip: !targetFolderUID }); + + const [createFile, createRequest] = useCreateRepositoryFilesWithPathMutation(); + const [deleteFile, deleteRequest] = useDeleteRepositoryFilesWithPathMutation(); + const [targetPath, setTargetPath] = useState<string>(''); + + const navigate = useNavigate(); + + useEffect(() => { + const currentSourcePath = currentFileData?.resource?.dryRun?.metadata?.annotations?.[AnnoKeySourcePath]; + if (!targetFolderUID || !targetFolder || !currentSourcePath) { + return; + } + + const folderAnnotations = targetFolder.metadata.annotations || {}; + const targetFolderPath = folderAnnotations[AnnoKeySourcePath] || targetFolderTitle; + + const filename = currentSourcePath.split('/').pop(); + const newPath = `${targetFolderPath}/${filename}`; + + setTargetPath(newPath); + }, [currentFileData, targetFolder, targetFolderUID, targetFolderTitle]); + + const handleSubmitForm = async ({ repo, path, comment }: ProvisionedDashboardFormData) => { + if (!currentFileData?.resource?.file) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [ + t( + 'dashboard-scene.move-provisioned-dashboard-form.current-file-not-found', + 'Current dashboard file could not be found' + ), + ], + }); + return; + } + + const branchRef = workflow === 'write' ? loadedFromRef : ref; + const commitMessage = comment || `Move dashboard: ${dashboard.state.title}`; + + try { + await createFile({ + name: repo, + path: targetPath, + ref: branchRef, + message: commitMessage, + body: currentFileData.resource.file, + }).unwrap(); + + await deleteFile({ + name: repo, + path: path, + ref: branchRef, + message: commitMessage, + }).unwrap(); + } catch (error) { + if (createRequest.isSuccess && !deleteRequest.isSuccess) { + appEvents.publish({ + type: AppEvents.alertWarning.name, + payload: [ + t( + 'dashboard-scene.move-provisioned-dashboard-form.partial-failure-warning', + 'Dashboard was created at new location but could not be deleted from original location. Please manually remove the old file.' + ), + ], + }); + } + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [t('dashboard-scene.move-provisioned-dashboard-form.api-error', 'Failed to move dashboard'), error], + }); + } + }; + + const onWriteSuccess = () => { + panelEditor?.onDiscard(); + if (targetFolderUID && targetFolderTitle) { + onSuccess(targetFolderUID, targetFolderTitle); + } + navigate('/dashboards'); + }; + + const onBranchSuccess = () => { + panelEditor?.onDiscard(); + navigate(`/dashboards?new_pull_request_url=${createRequest.data?.urls?.newPullRequestURL}`); + }; + + useProvisionedRequestHandler({ + dashboard, + request: createRequest, + workflow, + handlers: { + onBranchSuccess, + onWriteSuccess, + }, + }); + + const isLoading = createRequest.isLoading || deleteRequest.isLoading; + + return ( + <Drawer + title={t('dashboard-scene.move-provisioned-dashboard-form.drawer-title', 'Move Provisioned Dashboard')} + subtitle={dashboard.state.title} + onClose={onDismiss} + > + <FormProvider {...methods}> + <form onSubmit={handleSubmit(handleSubmitForm)}> + <Stack direction="column" gap={2}> + {readOnly && ( + <Alert + title={t( + 'dashboard-scene.move-provisioned-dashboard-form.title-this-repository-is-read-only', + 'This repository is read only' + )} + > + <Trans i18nKey="dashboard-scene.move-provisioned-dashboard-form.move-read-only-message"> + This dashboard cannot be moved directly from Grafana because the repository is read-only. To move this + dashboard, please move the file in your Git repository. + </Trans> + </Alert> + )} + + {isLoadingFileData && ( + <Stack alignItems="center" gap={2}> + <Spinner /> + <div> + {t('dashboard-scene.move-provisioned-dashboard-form.loading-file-data', 'Loading dashboard data')} + </div> + </Stack> + )} + + {currentFileData?.errors?.length && currentFileData.errors.length > 0 && ( + <Alert + title={t('dashboard-scene.move-provisioned-dashboard-form.file-load-error', 'Error loading dashboard')} + severity="error" + > + {currentFileData.errors.map((error, index) => ( + <div key={index}>{error}</div> + ))} + </Alert> + )} + + <Field + noMargin + label={t('dashboard-scene.move-provisioned-dashboard-form.target-path-label', 'Target path')} + > + <Input readOnly value={targetPath} /> + </Field> + + <ResourceEditFormSharedFields + resourceType="dashboard" + isNew={isNew} + readOnly={readOnly} + workflow={workflow} + workflowOptions={workflowOptions} + isGitHub={isGitHub} + /> + + <Stack gap={2}> + <Button + variant="primary" + type="submit" + disabled={isLoading || readOnly || !currentFileData || isLoadingFileData} + > + {isLoading + ? t('dashboard-scene.move-provisioned-dashboard-form.moving', 'Moving...') + : t('dashboard-scene.move-provisioned-dashboard-form.move-action', 'Move dashboard')} + </Button> + <Button variant="secondary" onClick={onDismiss} fill="outline"> + <Trans i18nKey="dashboard-scene.move-provisioned-dashboard-form.cancel-action">Cancel</Trans> + </Button> + </Stack> + </Stack> + </form> + </FormProvider> + </Drawer> + ); +} diff --git a/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx b/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx index 5dfa1a49659..d438ff9e694 100644 --- a/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx @@ -3,7 +3,7 @@ import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { Permissions } from 'app/core/components/AccessControl'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; diff --git a/public/app/features/dashboard-scene/settings/utils.ts b/public/app/features/dashboard-scene/settings/utils.ts index 907612875c8..88011f08b00 100644 --- a/public/app/features/dashboard-scene/settings/utils.ts +++ b/public/app/features/dashboard-scene/settings/utils.ts @@ -5,7 +5,8 @@ import { t } from '@grafana/i18n'; import { SceneObject, SceneObjectState } from '@grafana/scenes'; import { contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; -import { AccessControlAction, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useSelector } from 'app/types/store'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx index 0fe12b9e977..edc4aaf3384 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx @@ -1,5 +1,6 @@ import { AsyncState } from 'react-use/lib/useAsync'; +import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; @@ -28,6 +29,8 @@ interface Props { onViewYAML: () => void; } +const selector = e2eSelectors.pages.ExportDashboardDrawer.ExportAsJson; + export function ResourceExport({ dashboardJson, isSharingExternally, @@ -110,7 +113,12 @@ export function ResourceExport({ (initialSaveModelVersion === 'v2' && exportMode === ExportMode.V1Resource)) && ( <Stack gap={1} alignItems="start"> <Label>{switchExportLabel}</Label> - <Switch label={switchExportLabel} value={isSharingExternally} onChange={onShareExternallyChange} /> + <Switch + label={switchExportLabel} + value={isSharingExternally} + onChange={onShareExternallyChange} + data-testid={selector.exportExternallyToggle} + /> </Stack> )} </Stack> diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx index 0dad98fb05e..00af6541807 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx @@ -22,8 +22,8 @@ export default function ShareButton({ dashboard, panel }: { dashboard: Dashboard const [_, buildUrl] = useAsyncFn(async () => { DashboardInteractions.toolbarShareClick(); - return await buildShareUrl(dashboard, panel); - }, [dashboard]); + await buildShareUrl(dashboard, panel); + }, [dashboard, panel]); const onMenuClick = useCallback((isOpen: boolean) => { if (isOpen) { diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx index 91439fe662a..c049579fb38 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx @@ -3,9 +3,9 @@ import { render, screen } from '@testing-library/react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction } from 'app/types/accessControl'; import { config } from '../../../../core/config'; -import { AccessControlAction } from '../../../../types'; import { grantUserPermissions } from '../../../alerting/unified/mocks'; import { DashboardScene, DashboardSceneState } from '../../scene/DashboardScene'; import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx index bd8c983e1c8..0a50fb9530e 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx @@ -7,7 +7,7 @@ import { config, locationService } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; import { IconName, Menu } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { isPublicDashboardsEnabled } from '../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { getTrackingSource, shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx index d2affefd482..4d6ca049b89 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx @@ -7,7 +7,7 @@ import { contextSrv } from 'app/core/core'; import { publicDashboardApi, useAddRecipientMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { validEmailRegex } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { useShareDrawerContext } from '../../../../ShareDrawer/ShareDrawerContext'; import ShareConfiguration from '../../ShareConfiguration'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/CreateEmailSharing.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/CreateEmailSharing.tsx index 3f1c51db7b8..4c482660047 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/CreateEmailSharing.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/CreateEmailSharing.tsx @@ -9,7 +9,7 @@ import { contextSrv } from 'app/core/core'; import { useCreatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { PublicDashboardShareType } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { EmailSharingPricingAlert } from '../../../../../dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/EmailSharingPricingAlert'; import { useShareDrawerContext } from '../../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/PublicShare/CreatePublicSharing.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/PublicShare/CreatePublicSharing.tsx index 8352e8a9495..c871537e4c4 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/PublicShare/CreatePublicSharing.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/PublicShare/CreatePublicSharing.tsx @@ -9,7 +9,7 @@ import { contextSrv } from 'app/core/core'; import { useCreatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { PublicDashboardShareType } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PublicDashboardAlert } from '../../../../../dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/PublicDashboardAlert'; import { useShareDrawerContext } from '../../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.tsx index 9e74065d62e..6c7b0d55ec7 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.tsx @@ -7,7 +7,7 @@ import { PublicDashboard, PublicDashboardShareType, } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { NoUpsertPermissionsAlert } from '../../../../dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert'; import { PublicDashboardAlert } from '../../../../dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/PublicDashboardAlert'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareConfiguration.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareConfiguration.tsx index 57d2423e167..62d22223663 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareConfiguration.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareConfiguration.tsx @@ -10,7 +10,7 @@ import { contextSrv } from 'app/core/core'; import { publicDashboardApi, useUpdatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { ConfigPublicDashboardForm } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { useShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.tsx index ca95eee9110..41637090ad0 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.tsx @@ -21,7 +21,7 @@ import { } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { getDashboardSceneFor } from 'app/features/dashboard-scene/utils/utils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShareDrawerConfirmAction } from '../../ShareDrawer/ShareDrawerConfirmAction'; import { useShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareTypeSelect.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareTypeSelect.tsx index f58c16298ca..89e14df3c5f 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareTypeSelect.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareTypeSelect.tsx @@ -14,7 +14,7 @@ import { PublicDashboardShareType, } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { useShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx index 89285aa03a6..75861e9ee92 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { SceneComponentProps } from '@grafana/scenes'; import { Alert, Button, ClipboardButton, Spinner, Stack, TextLink } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { SnapshotSharingOptions } from '../../../../dashboard/services/SnapshotSrv'; import { ShareDrawerConfirmAction } from '../../ShareDrawer/ShareDrawerConfirmAction'; diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index db5f89a32fb..f389382cbaf 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -6,7 +6,7 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { getTrackingSource } from '../../dashboard/components/ShareModal/utils'; import { DashboardInteractions } from '../utils/interactions'; diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx b/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx index 303492f492f..99f55253598 100644 --- a/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx @@ -8,7 +8,7 @@ import { contextSrv } from 'app/core/core'; import { useDeletePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { ConfigPublicDashboardBase } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard'; import { PublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; import { getDashboardSceneFor } from '../../utils/utils'; diff --git a/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx b/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx index 6de468e798b..e38fd63e5d1 100644 --- a/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx +++ b/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx @@ -11,7 +11,7 @@ import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardPageRouteParams } from 'app/features/dashboard/containers/types'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts b/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts index 9c5badd54f7..fe93bb0a479 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts @@ -1,6 +1,6 @@ import { config, locationService } from '@grafana/runtime'; import { CustomVariable, UrlSyncManager } from '@grafana/scenes'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts index 40a547f68e1..7576d80f18c 100644 --- a/public/app/features/dashboard-scene/utils/test-utils.ts +++ b/public/app/features/dashboard-scene/utils/test-utils.ts @@ -14,7 +14,7 @@ import { } from '@grafana/scenes'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx index 4eb8476eaa5..aa4c06d3684 100644 --- a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx +++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx @@ -11,7 +11,7 @@ import { Form } from 'app/core/components/Form/Form'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { clearLoadedDashboard } from 'app/features/manage-dashboards/state/actions'; -import { useDispatch, useSelector, StoreState } from 'app/types'; +import { useDispatch, useSelector, StoreState } from 'app/types/store'; import { ImportDashboardFormV2 } from './ImportDashboardFormV2'; diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 432aaa300c6..5111fc74e5c 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -25,7 +25,7 @@ import { transformVariableHideToEnum, transformVariableRefreshToEnum, } from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { getDefaultDatasource, @@ -1016,14 +1016,15 @@ describe('ResponseTransformers', () => { function validateAnnotation(v1: AnnotationQuery, v2: DashboardV2Spec['annotations'][0]) { const { spec: v2Spec } = v2; + expect(v1.name).toBe(v2Spec.name); - expect(v1.datasource?.type).toBe(v2Spec.query.group); - expect(v1.datasource?.uid).toBe(v2Spec.query.datasource?.name); + expect(v1.datasource?.type).toBe(v2Spec.query?.spec.group); + expect(v1.datasource?.uid).toBe(v2Spec.query?.spec.datasource?.name); expect(v1.enable).toBe(v2Spec.enable); expect(v1.hide).toBe(v2Spec.hide); expect(v1.iconColor).toBe(v2Spec.iconColor); - expect(v1.builtIn).toBe(v2Spec.builtIn ? 1 : undefined); - expect(v1.target).toEqual(v2Spec.query.spec); + expect(v1.builtIn).toBe(v2Spec.builtIn ? 1 : 0); + expect(v1.target).toBe(v2Spec.query?.spec); expect(v1.filter).toEqual(v2Spec.filter); } diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 66dbbb8fdec..0fe72aaf365 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -56,7 +56,6 @@ import { DeprecatedInternalId, ObjectMeta, } from 'app/features/apiserver/types'; -import { transformV2ToV1AnnotationQuery } from 'app/features/dashboard-scene/serialization/annotations'; import { GRID_ROW_HEIGHT } from 'app/features/dashboard-scene/serialization/const'; import { validateFiltersOrigin } from 'app/features/dashboard-scene/serialization/sceneVariablesSetToVariables'; import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene'; @@ -75,7 +74,7 @@ import { transformVariableHideToEnum, transformVariableRefreshToEnum, } from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { DashboardWithAccessInfo } from './types'; import { isDashboardResource, isDashboardV0Spec, isDashboardV2Resource } from './utils'; @@ -893,6 +892,25 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] { return variables; } +function getAnnotationsV1(annotations: DashboardV2Spec['annotations']): AnnotationQuery[] { + // @ts-expect-error - target v2 query is not compatible with v1 target + return annotations.map((a) => { + return { + name: a.spec.name, + datasource: { + type: a.spec.query?.spec.group, + uid: a.spec.query?.spec.datasource?.name, + }, + enable: a.spec.enable, + hide: a.spec.hide, + iconColor: a.spec.iconColor, + builtIn: a.spec.builtIn ? 1 : 0, + target: a.spec.query?.spec, + filter: a.spec.filter, + }; + }); +} + interface LibraryPanelDTO extends Pick<Panel, 'libraryPanel' | 'id' | 'title' | 'gridPos' | 'type'> {} function getPanelsV1( @@ -1140,8 +1158,7 @@ function transformToV1VariableTypes(variable: TypedVariableModelV2): VariableTyp } export function transformDashboardV2SpecToV1(spec: DashboardV2Spec, metadata: ObjectMeta): DashboardDataDTO { - const annotations = spec.annotations.map(transformV2ToV1AnnotationQuery); - + const annotations = getAnnotationsV1(spec.annotations); const variables = getVariablesV1(spec.variables); const panels = getPanelsV1(spec.elements, spec.layout); return { diff --git a/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts b/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts index 6dc00b3190f..8b9e3cd2f47 100644 --- a/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts +++ b/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts @@ -4,7 +4,7 @@ import { defaultSpec as defaultDashboardV2Spec, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { ResourceList } from 'app/features/apiserver/types'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/UnifiedDashboardAPI.ts b/public/app/features/dashboard/api/UnifiedDashboardAPI.ts index 344f3ba9cd2..6851a6c0f76 100644 --- a/public/app/features/dashboard/api/UnifiedDashboardAPI.ts +++ b/public/app/features/dashboard/api/UnifiedDashboardAPI.ts @@ -2,7 +2,7 @@ import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { isResource } from 'app/features/apiserver/guards'; import { Resource, ResourceList } from 'app/features/apiserver/types'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index fb930516a0e..c4a73deccb7 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -1,6 +1,6 @@ import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { UnifiedDashboardAPI } from './UnifiedDashboardAPI'; import { LegacyDashboardAPI } from './legacy'; diff --git a/public/app/features/dashboard/api/legacy.test.ts b/public/app/features/dashboard/api/legacy.test.ts index b62450fd0ea..ce81f685afb 100644 --- a/public/app/features/dashboard/api/legacy.test.ts +++ b/public/app/features/dashboard/api/legacy.test.ts @@ -1,5 +1,5 @@ import { UrlQueryMap } from '@grafana/data'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { LegacyDashboardAPI } from './legacy'; diff --git a/public/app/features/dashboard/api/legacy.ts b/public/app/features/dashboard/api/legacy.ts index 5ce46cdf442..5a9e0b9707c 100644 --- a/public/app/features/dashboard/api/legacy.ts +++ b/public/app/features/dashboard/api/legacy.ts @@ -6,7 +6,7 @@ import appEvents from 'app/core/app_events'; import { Resource, ResourceList } from 'app/features/apiserver/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { SaveDashboardResponseDTO, DashboardDTO } from 'app/types'; +import { SaveDashboardResponseDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/types.ts b/public/app/features/dashboard/api/types.ts index 568fcfcf4a5..90c16d527dc 100644 --- a/public/app/features/dashboard/api/types.ts +++ b/public/app/features/dashboard/api/types.ts @@ -2,7 +2,7 @@ import { UrlQueryMap } from '@grafana/data'; import { Status } from '@grafana/schema/src/schema/dashboard/v2alpha1/types.status.gen'; import { ListOptions, Resource, ResourceList } from 'app/features/apiserver/types'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { AnnotationsPermissions, SaveDashboardResponseDTO } from 'app/types'; +import { AnnotationsPermissions, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/utils.ts b/public/app/features/dashboard/api/utils.ts index f49fcb9de12..ea9ba97cf57 100644 --- a/public/app/features/dashboard/api/utils.ts +++ b/public/app/features/dashboard/api/utils.ts @@ -3,7 +3,7 @@ import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Status } from '@grafana/schema/src/schema/dashboard/v2alpha1/types.status.gen'; import { Resource } from 'app/features/apiserver/types'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/v1.test.ts b/public/app/features/dashboard/api/v1.test.ts index a037136eeb9..7cdadd0995a 100644 --- a/public/app/features/dashboard/api/v1.test.ts +++ b/public/app/features/dashboard/api/v1.test.ts @@ -1,7 +1,7 @@ import { GrafanaConfig, locationUtil } from '@grafana/data'; import { backendSrv } from 'app/core/services/backend_srv'; import { AnnoKeyFolder } from 'app/features/apiserver/types'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardWithAccessInfo } from './types'; import { K8sDashboardAPI } from './v1'; diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index 58708bcdcd8..f196e9d71e2 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -21,7 +21,7 @@ import { } from 'app/features/apiserver/types'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { DashboardDataDTO, DashboardDTO, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index 0a0830e4ffa..676f7b26e27 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -19,7 +19,7 @@ import { } from 'app/features/apiserver/types'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx index 8b634dab7f0..75cedae0807 100644 --- a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx @@ -13,8 +13,8 @@ import { import AddPanelMenu from './AddPanelMenu'; -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => jest.fn(), useSelector: () => jest.fn(), })); diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx index 560f924a8a3..138f2bf285d 100644 --- a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx @@ -13,7 +13,7 @@ import { onPasteCopiedPanel, } from 'app/features/dashboard/utils/dashboard'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { setInitialDatasource } from '../../state/reducers'; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index aeb072faf93..78dd8c02a40 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -31,8 +31,9 @@ import { PublicDashboardBadgeLegacy } from 'app/features/dashboard-scene/scene/n import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; -import { KioskMode, StoreState } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { DashboardMetaChangedEvent, ShowModalReactEvent } from 'app/types/events'; +import { StoreState } from 'app/types/store'; import { DynamicDashNavButtonModel, diff --git a/public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx b/public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx index 9601c1a026f..69fd629ccd6 100644 --- a/public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx +++ b/public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx @@ -2,7 +2,8 @@ import { css } from '@emotion/css'; import { Alert } from '@grafana/ui'; import { getMessageFromError } from 'app/core/utils/errors'; -import { DashboardInitError, AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; +import { DashboardInitError } from 'app/types/dashboard'; export interface Props { initError?: DashboardInitError; diff --git a/public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx b/public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx index 5dcb8d870f5..f5e279e5af1 100644 --- a/public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx +++ b/public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { Button, HorizontalGroup, Spinner, useStyles2, VerticalGroup } from '@grafana/ui'; -import { DashboardInitPhase } from 'app/types'; +import { DashboardInitPhase } from 'app/types/dashboard'; export interface Props { initPhase: DashboardInitPhase; diff --git a/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx index 6b31090b447..479521c91bd 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx @@ -1,7 +1,7 @@ import { Permissions } from 'app/core/components/AccessControl'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { SettingsPageProps } from '../DashboardSettings/types'; diff --git a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx index 5514f11da67..96c63448806 100644 --- a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx +++ b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx @@ -10,8 +10,8 @@ import { Prompt } from 'app/core/components/FormPrompt/Prompt'; import { contextSrv } from 'app/core/services/context_srv'; import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal'; import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types'; -import { useDispatch } from 'app/types'; import { DashboardSavedEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { DashboardModel } from '../../state/DashboardModel'; import { discardPanelChanges, exitPanelEditor } from '../PanelEditor/state/actions'; diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index 063eccf4ebc..26d52f52a63 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -10,7 +10,7 @@ import { Button, Stack, Text, ToolbarButtonRow } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { DashboardMetaChangedEvent } from 'app/types/events'; import { VariableEditorContainer } from '../../../variables/editor/VariableEditorContainer'; diff --git a/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx b/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx index b7eefcba85a..ff10cbf0367 100644 --- a/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx +++ b/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx @@ -23,7 +23,7 @@ import { } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowMessage, SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index cbd6b6c4e3f..a60e02d2ae0 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -8,7 +8,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { InspectTab } from 'app/features/inspector/types'; import { getPanelStateForModel } from 'app/features/panel/state/selectors'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { GetDataOptions } from '../../../query/state/PanelQueryRunner'; import { HelpWizard } from '../HelpWizard/HelpWizard'; diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx index d44d699c978..cacdb672a11 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { OptionsPaneOptions } from './OptionsPaneOptions'; import { VisualizationButton } from './VisualizationButton'; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 81e184b7e59..76c38e695d9 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -30,8 +30,8 @@ import { SaveLibraryPanelModal } from 'app/features/library-panels/components/Sa import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types'; import { getPanelStateForModel } from 'app/features/panel/state/selectors'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; -import { StoreState } from 'app/types'; import { PanelOptionsChangedEvent, ShowModalReactEvent } from 'app/types/events'; +import { StoreState } from 'app/types/store'; import { notifyApp } from '../../../../core/actions'; import { UnlinkModal } from '../../../dashboard-scene/scene/UnlinkModal'; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx index 020eea8d8e8..b24344519db 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx @@ -5,7 +5,7 @@ import { locationService } from '@grafana/runtime'; import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { QueryGroup } from 'app/features/query/components/QueryGroup'; -import { QueryGroupDataSource, QueryGroupOptions } from 'app/types'; +import { QueryGroupDataSource, QueryGroupOptions } from 'app/types/query'; import { getDashboardSrv } from '../../services/DashboardSrv'; import { PanelModel } from '../../state/PanelModel'; diff --git a/public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx b/public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx index d98b0ae99c2..51a84846f1d 100644 --- a/public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx +++ b/public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { ToolbarButton, ButtonGroup } from '@grafana/ui'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { PanelModel } from '../../state/PanelModel'; import { getPanelPluginWithFallback } from '../../state/selectors'; diff --git a/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx b/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx index 0edd8c13fe0..ffa32a42254 100644 --- a/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx +++ b/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx @@ -10,7 +10,7 @@ import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants'; import { PanelLibraryOptionsGroup } from 'app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup'; import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions'; import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { VizTypePicker } from '../../../panel/components/VizTypePicker/VizTypePicker'; import { changePanelPlugin } from '../../../panel/state/actions'; diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.ts index 00a96421aea..f1313156cb0 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.ts @@ -4,7 +4,7 @@ import store from 'app/core/store'; import { removePanel } from 'app/features/dashboard/utils/panel'; import { cleanUpPanelState } from 'app/features/panel/state/actions'; import { panelModelAndPluginReady } from 'app/features/panel/state/reducers'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { DashboardModel } from '../../../state/DashboardModel'; import { PanelModel } from '../../../state/PanelModel'; diff --git a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx index cafa4578540..de68a822e3a 100644 --- a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx +++ b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx @@ -4,7 +4,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { SceneObject, sceneGraph } from '@grafana/scenes'; import { Combobox, ComboboxOption, Select } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { getLastKey, getVariablesByKey } from '../../../variables/state/selectors'; diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx index 264a2640ebf..de5b14e257c 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from 'test/test-utils'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import * as api from 'app/features/manage-dashboards/state/actions'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardAsForm, SaveDashboardAsFormProps } from './SaveDashboardAsForm'; diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx index 3c2ec091317..0f50238b88b 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { Dashboard } from '@grafana/schema'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardOptions } from '../types'; diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx index 510222479bd..18f0c61f02e 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx @@ -8,7 +8,7 @@ import { config } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Button, Checkbox, TextArea, useStyles2, Stack } from '@grafana/ui'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; import { GenAIDashboardChangesButton } from '../../GenAI/GenAIDashboardChangesButton'; import { SaveDashboardData, SaveDashboardOptions } from '../types'; diff --git a/public/app/features/dashboard/components/SaveDashboard/types.ts b/public/app/features/dashboard/components/SaveDashboard/types.ts index 7e8eccc605f..05c6356f61f 100644 --- a/public/app/features/dashboard/components/SaveDashboard/types.ts +++ b/public/app/features/dashboard/components/SaveDashboard/types.ts @@ -2,7 +2,7 @@ import { Dashboard } from '@grafana/schema'; import { ObjectMeta } from 'app/features/apiserver/types'; import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; export interface SaveDashboardData { clone: Dashboard; // cloned copy diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index 18435bc1e31..33b036e05a8 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -10,8 +10,8 @@ import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; -import { useDispatch } from 'app/types'; import { DashboardSavedEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { updateDashboardUidLastUsedDatasource } from '../../utils/dashboard'; diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx index 73597b197f2..16d6220adcc 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx @@ -10,7 +10,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShareEmbed } from './ShareEmbed'; import { ShareExport } from './ShareExport'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx index b08aebc934e..ae88d932140 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -26,9 +26,10 @@ import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { DeletePublicDashboardModal } from 'app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardModal'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useSelector } from 'app/types/store'; import { contextSrv } from '../../../../../../core/services/context_srv'; -import { AccessControlAction, useSelector } from '../../../../../../types'; import { useIsDesktop } from '../../../../utils/screen'; import { ShareModal } from '../../ShareModal'; import { shareDashboardType } from '../../utils'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx index 930953f859e..c63628c8d17 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx @@ -17,7 +17,7 @@ import { import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PublicDashboard, PublicDashboardShareType, validEmailRegex } from '../SharePublicDashboardUtils'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx index 3d4d098023d..7cbc1b6b9f6 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx @@ -10,7 +10,8 @@ import { useCreatePublicDashboardMutation } from 'app/features/dashboard/api/pub import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useSelector } from 'app/types/store'; import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert'; import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx index 1df31f7b635..9636e2dd6ef 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx @@ -6,7 +6,7 @@ import { Spinner, useStyles2 } from '@grafana/ui'; import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi'; import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { HorizontalGroup } from '../../../../plugins/admin/components/HorizontalGroup'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx index f1a2eb97686..993fc55ae8e 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx @@ -3,8 +3,9 @@ import { http, HttpResponse } from 'msw'; import * as React from 'react'; import { Provider } from 'react-redux'; +import { DashboardInitPhase } from 'app/types/dashboard'; + import { configureStore } from '../../../../../store/configureStore'; -import { DashboardInitPhase } from '../../../../../types'; import { DashboardModel } from '../../../state/DashboardModel'; import { PanelModel } from '../../../state/PanelModel'; import { createDashboardModelFixture } from '../../../state/__fixtures__/dashboardFixtures'; diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx index fdc83c449d8..3ec288df413 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -7,8 +7,8 @@ import { AnnotationQuery, DataQuery, TypedVariableModel, GrafanaTheme2 } from '@ import { t } from '@grafana/i18n'; import { DashboardLink } from '@grafana/schema'; import { stylesFactory, Themeable2, withTheme2 } from '@grafana/ui'; +import { StoreState } from 'app/types/store'; -import { StoreState } from '../../../../types'; import { getSubMenuVariables, getVariablesState } from '../../../variables/state/selectors'; import { DashboardModel } from '../../state/DashboardModel'; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx index ea710a890e5..4dab1da8b45 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx @@ -12,7 +12,7 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2 } from '@grafana/ui'; +import { Badge, Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2 } from '@grafana/ui'; import config from 'app/core/config'; import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; import { categoriesLabels } from 'app/features/transformers/utils'; @@ -146,6 +146,7 @@ function getTransformationPickerStyles(theme: GrafanaTheme2) { columnGap: '27px', rowGap: '16px', width: '100%', + paddingBottom: theme.spacing(1), }), searchInput: css({ flexGrow: '1', @@ -208,10 +209,24 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data key={transform.id} > <Card.Heading className={styles.heading}> - <span>{transform.name}</span> - <span className={styles.pluginStateInfoWrapper}> - <PluginStateInfo state={transform.state} /> - </span> + <div className={styles.titleRow}> + <span>{transform.name}</span> + <span className={styles.pluginStateInfoWrapper}> + <PluginStateInfo state={transform.state} /> + </span> + </div> + {transform.tags && transform.tags.size > 0 && ( + <div className={styles.tagsWrapper}> + {Array.from(transform.tags).map((tag) => ( + <Badge + color="darkgrey" + icon="tag-alt" + key={tag} + text={t('transformers.tag', '{{ tag }}', { tag })} + /> + ))} + </div> + )} </Card.Heading> <Card.Description className={styles.description}> <span>{getTransformationsRedesignDescriptions(transform.id)}</span> @@ -242,11 +257,18 @@ function getTransformationGridStyles(theme: GrafanaTheme2) { '> button': { width: '100%', display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - flexWrap: 'nowrap', + flexDirection: 'column', + alignItems: 'flex-start', + gap: theme.spacing(1), }, }), + titleRow: css({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'nowrap', + width: '100%', + }), description: css({ fontSize: '12px', display: 'flex', @@ -255,19 +277,19 @@ function getTransformationGridStyles(theme: GrafanaTheme2) { }), image: css({ display: 'block', - maxEidth: '100%`', - marginTop: `${theme.spacing(2)}`, + maxWidth: '100%', + marginTop: theme.spacing(2), }), grid: css({ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gridAutoRows: '1fr', - gap: `${theme.spacing(2)} ${theme.spacing(1)}`, + gap: theme.spacing(1), width: '100%', + padding: `${theme.spacing(1)} 0`, }), cardDisabled: css({ - backgroundColor: 'rgb(204, 204, 220, 0.045)', - color: `${theme.colors.text.disabled} !important`, + backgroundColor: theme.colors.action.disabledBackground, img: { filter: 'grayscale(100%)', opacity: 0.33, @@ -275,14 +297,20 @@ function getTransformationGridStyles(theme: GrafanaTheme2) { }), cardApplicableInfo: css({ position: 'absolute', - bottom: `${theme.spacing(1)}`, - right: `${theme.spacing(1)}`, + bottom: theme.spacing(1), + right: theme.spacing(1), }), newCard: css({ gridTemplateRows: 'min-content 0 1fr 0', + marginBottom: 0, }), pluginStateInfoWrapper: css({ - marginLeft: '5px', + marginLeft: theme.spacing(0.5), + }), + tagsWrapper: css({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(0.5), }), }; } diff --git a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx index f95568f8159..53c7633fbaa 100644 --- a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx +++ b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx @@ -5,7 +5,7 @@ import { locationUtil } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { useAppNotification } from 'app/core/copy/appNotification'; import { historySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { dashboardWatcher } from '../../../live/dashboard/dashboardWatcher'; import { DashboardModel } from '../../state/DashboardModel'; diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index 19e245cf03b..da7454a6d02 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -14,7 +14,7 @@ import { AppChrome } from 'app/core/components/AppChrome/AppChrome'; import { getRouteComponentProps } from 'app/core/navigation/mocks/routeProps'; import { RouteDescriptor } from 'app/core/navigation/types'; import { HOME_NAV_ID } from 'app/core/reducers/navModel'; -import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types'; +import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types/dashboard'; import { Props as LazyLoaderProps } from '../dashgrid/LazyLoader'; import { DashboardSrv, setDashboardSrv } from '../services/DashboardSrv'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index bb4ad2a995c..16ed48ea618 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -17,8 +17,9 @@ import { ID_PREFIX } from 'app/core/reducers/navBarTree'; import { getNavModel } from 'app/core/selectors/navModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; -import { KioskMode, StoreState } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; +import { StoreState } from 'app/types/store'; import { cancelVariables, templateVarsChangedInUrl } from '../../variables/state/actions'; import { findTemplateVarChanges } from '../../variables/utils'; diff --git a/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx index ee38959345f..e26b1099d2d 100644 --- a/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx @@ -13,7 +13,7 @@ import { setupLoadDashboardMockReject, setupLoadDashboardRuntimeErrorMock, } from 'app/features/dashboard-scene/utils/test-utils'; -import { DashboardDTO, DashboardRoutes } from 'app/types'; +import { DashboardDTO, DashboardRoutes } from 'app/types/dashboard'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from '../services/DashboardLoaderSrv'; diff --git a/public/app/features/dashboard/containers/DashboardPageProxy.tsx b/public/app/features/dashboard/containers/DashboardPageProxy.tsx index a574ceb1e7e..2e37c4b3749 100644 --- a/public/app/features/dashboard/containers/DashboardPageProxy.tsx +++ b/public/app/features/dashboard/containers/DashboardPageProxy.tsx @@ -5,7 +5,7 @@ import { config } from '@grafana/runtime'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import DashboardScenePage from 'app/features/dashboard-scene/pages/DashboardScenePage'; import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { isDashboardV2Resource } from '../api/utils'; diff --git a/public/app/features/dashboard/containers/NewDashboardWithDS.tsx b/public/app/features/dashboard/containers/NewDashboardWithDS.tsx index f247b8865d5..46064030ca2 100644 --- a/public/app/features/dashboard/containers/NewDashboardWithDS.tsx +++ b/public/app/features/dashboard/containers/NewDashboardWithDS.tsx @@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom-v5-compat'; import { Trans } from '@grafana/i18n'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { setInitialDatasource } from '../state/reducers'; diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index 6b3ebdcbee8..d9038bf1f2b 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -8,8 +8,8 @@ import { render } from 'test/test-utils'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { Dashboard, DashboardCursorSync, FieldConfigSource, Panel, ThresholdsMode } from '@grafana/schema/src'; import { getRouteComponentProps } from 'app/core/navigation/mocks/routeProps'; -import * as appTypes from 'app/types'; -import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types'; +import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types/dashboard'; +import { StoreState } from 'app/types/store'; import { configureStore } from '../../../store/configureStore'; import { Props as LazyLoaderProps } from '../dashgrid/LazyLoader'; @@ -45,12 +45,12 @@ jest.mock('app/features/dashboard/state/initDashboard', () => ({ initDashboard: jest.fn(), })); -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => jest.fn(), })); -const setup = (propOverrides?: Partial<Props>, initialState?: Partial<appTypes.StoreState>) => { +const setup = (propOverrides?: Partial<Props>, initialState?: Partial<StoreState>) => { const store = configureStore(initialState); const props: Props = { ...getRouteComponentProps({ diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 4c3957431d8..b6f74800a76 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -14,7 +14,8 @@ import { PublicDashboardPageRouteSearchParams, } from 'app/features/dashboard/containers/types'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; -import { useSelector, useDispatch, DashboardInitError } from 'app/types'; +import { DashboardInitError } from 'app/types/dashboard'; +import { useDispatch, useSelector } from 'app/types/store'; import { DashNavTimeControls } from '../components/DashNav/DashNavTimeControls'; import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed'; diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx index 04b3678a0a1..b8ca09b1b4e 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx @@ -5,8 +5,7 @@ import { render } from 'test/test-utils'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { config, locationService } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; - -import { DashboardRoutes } from '../../../types'; +import { DashboardRoutes } from 'app/types/dashboard'; import PublicDashboardPageProxy, { PublicDashboardPageProxyProps } from './PublicDashboardPageProxy'; diff --git a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx index 2a159054622..421d7456acf 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx @@ -5,7 +5,7 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { Dashboard } from '@grafana/schema'; import { GrafanaContext } from 'app/core/context/GrafanaContext'; import { RouteDescriptor } from 'app/core/navigation/types'; -import { DashboardMeta, DashboardRoutes } from 'app/types'; +import { DashboardMeta, DashboardRoutes } from 'app/types/dashboard'; import { getRouteComponentProps } from '../../../core/navigation/mocks/routeProps'; import { Props as DashboardPanelProps } from '../dashgrid/DashboardPanel'; diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 7fbb737de76..1904e5eb487 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -10,7 +10,7 @@ import { Alert, useStyles2 } from '@grafana/ui'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { useGrafana } from '../../../core/context/GrafanaContext'; import { DashboardPanel } from '../dashgrid/DashboardPanel'; diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx index d18120fc5f0..09370002336 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx @@ -8,8 +8,8 @@ import { onCreateNewPanel, onImportDashboard, onAddLibraryPanel } from '../utils import DashboardEmpty, { Props } from './DashboardEmpty'; -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => jest.fn(), useSelector: () => jest.fn(), })); diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index 9c82b005616..38f57b344e5 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -14,7 +14,7 @@ import { import { buildPanelEditScene } from 'app/features/dashboard-scene/panel-edit/PanelEditor'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { setInitialDatasource } from '../state/reducers'; diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx index 164d49ba4b8..330b4df5a43 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx @@ -7,7 +7,7 @@ import { Dashboard } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { GetVariables } from 'app/features/variables/state/selectors'; import { VariablesChanged } from 'app/features/variables/types'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { DashboardModel } from '../state/DashboardModel'; import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures'; diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index e1ba53b8142..db9f93bb92a 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,7 +1,7 @@ import { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { initPanelState } from '../../panel/state/actions'; import { setPanelInstanceState } from '../../panel/state/reducers'; diff --git a/public/app/features/dashboard/routes.ts b/public/app/features/dashboard/routes.ts index 4c20ec7229f..84e7d514b99 100644 --- a/public/app/features/dashboard/routes.ts +++ b/public/app/features/dashboard/routes.ts @@ -1,7 +1,8 @@ +import { DashboardRoutes } from 'app/types/dashboard'; + import { SafeDynamicImport } from '../../core/components/DynamicImports/SafeDynamicImport'; import { config } from '../../core/config'; import { RouteDescriptor } from '../../core/navigation/types'; -import { DashboardRoutes } from '../../types'; export const getPublicDashboardRoutes = (): RouteDescriptor[] => { if (!config.publicDashboardsEnabled) { diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index 22bb842a761..d28f575ce86 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -10,7 +10,7 @@ import impressionSrv from 'app/core/services/impression_srv'; import kbn from 'app/core/utils/kbn'; import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { appEvents } from '../../../core/core'; import { ResponseTransformers } from '../api/ResponseTransformers'; diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 99d7423e389..0ae57780d81 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -5,7 +5,7 @@ import { Dashboard } from '@grafana/schema'; import { appEvents } from 'app/core/app_events'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { RemovePanelEvent } from '../../../types/events'; import { DashboardModel } from '../state/DashboardModel'; diff --git a/public/app/features/dashboard/services/SnapshotSrv.ts b/public/app/features/dashboard/services/SnapshotSrv.ts index 1ca4894de22..c9d240ee3b4 100644 --- a/public/app/features/dashboard/services/SnapshotSrv.ts +++ b/public/app/features/dashboard/services/SnapshotSrv.ts @@ -2,7 +2,7 @@ import { lastValueFrom, map } from 'rxjs'; import { config, getBackendSrv, FetchResponse } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { getAPINamespace } from '../../../api/utils'; diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index eacc6be7378..cea82ff384a 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -27,8 +27,13 @@ import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { variableAdapters } from 'app/features/variables/adapters'; import { onTimeRangeUpdated } from 'app/features/variables/state/actions'; import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors'; -import { CoreEvents, DashboardMeta } from 'app/types'; -import { DashboardMetaChangedEvent, DashboardPanelsChangedEvent, RenderEvent } from 'app/types/events'; +import { DashboardMeta } from 'app/types/dashboard'; +import { + DashboardMetaChangedEvent, + DashboardPanelsChangedEvent, + RenderEvent, + templateVariableValueUpdated, +} from 'app/types/events'; import { appEvents } from '../../../core/core'; import { dispatch } from '../../../store/store'; @@ -1155,7 +1160,7 @@ export class DashboardModel implements TimeModel { templateVariableValueUpdated() { this.processRepeats(); - this.events.emit(CoreEvents.templateVariableValueUpdated); + this.events.emit(templateVariableValueUpdated); } getPanelByUrlId(panelUrlId: string) { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 3002dd32134..8556006879d 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -25,13 +25,13 @@ import { getTemplateSrv, RefreshEvent } from '@grafana/runtime'; import { LibraryPanel, LibraryPanelRef } from '@grafana/schema'; import config from 'app/core/config'; import { safeStringifyValue } from 'app/core/utils/explore'; -import { QueryGroupOptions } from 'app/types'; import { PanelOptionsChangedEvent, PanelQueriesChangedEvent, PanelTransformationsChangedEvent, RenderEvent, } from 'app/types/events'; +import { QueryGroupOptions } from 'app/types/query'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { TimeOverrideResult } from '../utils/panel'; diff --git a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts index 8dec4d28339..28bc7897330 100644 --- a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts +++ b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts @@ -8,7 +8,7 @@ import { VariableModel, } from '@grafana/schema'; import { GetVariables } from 'app/features/variables/state/selectors'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { DashboardModel } from '../DashboardModel'; diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index ed28663f745..0395c6e9201 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -7,7 +7,7 @@ import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { removeAllPanels } from 'app/features/panel/state/reducers'; import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { loadPluginDashboards } from '../../plugins/admin/state/actions'; import { cancelVariables } from '../../variables/state/actions'; diff --git a/public/app/features/dashboard/state/initDashboard.test.ts b/public/app/features/dashboard/state/initDashboard.test.ts index d291b62f379..123552c6984 100644 --- a/public/app/features/dashboard/state/initDashboard.test.ts +++ b/public/app/features/dashboard/state/initDashboard.test.ts @@ -9,7 +9,7 @@ import { KeybindingSrv } from 'app/core/services/keybindingSrv'; import { variableAdapters } from 'app/features/variables/adapters'; import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter'; import { constantBuilder } from 'app/features/variables/shared/testing/builders'; -import { DashboardInitPhase, DashboardRoutes } from 'app/types'; +import { DashboardInitPhase, DashboardRoutes } from 'app/types/dashboard'; import { Echo } from '../../../core/services/echo/Echo'; import { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 92feb552f55..348bb55616c 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -27,10 +27,8 @@ import { DashboardRoutes, HomeDashboardRedirectDTO, isRedirectResponse, - StoreState, - ThunkDispatch, - ThunkResult, -} from 'app/types'; +} from 'app/types/dashboard'; +import { StoreState, ThunkDispatch, ThunkResult } from 'app/types/store'; import { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner'; import { initVariablesTransaction } from '../../variables/state/actions'; diff --git a/public/app/features/dashboard/state/reducers.test.ts b/public/app/features/dashboard/state/reducers.test.ts index 0cb8ee5cd32..f2d44fa61fb 100644 --- a/public/app/features/dashboard/state/reducers.test.ts +++ b/public/app/features/dashboard/state/reducers.test.ts @@ -1,4 +1,4 @@ -import { DashboardInitPhase, DashboardState } from 'app/types'; +import { DashboardInitPhase, DashboardState } from 'app/types/dashboard'; import { createDashboardModelFixture, createPanelSaveModel } from './__fixtures__/dashboardFixtures'; import { diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index ddc842fc568..c96d82d8bef 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -3,7 +3,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; import { defaultDashboard } from '@grafana/schema'; -import { DashboardInitError, DashboardInitPhase, DashboardState } from 'app/types'; +import { DashboardInitError, DashboardInitPhase, DashboardState } from 'app/types/dashboard'; import { DashboardModel } from './DashboardModel'; import { PanelModel } from './PanelModel'; diff --git a/public/app/features/dashboard/state/selectors.ts b/public/app/features/dashboard/state/selectors.ts index 59d486d3a73..d45630812fd 100644 --- a/public/app/features/dashboard/state/selectors.ts +++ b/public/app/features/dashboard/state/selectors.ts @@ -1,5 +1,5 @@ import { PanelPlugin } from '@grafana/data'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { getPanelPluginNotFound } from '../../panel/components/PanelPluginError'; diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts index 496e825cb69..3a18ce049dc 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.test.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.test.ts @@ -6,7 +6,7 @@ import config from 'app/core/config'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import * as actions from 'app/features/explore/state/main'; import { setStore } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PanelModel } from '../state/PanelModel'; import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures'; diff --git a/public/app/features/datasources/components/DashboardsTable.test.tsx b/public/app/features/datasources/components/DashboardsTable.test.tsx index 3aac4b5a6a9..46b8148c992 100644 --- a/public/app/features/datasources/components/DashboardsTable.test.tsx +++ b/public/app/features/datasources/components/DashboardsTable.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { PluginDashboard } from 'app/types'; +import { PluginDashboard } from 'app/types/plugins'; import DashboardsTable, { Props } from './DashboardsTable'; diff --git a/public/app/features/datasources/components/DashboardsTable.tsx b/public/app/features/datasources/components/DashboardsTable.tsx index 0f3424a05df..145114ed6d5 100644 --- a/public/app/features/datasources/components/DashboardsTable.tsx +++ b/public/app/features/datasources/components/DashboardsTable.tsx @@ -1,6 +1,6 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Icon } from '@grafana/ui'; -import { PluginDashboard } from 'app/types'; +import { PluginDashboard } from 'app/types/plugins'; export interface Props { // List of plugin dashboards to show in the table diff --git a/public/app/features/datasources/components/DataSourceAddButton.tsx b/public/app/features/datasources/components/DataSourceAddButton.tsx index f41ad1a657b..627c3695da0 100644 --- a/public/app/features/datasources/components/DataSourceAddButton.tsx +++ b/public/app/features/datasources/components/DataSourceAddButton.tsx @@ -5,7 +5,7 @@ import { config } from '@grafana/runtime'; import { LinkButton } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { ROUTES } from 'app/features/connections/constants'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { trackAddNewDsClicked } from '../tracking'; diff --git a/public/app/features/datasources/components/DataSourceCategories.tsx b/public/app/features/datasources/components/DataSourceCategories.tsx index 58fa0e3783f..45eb2623e45 100644 --- a/public/app/features/datasources/components/DataSourceCategories.tsx +++ b/public/app/features/datasources/components/DataSourceCategories.tsx @@ -5,7 +5,7 @@ import { DataSourcePluginMeta, GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { LinkButton, useStyles2 } from '@grafana/ui'; -import { DataSourcePluginCategory } from 'app/types'; +import { DataSourcePluginCategory } from 'app/types/datasources'; import { ROUTES } from '../../connections/constants'; diff --git a/public/app/features/datasources/components/DataSourceDashboards.tsx b/public/app/features/datasources/components/DataSourceDashboards.tsx index d4011844e4c..0a16c265a02 100644 --- a/public/app/features/datasources/components/DataSourceDashboards.tsx +++ b/public/app/features/datasources/components/DataSourceDashboards.tsx @@ -3,7 +3,8 @@ import { useEffect } from 'react'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { importDashboard, removeDashboard } from 'app/features/dashboard/state/actions'; import { loadPluginDashboards } from 'app/features/plugins/admin/state/actions'; -import { PluginDashboard, StoreState, useDispatch, useSelector } from 'app/types'; +import { PluginDashboard } from 'app/types/plugins'; +import { StoreState, useDispatch, useSelector } from 'app/types/store'; import DashboardTable from '../components/DashboardsTable'; import { useInitDataSourceSettings } from '../state/hooks'; diff --git a/public/app/features/datasources/components/DataSourcesList.tsx b/public/app/features/datasources/components/DataSourcesList.tsx index 1437fa395e8..b4337792ebc 100644 --- a/public/app/features/datasources/components/DataSourcesList.tsx +++ b/public/app/features/datasources/components/DataSourcesList.tsx @@ -7,7 +7,8 @@ import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { EmptyState, LinkButton, TextLink, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { StoreState, AccessControlAction, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState, useSelector } from 'app/types/store'; import { ROUTES } from '../../connections/constants'; import { useLoadDataSources } from '../state/hooks'; diff --git a/public/app/features/datasources/components/DataSourcesListHeader.tsx b/public/app/features/datasources/components/DataSourcesListHeader.tsx index f23f8b3cb6b..26db52a7a33 100644 --- a/public/app/features/datasources/components/DataSourcesListHeader.tsx +++ b/public/app/features/datasources/components/DataSourcesListHeader.tsx @@ -3,7 +3,7 @@ import { useCallback, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; -import { StoreState, useSelector, useDispatch } from 'app/types'; +import { StoreState, useSelector, useDispatch } from 'app/types/store'; import { setDataSourcesSearchQuery, setIsSortAscending } from '../state/reducers'; import { getDataSourcesSearchQuery, getDataSourcesSort } from '../state/selectors'; diff --git a/public/app/features/datasources/components/EditDataSource.tsx b/public/app/features/datasources/components/EditDataSource.tsx index 920979f0cd4..06877e8ad72 100644 --- a/public/app/features/datasources/components/EditDataSource.tsx +++ b/public/app/features/datasources/components/EditDataSource.tsx @@ -13,7 +13,8 @@ import { import { getDataSourceSrv, usePluginComponents, UsePluginComponentsResult } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; -import { DataSourceSettingsState, useDispatch } from 'app/types'; +import { DataSourceSettingsState } from 'app/types/datasources'; +import { useDispatch } from 'app/types/store'; import { useDataSource, diff --git a/public/app/features/datasources/components/NewDataSource.tsx b/public/app/features/datasources/components/NewDataSource.tsx index 7803caac224..8db925f5e87 100644 --- a/public/app/features/datasources/components/NewDataSource.tsx +++ b/public/app/features/datasources/components/NewDataSource.tsx @@ -5,7 +5,8 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, FilterInput } from '@grafana/ui'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { PluginsErrorsInfo } from 'app/features/plugins/components/PluginsErrorsInfo'; -import { DataSourcePluginCategory, StoreState, useDispatch, useSelector } from 'app/types'; +import { DataSourcePluginCategory } from 'app/types/datasources'; +import { StoreState, useDispatch, useSelector } from 'app/types/store'; import { ROUTES } from '../../connections/constants'; import { DataSourceCategories } from '../components/DataSourceCategories'; diff --git a/public/app/features/datasources/components/picker/AddNewDataSourceButton.tsx b/public/app/features/datasources/components/picker/AddNewDataSourceButton.tsx index b5489e59cb1..7e03ccf2c8b 100644 --- a/public/app/features/datasources/components/picker/AddNewDataSourceButton.tsx +++ b/public/app/features/datasources/components/picker/AddNewDataSourceButton.tsx @@ -3,7 +3,7 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, ButtonVariant } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; interface AddNewDataSourceButtonProps { onClick?: () => void; diff --git a/public/app/features/datasources/components/picker/utils.ts b/public/app/features/datasources/components/picker/utils.ts index c626047631c..ff987c26f5b 100644 --- a/public/app/features/datasources/components/picker/utils.ts +++ b/public/app/features/datasources/components/picker/utils.ts @@ -5,7 +5,7 @@ import { setLastUsedDatasourceKeyForDashboard, } from 'app/features/dashboard/utils/dashboard'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { QueryGroupDataSource } from 'app/types'; +import { QueryGroupDataSource } from 'app/types/query'; export function isDataSourceMatch( ds: DataSourceInstanceSettings | undefined, diff --git a/public/app/features/datasources/mocks/dataSourcesMocks.ts b/public/app/features/datasources/mocks/dataSourcesMocks.ts index b58dec19453..807082bd843 100644 --- a/public/app/features/datasources/mocks/dataSourcesMocks.ts +++ b/public/app/features/datasources/mocks/dataSourcesMocks.ts @@ -2,7 +2,8 @@ import { merge } from 'lodash'; import { DataSourceSettings, DataSourcePluginMeta, DataSourceJsonData } from '@grafana/data'; import amazonWebServicesPng from 'app/plugins/datasource/cloudwatch/img/amazon-web-services.png'; -import { DataSourceSettingsState, PluginDashboard } from 'app/types'; +import { DataSourceSettingsState } from 'app/types/datasources'; +import { PluginDashboard } from 'app/types/plugins'; export const getMockDashboard = (override?: Partial<PluginDashboard>) => ({ uid: 'G1btqkgkK', diff --git a/public/app/features/datasources/state/actions.test.ts b/public/app/features/datasources/state/actions.test.ts index 0109a8c70fe..79aaf70a62c 100644 --- a/public/app/features/datasources/state/actions.test.ts +++ b/public/app/features/datasources/state/actions.test.ts @@ -4,7 +4,7 @@ import { AppPluginMeta, DataSourceSettings, PluginMetaInfo, PluginType } from '@ import { DataSourceSrv, FetchError } from '@grafana/runtime'; import { appEvents } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { ThunkResult, ThunkDispatch } from 'app/types'; +import { ThunkResult, ThunkDispatch } from 'app/types/store'; import * as api from '../api'; import { DATASOURCES_ROUTES } from '../constants'; diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 7cd1411b9eb..09835ce72b2 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -23,9 +23,11 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceAPIVersions } from 'app/features/apiserver/client'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { importDataSourcePlugin } from 'app/features/plugins/pluginLoader'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; -import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader'; -import { AccessControlAction, DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { DataSourcePluginCategory } from 'app/types/datasources'; +import { ThunkDispatch, ThunkResult } from 'app/types/store'; import * as api from '../api'; import { DATASOURCES_ROUTES } from '../constants'; diff --git a/public/app/features/datasources/state/buildCategories.ts b/public/app/features/datasources/state/buildCategories.ts index b357798fdaa..ec813a6e1e6 100644 --- a/public/app/features/datasources/state/buildCategories.ts +++ b/public/app/features/datasources/state/buildCategories.ts @@ -1,7 +1,7 @@ import { DataSourcePluginMeta, PluginType } from '@grafana/data'; import { t } from '@grafana/i18n'; import { featureEnabled } from '@grafana/runtime'; -import { DataSourcePluginCategory } from 'app/types'; +import { DataSourcePluginCategory } from 'app/types/datasources'; import grafanaIconSvg from 'img/grafana_icon.svg'; import adobeAnalyticsSvg from 'img/plugins/adobe-analytics.svg'; import appdynamicsSvg from 'img/plugins/appdynamics.svg'; diff --git a/public/app/features/datasources/state/hooks.ts b/public/app/features/datasources/state/hooks.ts index af0c5dd7e6b..61e7be7ae09 100644 --- a/public/app/features/datasources/state/hooks.ts +++ b/public/app/features/datasources/state/hooks.ts @@ -5,8 +5,9 @@ import { t } from '@grafana/i18n'; import { cleanUpAction } from 'app/core/actions/cleanUp'; import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, useDispatch, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { useDispatch, useSelector } from 'app/types/store'; import { ROUTES } from '../../connections/constants'; import { DataSourceRights } from '../types'; diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index c71bff78248..2c685beae68 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -5,7 +5,7 @@ import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { highlightTrial } from 'app/features/admin/utils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import icnDatasourceSvg from 'img/icn-datasource.svg'; import { GenericDataSourcePlugin } from '../types'; diff --git a/public/app/features/datasources/state/reducers.test.ts b/public/app/features/datasources/state/reducers.test.ts index 85f6455e837..43555d3208b 100644 --- a/public/app/features/datasources/state/reducers.test.ts +++ b/public/app/features/datasources/state/reducers.test.ts @@ -1,7 +1,7 @@ import { reducerTester } from 'test/core/redux/reducerTester'; import { PluginMeta, PluginMetaInfo, PluginType, LayoutModes } from '@grafana/data'; -import { DataSourceSettingsState, DataSourcesState } from 'app/types'; +import { DataSourceSettingsState, DataSourcesState } from 'app/types/datasources'; import { getMockDataSource, getMockDataSources } from '../mocks/dataSourcesMocks'; import { GenericDataSourcePlugin } from '../types'; diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index e5b2588499c..700f06e6133 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -3,7 +3,7 @@ import { AnyAction, createAction } from '@reduxjs/toolkit'; import { DataSourcePluginMeta, DataSourceSettings, LayoutMode, LayoutModes } from '@grafana/data'; import { t } from '@grafana/i18n'; import { TestingStatus } from '@grafana/runtime'; -import { DataSourcesState, DataSourceSettingsState } from 'app/types'; +import { DataSourcesState, DataSourceSettingsState } from 'app/types/datasources'; import { GenericDataSourcePlugin } from '../types'; diff --git a/public/app/features/explore/CorrelationEditorModeBar.tsx b/public/app/features/explore/CorrelationEditorModeBar.tsx index 87d531eb41d..25739ad1b5c 100644 --- a/public/app/features/explore/CorrelationEditorModeBar.tsx +++ b/public/app/features/explore/CorrelationEditorModeBar.tsx @@ -7,7 +7,8 @@ import { Trans, t } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { Button, Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { Prompt } from 'app/core/components/FormPrompt/Prompt'; -import { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState, useDispatch, useSelector } from 'app/types'; +import { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState } from 'app/types/explore'; +import { useDispatch, useSelector } from 'app/types/store'; import { CorrelationUnsavedChangesModal } from './CorrelationUnsavedChangesModal'; import { showModalMessage } from './correlationEditLogic'; diff --git a/public/app/features/explore/CorrelationHelper.tsx b/public/app/features/explore/CorrelationHelper.tsx index 2444a4dd7b6..bf57509bf63 100644 --- a/public/app/features/explore/CorrelationHelper.tsx +++ b/public/app/features/explore/CorrelationHelper.tsx @@ -19,7 +19,7 @@ import { Icon, Stack, } from '@grafana/ui'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { getTransformationVars } from '../correlations/transformations'; import { generateDefaultLabel } from '../correlations/utils'; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index bbaf27f3524..57ef823de4d 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -33,7 +33,7 @@ import { import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/internal'; import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { getTimeZone } from '../profile/state/selectors'; diff --git a/public/app/features/explore/ExploreActions.tsx b/public/app/features/explore/ExploreActions.tsx index de35e644b4e..376cc25d715 100644 --- a/public/app/features/explore/ExploreActions.tsx +++ b/public/app/features/explore/ExploreActions.tsx @@ -4,7 +4,8 @@ import { useEffect, useState } from 'react'; import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { AccessControlAction, useDispatch, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useDispatch, useSelector } from 'app/types/store'; import { splitOpen, splitClose, changeCorrelationEditorDetails } from './state/main'; import { runQueries } from './state/query'; diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 6799eddec3b..d5974011bbb 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -9,8 +9,8 @@ import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPane import { useGrafana } from 'app/core/context/GrafanaContext'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { useSelector } from 'app/types'; import { ExploreQueryParams } from 'app/types/explore'; +import { useSelector } from 'app/types/store'; import { CorrelationEditorModeBar } from './CorrelationEditorModeBar'; import { ExploreActions } from './ExploreActions'; diff --git a/public/app/features/explore/ExplorePaneContainer.tsx b/public/app/features/explore/ExplorePaneContainer.tsx index 07aba1a5a41..d6d9831bbc2 100644 --- a/public/app/features/explore/ExplorePaneContainer.tsx +++ b/public/app/features/explore/ExplorePaneContainer.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { EventBusSrv, getTimeZone } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { stopQueryState } from 'app/core/utils/explore'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; import Explore from './Explore'; import ExploreQueryInspector from './ExploreQueryInspector'; diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index 24f73322bc1..70badc6c689 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -15,7 +15,8 @@ import { InspectJSONTab } from 'app/features/inspector/InspectJSONTab'; import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab'; import { QueryInspector } from 'app/features/inspector/QueryInspector'; import { mixedRequestId } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { StoreState, ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import { GetDataOptions } from '../query/state/PanelQueryRunner'; diff --git a/public/app/features/explore/ExploreRunQueryButton.test.tsx b/public/app/features/explore/ExploreRunQueryButton.test.tsx index e2b518654e9..0cb9290f62c 100644 --- a/public/app/features/explore/ExploreRunQueryButton.test.tsx +++ b/public/app/features/explore/ExploreRunQueryButton.test.tsx @@ -6,7 +6,7 @@ import { DatasourceSrvMock, MockDataSourceApi } from 'test/mocks/datasource_srv' import { DataSourceApi } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; import { configureStore } from 'app/store/configureStore'; -import { ExploreItemState, ExploreState } from 'app/types'; +import { ExploreItemState, ExploreState } from 'app/types/explore'; import { Props, ExploreRunQueryButton } from './ExploreRunQueryButton'; import { makeExplorePaneState } from './state/utils'; diff --git a/public/app/features/explore/ExploreRunQueryButton.tsx b/public/app/features/explore/ExploreRunQueryButton.tsx index 49a2fe3acaa..78f54525705 100644 --- a/public/app/features/explore/ExploreRunQueryButton.tsx +++ b/public/app/features/explore/ExploreRunQueryButton.tsx @@ -5,7 +5,7 @@ import { t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Button, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { changeDatasource } from './state/datasource'; import { setQueries } from './state/query'; diff --git a/public/app/features/explore/Graph/ExploreGraph.tsx b/public/app/features/explore/Graph/ExploreGraph.tsx index cd69a11af37..e4dedb3f76c 100644 --- a/public/app/features/explore/Graph/ExploreGraph.tsx +++ b/public/app/features/explore/Graph/ExploreGraph.tsx @@ -31,7 +31,7 @@ import { import { PanelContext, PanelContextProvider, SeriesVisibilityChangeMode, useTheme2 } from '@grafana/ui'; import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config'; import { Options as TimeSeriesOptions } from 'app/plugins/panel/timeseries/panelcfg.gen'; -import { ExploreGraphStyle } from 'app/types'; +import { ExploreGraphStyle } from 'app/types/explore'; import { seriesVisibilityConfigFactory } from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory'; import { useExploreDataLinkPostProcessor } from '../hooks/useExploreDataLinkPostProcessor'; diff --git a/public/app/features/explore/Graph/ExploreGraphLabel.tsx b/public/app/features/explore/Graph/ExploreGraphLabel.tsx index a8be709f53b..9b2069546eb 100644 --- a/public/app/features/explore/Graph/ExploreGraphLabel.tsx +++ b/public/app/features/explore/Graph/ExploreGraphLabel.tsx @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; import { RadioButtonGroup } from '@grafana/ui'; -import { EXPLORE_GRAPH_STYLES, ExploreGraphStyle } from 'app/types'; +import { EXPLORE_GRAPH_STYLES, ExploreGraphStyle } from 'app/types/explore'; const ALL_GRAPH_STYLE_OPTIONS: Array<SelectableValue<ExploreGraphStyle>> = EXPLORE_GRAPH_STYLES.map((style) => ({ value: style, diff --git a/public/app/features/explore/Graph/GraphContainer.tsx b/public/app/features/explore/Graph/GraphContainer.tsx index b9a9fe44a5f..7cbf420dccf 100644 --- a/public/app/features/explore/Graph/GraphContainer.tsx +++ b/public/app/features/explore/Graph/GraphContainer.tsx @@ -13,7 +13,7 @@ import { } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { GraphThresholdsStyleConfig, PanelChrome, PanelChromeProps } from '@grafana/ui'; -import { ExploreGraphStyle } from 'app/types'; +import { ExploreGraphStyle } from 'app/types/explore'; import { LimitedDataDisclaimer } from '../LimitedDataDisclaimer'; import { storeGraphStyle } from '../state/utils'; diff --git a/public/app/features/explore/Graph/exploreGraphStyleUtils.ts b/public/app/features/explore/Graph/exploreGraphStyleUtils.ts index 224ece64100..6206d387058 100644 --- a/public/app/features/explore/Graph/exploreGraphStyleUtils.ts +++ b/public/app/features/explore/Graph/exploreGraphStyleUtils.ts @@ -2,7 +2,7 @@ import { produce } from 'immer'; import { FieldConfigSource, ThresholdsConfig } from '@grafana/data'; import { GraphDrawStyle, GraphFieldConfig, GraphThresholdsStyleConfig, StackingMode } from '@grafana/schema'; -import { ExploreGraphStyle } from 'app/types'; +import { ExploreGraphStyle } from 'app/types/explore'; export type FieldConfig = FieldConfigSource<GraphFieldConfig>; diff --git a/public/app/features/explore/Graph/utils.ts b/public/app/features/explore/Graph/utils.ts index 3b42e342c1f..38ee661969b 100644 --- a/public/app/features/explore/Graph/utils.ts +++ b/public/app/features/explore/Graph/utils.ts @@ -1,5 +1,5 @@ import store from 'app/core/store'; -import { ExploreGraphStyle, EXPLORE_GRAPH_STYLES } from 'app/types'; +import { ExploreGraphStyle, EXPLORE_GRAPH_STYLES } from 'app/types/explore'; const GRAPH_STYLE_KEY = 'grafana.explore.style.graph'; export const storeGraphStyle = (graphStyle: string): void => { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 897bca4f24d..4507fa9e028 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -60,7 +60,8 @@ import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; import { isLokiQuery } from 'app/plugins/datasource/loki/queryUtils'; import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { getState } from 'app/store/store'; -import { ExploreItemState, useDispatch } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { useDispatch } from 'app/types/store'; import { contentOutlineTrackPinAdded, diff --git a/public/app/features/explore/Logs/LogsContainer.tsx b/public/app/features/explore/Logs/LogsContainer.tsx index 8ca38ab007d..6ff6c9a248c 100644 --- a/public/app/features/explore/Logs/LogsContainer.tsx +++ b/public/app/features/explore/Logs/LogsContainer.tsx @@ -27,8 +27,8 @@ import { DataQuery } from '@grafana/schema'; import { Collapse } from '@grafana/ui'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; -import { StoreState } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import { getTimeZone } from '../../profile/state/selectors'; import { diff --git a/public/app/features/explore/NoDataSourceCallToAction.tsx b/public/app/features/explore/NoDataSourceCallToAction.tsx index 822da87b1c6..5d67b2d1475 100644 --- a/public/app/features/explore/NoDataSourceCallToAction.tsx +++ b/public/app/features/explore/NoDataSourceCallToAction.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { LinkButton, CallToActionCard, Icon, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; function getCardStyles(theme: GrafanaTheme2) { return css({ diff --git a/public/app/features/explore/NodeGraph/NodeGraphContainer.tsx b/public/app/features/explore/NodeGraph/NodeGraphContainer.tsx index c0e15131f0d..5c73e85776f 100644 --- a/public/app/features/explore/NodeGraph/NodeGraphContainer.tsx +++ b/public/app/features/explore/NodeGraph/NodeGraphContainer.tsx @@ -8,11 +8,11 @@ import { Trans, t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { useStyles2, useTheme2, PanelChrome } from '@grafana/ui'; import { layeredLayoutThreshold } from 'app/plugins/panel/nodeGraph/NodeGraph'; +import { StoreState } from 'app/types/store'; import { NodeGraph } from '../../../plugins/panel/nodeGraph'; import { LayoutAlgorithm } from '../../../plugins/panel/nodeGraph/panelcfg.gen'; import { useCategorizeFrames } from '../../../plugins/panel/nodeGraph/useCategorizeFrames'; -import { StoreState } from '../../../types'; import { useLinks } from '../utils/links'; const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx b/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx index abcbc468bd5..67cf1055912 100644 --- a/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx +++ b/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useState, createContext, useContext, useEffect } from 'react'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { selectRichHistorySettings } from '../state/selectors'; diff --git a/public/app/features/explore/QueryRows.test.tsx b/public/app/features/explore/QueryRows.test.tsx index 8acb5e80b71..03989ec2950 100644 --- a/public/app/features/explore/QueryRows.test.tsx +++ b/public/app/features/explore/QueryRows.test.tsx @@ -5,7 +5,7 @@ import { DataSourceApi } from '@grafana/data'; import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { configureStore } from 'app/store/configureStore'; -import { ExploreState } from 'app/types'; +import { ExploreState } from 'app/types/explore'; import { UserState } from '../profile/state/reducers'; diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 42b39a20f6b..084c180544f 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react'; import { CoreApp, getNextRefId } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { DataQuery, DataSourceRef } from '@grafana/schema'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { getDatasourceSrv } from '../plugins/datasource_srv'; import { QueryEditorRows } from '../query/components/QueryEditorRows'; diff --git a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.test.tsx b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.test.tsx index 7efd5f866e9..c1f0ed2aad2 100644 --- a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.test.tsx +++ b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react'; import { FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame, LoadingState } from '@grafana/data'; import { getTemplateSrv } from 'app/features/templating/template_srv'; -import { TABLE_RESULTS_STYLE } from 'app/types'; +import { TABLE_RESULTS_STYLE } from 'app/types/explore'; import { RawPrometheusContainer } from './RawPrometheusContainer'; diff --git a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx index 4269c1d7724..5ba8c1a9e2b 100644 --- a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx +++ b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx @@ -8,8 +8,8 @@ import { TimeZone } from '@grafana/schema'; import { RadioButtonGroup, Table, AdHocFilterItem, PanelChrome } from '@grafana/ui'; import { config } from 'app/core/config'; import { PANEL_BORDER } from 'app/core/constants'; -import { StoreState, TABLE_RESULTS_STYLE } from 'app/types'; -import { ExploreItemState, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore'; +import { ExploreItemState, TABLE_RESULTS_STYLE, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import { MetaInfoText } from '../MetaInfoText'; import RawListContainer from '../PrometheusListView/RawListContainer'; diff --git a/public/app/features/explore/ResponseErrorContainer.tsx b/public/app/features/explore/ResponseErrorContainer.tsx index e0724c1a666..8124d2e68d1 100644 --- a/public/app/features/explore/ResponseErrorContainer.tsx +++ b/public/app/features/explore/ResponseErrorContainer.tsx @@ -1,5 +1,5 @@ import { LoadingState } from '@grafana/data'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { ErrorContainer } from './ErrorContainer'; diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index d78a69d14f5..b4fdc2b28e6 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -11,8 +11,8 @@ import { RichHistorySettings, createDatasourcesList, } from 'app/core/utils/richHistory'; -import { useSelector } from 'app/types'; import { RichHistoryQuery } from 'app/types/explore'; +import { useSelector } from 'app/types/store'; import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { Tabs } from '../QueriesDrawer/QueriesDrawerContext'; diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx index f0a9d5410f3..669f7d0dd4a 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx @@ -6,8 +6,8 @@ import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta } from import { DataQuery, DataSourceRef } from '@grafana/schema'; import { MixedDatasource } from 'app/plugins/datasource/mixed/MixedDataSource'; import { configureStore } from 'app/store/configureStore'; -import { ExploreState, RichHistoryQuery } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { ExploreState, RichHistoryQuery } from 'app/types/explore'; import { RichHistoryCard, Props } from './RichHistoryCard'; diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index a08c439aa33..6d622353d38 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -6,7 +6,7 @@ import { Trans } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { useTheme2 } from '@grafana/ui'; // Types -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; // Components, enums import { useQueriesDrawerContext } from '../QueriesDrawer/QueriesDrawerContext'; diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index 542f3b3d9e1..51c8f36851b 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -12,8 +12,8 @@ import { RichHistorySearchFilters, RichHistorySettings, } from 'app/core/utils/richHistory'; -import { useSelector } from 'app/types'; import { RichHistoryQuery } from 'app/types/explore'; +import { useSelector } from 'app/types/store'; import { selectExploreDSMaps } from '../state/selectors'; diff --git a/public/app/features/explore/SecondaryActions.tsx b/public/app/features/explore/SecondaryActions.tsx index e1f378bf262..cd9d25b860b 100644 --- a/public/app/features/explore/SecondaryActions.tsx +++ b/public/app/features/explore/SecondaryActions.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { ToolbarButton, useTheme2 } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { createDatasourcesList } from '../../core/utils/richHistory'; import { MIXED_DATASOURCE_NAME } from '../../plugins/datasource/mixed/MixedDataSource'; diff --git a/public/app/features/explore/ShortLinkButtonMenu.tsx b/public/app/features/explore/ShortLinkButtonMenu.tsx index 11ba51875e8..6764e007e3b 100644 --- a/public/app/features/explore/ShortLinkButtonMenu.tsx +++ b/public/app/features/explore/ShortLinkButtonMenu.tsx @@ -6,7 +6,7 @@ import { reportInteraction, config } from '@grafana/runtime'; import { Dropdown, Menu, MenuGroup, ButtonGroup, Button } from '@grafana/ui'; import { copyStringToClipboard } from 'app/core/utils/explore'; import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { selectPanes } from './state/selectors'; import { constructAbsoluteUrl } from './utils/links'; diff --git a/public/app/features/explore/Table/TableContainer.tsx b/public/app/features/explore/Table/TableContainer.tsx index 802c5602861..b0a203bd22a 100644 --- a/public/app/features/explore/Table/TableContainer.tsx +++ b/public/app/features/explore/Table/TableContainer.tsx @@ -12,8 +12,8 @@ import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames, } from 'app/plugins/panel/table/migrations'; -import { StoreState } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import { LimitedDataDisclaimer } from '../LimitedDataDisclaimer'; import { MetaInfoText } from '../MetaInfoText'; diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index 4afbe1d6b4a..ecb71ee92a8 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -23,7 +23,7 @@ import { useStyles2 } from '@grafana/ui'; import { TempoQuery } from '@grafana-plugins/tempo/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getTimeZone } from 'app/features/profile/state/selectors'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { changePanelState } from '../state/explorePane'; diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index 5fbcd3ba4a5..9eff3de76f2 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { DataFrame, SplitOpen, TimeRange } from '@grafana/data'; import { t } from '@grafana/i18n'; import { PanelChrome } from '@grafana/ui'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; import { TraceView } from './TraceView'; import { transformDataFrames } from './utils/transform'; diff --git a/public/app/features/explore/correlationEditLogic.test.ts b/public/app/features/explore/correlationEditLogic.test.ts index dd3c37e95c6..fa80b340226 100644 --- a/public/app/features/explore/correlationEditLogic.test.ts +++ b/public/app/features/explore/correlationEditLogic.test.ts @@ -1,4 +1,4 @@ -import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types'; +import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types/explore'; import { showModalMessage } from './correlationEditLogic'; diff --git a/public/app/features/explore/correlationEditLogic.ts b/public/app/features/explore/correlationEditLogic.ts index 326389505c7..0e507453306 100644 --- a/public/app/features/explore/correlationEditLogic.ts +++ b/public/app/features/explore/correlationEditLogic.ts @@ -1,6 +1,6 @@ import { template } from 'lodash'; -import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types'; +import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types/explore'; enum CONSEQUENCES { SOURCE_TARGET_CHANGE = 'cause the query in the right pane to be re-ran and links added to that data', diff --git a/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx b/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx index 87dbc64f5ac..a3c5f1a22fb 100644 --- a/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx +++ b/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx @@ -1,7 +1,7 @@ import { type ReactElement } from 'react'; import { AddToDashboardForm } from 'app/features/dashboard-scene/addToDashboard/AddToDashboardForm'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { getExploreItemSelector } from '../../state/selectors'; diff --git a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts index 1c7c8912a17..2f42709c2b0 100644 --- a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts +++ b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts @@ -1,6 +1,6 @@ import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data'; import { DataQuery, LoadingState } from '@grafana/schema'; -import { ExplorePanelData } from 'app/types'; +import { ExplorePanelData } from 'app/types/explore'; import { buildDashboardPanelFromExploreState } from './addToDashboard'; diff --git a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts index ac2e9cca741..ea78a8f81ff 100644 --- a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts +++ b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts @@ -2,7 +2,7 @@ import { DataFrame, ExplorePanelsState } from '@grafana/data'; import { t } from '@grafana/i18n'; import { DataQuery, DataSourceRef, Panel } from '@grafana/schema'; import { DataTransformerConfig } from '@grafana/schema/dist/esm/raw/dashboard/x/dashboard_types.gen'; -import { ExplorePanelData } from 'app/types'; +import { ExplorePanelData } from 'app/types/explore'; interface ExploreToDashboardPanelOptions { queries: DataQuery[]; diff --git a/public/app/features/explore/extensions/AddToDashboard/getAddToDashboardTitle.ts b/public/app/features/explore/extensions/AddToDashboard/getAddToDashboardTitle.ts index d528956cb9f..ed90ef5aff7 100644 --- a/public/app/features/explore/extensions/AddToDashboard/getAddToDashboardTitle.ts +++ b/public/app/features/explore/extensions/AddToDashboard/getAddToDashboardTitle.ts @@ -1,5 +1,5 @@ import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export function getAddToDashboardTitle(): string { const canCreateDashboard = contextSrv.hasPermission(AccessControlAction.DashboardsCreate); diff --git a/public/app/features/explore/extensions/AddToDashboard/index.test.tsx b/public/app/features/explore/extensions/AddToDashboard/index.test.tsx index 79a51c929cb..32d84a9466b 100644 --- a/public/app/features/explore/extensions/AddToDashboard/index.test.tsx +++ b/public/app/features/explore/extensions/AddToDashboard/index.test.tsx @@ -8,7 +8,7 @@ import { DataQuery } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { Echo } from 'app/core/services/echo/Echo'; import { configureStore } from 'app/store/configureStore'; -import { ExploreState } from 'app/types'; +import { ExploreState } from 'app/types/explore'; import { createEmptyQueryResponse } from '../../state/utils'; diff --git a/public/app/features/explore/extensions/AddToDashboard/index.tsx b/public/app/features/explore/extensions/AddToDashboard/index.tsx index 32277343c17..ef3278a58e9 100644 --- a/public/app/features/explore/extensions/AddToDashboard/index.tsx +++ b/public/app/features/explore/extensions/AddToDashboard/index.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'; import { t } from '@grafana/i18n'; import { Modal, ToolbarButton } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { getExploreItemSelector } from '../../state/selectors'; diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx index 3adcc8ca6fb..0a72d621269 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx @@ -8,7 +8,7 @@ import { usePluginLinks } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; -import { ExplorePanelData, ExploreState } from 'app/types'; +import { ExplorePanelData, ExploreState } from 'app/types/explore'; import { createEmptyQueryResponse } from '../state/utils'; diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx index df78195ab3d..3c3a5f2286a 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx @@ -4,7 +4,9 @@ import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange, getTimeZ import { config, reportInteraction, usePluginLinks } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, ExplorePanelData, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ExplorePanelData } from 'app/types/explore'; +import { useSelector } from 'app/types/store'; import { getExploreItemSelector, isLeftPaneSelector, selectCorrelationDetails } from '../state/selectors'; diff --git a/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx b/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx index 9be348955cf..7bbade45ad9 100644 --- a/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx +++ b/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx @@ -1,7 +1,7 @@ import { PluginExtensionAddedLinkConfig, PluginExtensionPoints } from '@grafana/data'; import { contextSrv } from 'app/core/core'; import { dispatch } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { log } from '../../plugins/extensions/logs/log'; import { createAddedLinkConfig } from '../../plugins/extensions/utils'; diff --git a/public/app/features/explore/hooks/useExplorePageTitle.ts b/public/app/features/explore/hooks/useExplorePageTitle.ts index 804b2cf69d9..564064ef8dc 100644 --- a/public/app/features/explore/hooks/useExplorePageTitle.ts +++ b/public/app/features/explore/hooks/useExplorePageTitle.ts @@ -4,7 +4,7 @@ import { getDataSourceSrv } from '@grafana/runtime'; import { Branding } from 'app/core/components/Branding/Branding'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useNavModel } from 'app/core/hooks/useNavModel'; -import { ExploreQueryParams } from 'app/types'; +import { ExploreQueryParams } from 'app/types/explore'; import { isFulfilled, hasKey } from './utils'; diff --git a/public/app/features/explore/hooks/useKeyboardShortcuts.ts b/public/app/features/explore/hooks/useKeyboardShortcuts.ts index bd90b13e3f2..9ff1e32e5ed 100644 --- a/public/app/features/explore/hooks/useKeyboardShortcuts.ts +++ b/public/app/features/explore/hooks/useKeyboardShortcuts.ts @@ -3,8 +3,8 @@ import { Unsubscribable } from 'rxjs'; import { getAppEvents } from '@grafana/runtime'; import { useGrafana } from 'app/core/context/GrafanaContext'; -import { useDispatch } from 'app/types'; import { AbsoluteTimeEvent, CopyTimeEvent, PasteTimeEvent, ShiftTimeEvent, ZoomOutEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { copyTimeRangeToClipboard, diff --git a/public/app/features/explore/hooks/useSplitSizeUpdater.ts b/public/app/features/explore/hooks/useSplitSizeUpdater.ts index ea2f6279b8a..81c9fa2edeb 100644 --- a/public/app/features/explore/hooks/useSplitSizeUpdater.ts +++ b/public/app/features/explore/hooks/useSplitSizeUpdater.ts @@ -2,7 +2,7 @@ import { inRange } from 'lodash'; import { useState } from 'react'; import { useWindowSize } from 'react-use'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { splitSizeUpdateAction } from '../state/main'; import { isSplit, selectPanesEntries } from '../state/selectors'; diff --git a/public/app/features/explore/hooks/useStateSync/external.utils.ts b/public/app/features/explore/hooks/useStateSync/external.utils.ts index e691ba13b33..2df25f0dfde 100644 --- a/public/app/features/explore/hooks/useStateSync/external.utils.ts +++ b/public/app/features/explore/hooks/useStateSync/external.utils.ts @@ -2,7 +2,7 @@ import { isEmpty, isObject, mapValues, omitBy } from 'lodash'; import { ExploreUrlState, toURLRange } from '@grafana/data'; import { clearQueryKeys } from 'app/core/utils/explore'; -import { ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState { return { diff --git a/public/app/features/explore/hooks/useStateSync/index.ts b/public/app/features/explore/hooks/useStateSync/index.ts index 348a12b0ac4..44462fa0d35 100644 --- a/public/app/features/explore/hooks/useStateSync/index.ts +++ b/public/app/features/explore/hooks/useStateSync/index.ts @@ -2,7 +2,8 @@ import { useEffect, useRef } from 'react'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { addListener, ExploreQueryParams, useDispatch, useSelector } from 'app/types'; +import { ExploreQueryParams } from 'app/types/explore'; +import { addListener, useDispatch, useSelector } from 'app/types/store'; import { selectPanes } from '../../state/selectors'; diff --git a/public/app/features/explore/hooks/useStateSync/parseURL.ts b/public/app/features/explore/hooks/useStateSync/parseURL.ts index 4325630c1a9..ca40e024e0f 100644 --- a/public/app/features/explore/hooks/useStateSync/parseURL.ts +++ b/public/app/features/explore/hooks/useStateSync/parseURL.ts @@ -1,4 +1,4 @@ -import { ExploreQueryParams } from 'app/types'; +import { ExploreQueryParams } from 'app/types/explore'; import { v0Migrator } from './migrators/v0'; import { ExploreURLV1, v1Migrator } from './migrators/v1'; diff --git a/public/app/features/explore/hooks/useStateSync/synchronizer/fromURL.ts b/public/app/features/explore/hooks/useStateSync/synchronizer/fromURL.ts index 5497f481407..c196320a859 100644 --- a/public/app/features/explore/hooks/useStateSync/synchronizer/fromURL.ts +++ b/public/app/features/explore/hooks/useStateSync/synchronizer/fromURL.ts @@ -8,7 +8,8 @@ import { cancelQueries, runQueries, setQueriesAction } from 'app/features/explor import { updateTime } from 'app/features/explore/state/time'; import { fromURLRange } from 'app/features/explore/state/utils'; import { withUniqueRefIds } from 'app/features/explore/utils/queries'; -import { ExploreItemState, ThunkDispatch } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { ThunkDispatch } from 'app/types/store'; import { getUrlStateFromPaneState } from '../index'; import { urlDiff } from '../internal.utils'; diff --git a/public/app/features/explore/hooks/useStateSync/synchronizer/init.ts b/public/app/features/explore/hooks/useStateSync/synchronizer/init.ts index dae7cd105d3..f4dec0dcecb 100644 --- a/public/app/features/explore/hooks/useStateSync/synchronizer/init.ts +++ b/public/app/features/explore/hooks/useStateSync/synchronizer/init.ts @@ -9,7 +9,7 @@ import { clearPanes, syncTimesAction } from 'app/features/explore/state/main'; import { fromURLRange } from 'app/features/explore/state/utils'; import { withUniqueRefIds } from 'app/features/explore/utils/queries'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { ThunkDispatch } from 'app/types'; +import { ThunkDispatch } from 'app/types/store'; import { getUrlStateFromPaneState } from '../index'; import { diff --git a/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts b/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts index ba164419822..9485eef2446 100644 --- a/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts +++ b/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts @@ -9,7 +9,7 @@ import { changePanelsStateAction } from 'app/features/explore/state/explorePane' import { splitClose, splitOpen } from 'app/features/explore/state/main'; import { runQueries } from 'app/features/explore/state/query'; import { changeRangeAction } from 'app/features/explore/state/time'; -import { ExploreState } from 'app/types'; +import { ExploreState } from 'app/types/explore'; import { getUrlStateFromPaneState } from '../index'; import { InitState } from '../internal.utils'; diff --git a/public/app/features/explore/mocks/data.ts b/public/app/features/explore/mocks/data.ts index 46f9355c104..3cf601b623f 100644 --- a/public/app/features/explore/mocks/data.ts +++ b/public/app/features/explore/mocks/data.ts @@ -1,8 +1,7 @@ import { Observable, of } from 'rxjs'; import { getDefaultTimeRange, LoadingState, LogsModel } from '@grafana/data'; - -import { ExplorePanelData } from '../../../types'; +import { ExplorePanelData } from 'app/types/explore'; type MockProps = { logsResult?: Partial<LogsModel>; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index de530b584cc..647284ad25c 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -39,11 +39,11 @@ import { Echo } from 'app/core/services/echo/Echo'; import { setLastUsedDatasourceUID } from 'app/core/utils/explore'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { configureStore } from 'app/store/configureStore'; +import { ExploreQueryParams } from 'app/types/explore'; import { RichHistoryRemoteStorageDTO } from '../../../../core/history/RichHistoryRemoteStorage'; import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource'; import { LokiQuery } from '../../../../plugins/datasource/loki/types'; -import { ExploreQueryParams } from '../../../../types'; import { initialUserState } from '../../../profile/state/reducers'; import ExplorePage from '../../ExplorePage'; import { QueriesDrawerContextProvider } from '../../QueriesDrawer/QueriesDrawerContext'; diff --git a/public/app/features/explore/state/correlations.ts b/public/app/features/explore/state/correlations.ts index 4e093ca3669..cc99f6388b7 100644 --- a/public/app/features/explore/state/correlations.ts +++ b/public/app/features/explore/state/correlations.ts @@ -7,7 +7,7 @@ import { createErrorNotification } from 'app/core/copy/appNotification'; import { CreateCorrelationParams } from 'app/features/correlations/types'; import { getCorrelationsBySourceUIDs, createCorrelation, generateDefaultLabel } from 'app/features/correlations/utils'; import { store } from 'app/store/store'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { saveCorrelationsAction } from './explorePane'; import { splitClose } from './main'; diff --git a/public/app/features/explore/state/datasource.test.ts b/public/app/features/explore/state/datasource.test.ts index b024fdb35dd..44e6d7c74e5 100644 --- a/public/app/features/explore/state/datasource.test.ts +++ b/public/app/features/explore/state/datasource.test.ts @@ -1,6 +1,6 @@ import { DataSourceApi } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; -import { ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; import { updateDatasourceInstanceAction, datasourceReducer } from './datasource'; import { createEmptyQueryResponse } from './utils'; diff --git a/public/app/features/explore/state/datasource.ts b/public/app/features/explore/state/datasource.ts index 762aebe3ee8..0cea9e1126c 100644 --- a/public/app/features/explore/state/datasource.ts +++ b/public/app/features/explore/state/datasource.ts @@ -7,7 +7,8 @@ import { DataSourceRef } from '@grafana/schema'; import { RefreshPicker } from '@grafana/ui'; import { stopQueryState } from 'app/core/utils/explore'; import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils'; -import { ExploreItemState, createAsyncThunk } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { createAsyncThunk } from 'app/types/store'; import { loadSupplementaryQueries } from '../utils/supplementaryQueries'; diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 601ceea2dae..9835b9f3e09 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -16,8 +16,8 @@ import { DataQuery, DataSourceRef } from '@grafana/schema'; import { getQueryKeys } from 'app/core/utils/explore'; import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils'; import { getTimeZone } from 'app/features/profile/state/selectors'; -import { createAsyncThunk, ThunkResult } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; +import { createAsyncThunk, ThunkResult } from 'app/types/store'; import { datasourceReducer } from './datasource'; import { queryReducer, runQueries } from './query'; diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index d582ef42878..4168668091d 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -12,7 +12,8 @@ import { updateRichHistorySettings, updateStarredInRichHistory, } from 'app/core/utils/richHistory'; -import { RichHistoryQuery, ThunkResult } from 'app/types'; +import { RichHistoryQuery } from 'app/types/explore'; +import { ThunkResult } from 'app/types/store'; import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; diff --git a/public/app/features/explore/state/main.test.ts b/public/app/features/explore/state/main.test.ts index d52a1be7211..7d504613831 100644 --- a/public/app/features/explore/state/main.test.ts +++ b/public/app/features/explore/state/main.test.ts @@ -3,11 +3,12 @@ import { thunkTester } from 'test/core/thunk/thunkTester'; import { dateTime, ExploreUrlState, serializeStateToUrlParam } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; +import { ExploreItemState, ExploreState } from 'app/types/explore'; +import { StoreState, ThunkDispatch } from 'app/types/store'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; import { configureStore } from '../../../store/configureStore'; -import { ExploreItemState, ExploreState, StoreState, ThunkDispatch } from '../../../types'; import { exploreReducer, navigateToExplore, splitClose, splitOpen } from './main'; diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 42a3a054d7f..d64ec7eca95 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -8,10 +8,10 @@ import { generateExploreId, GetExploreUrlArguments } from 'app/core/utils/explor import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getTemplateSrv } from 'app/features/templating/template_srv'; import { CorrelationEditorDetailsUpdate, ExploreItemState, ExploreState } from 'app/types/explore'; +import { createAsyncThunk, ThunkResult } from 'app/types/store'; import { RichHistoryResults } from '../../../core/history/RichHistoryStorage'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; -import { createAsyncThunk, ThunkResult } from '../../../types'; import { withUniqueRefIds } from '../utils/queries'; import { DEFAULT_RANGE } from './constants'; diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 8a4f3b96be0..42e486fa978 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -18,7 +18,8 @@ import { import { DataQuery, DataSourceRef } from '@grafana/schema'; import config from 'app/core/config'; import { queryLogsSample, queryLogsVolume } from 'app/features/logs/logsModel'; -import { createAsyncThunk, ExploreItemState, StoreState, ThunkDispatch } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { createAsyncThunk, StoreState, ThunkDispatch } from 'app/types/store'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; import * as richHistory from '../../../core/utils/richHistory'; diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index a070253ebc3..63091242c47 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -39,15 +39,14 @@ import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state import { SupportingQueryType } from 'app/plugins/datasource/loki/types'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { - createAsyncThunk, ExploreItemState, ExplorePanelData, + ExploreState, + QueryOptions, QueryTransaction, - StoreState, - ThunkDispatch, - ThunkResult, -} from 'app/types'; -import { ExploreState, QueryOptions, SupplementaryQueries } from 'app/types/explore'; + SupplementaryQueries, +} from 'app/types/explore'; +import { createAsyncThunk, StoreState, ThunkDispatch, ThunkResult } from 'app/types/store'; import { notifyApp } from '../../../core/actions'; import { createErrorNotification } from '../../../core/copy/appNotification'; diff --git a/public/app/features/explore/state/selectors.test.ts b/public/app/features/explore/state/selectors.test.ts index 7e3bd990fe1..12531a34fec 100644 --- a/public/app/features/explore/state/selectors.test.ts +++ b/public/app/features/explore/state/selectors.test.ts @@ -1,7 +1,7 @@ import { DataSourceApi, DataSourceJsonData } from '@grafana/data'; import { DataQuery } from '@grafana/schema/dist/esm/index'; import { configureStore } from 'app/store/configureStore'; -import { StoreState, ThunkDispatch } from 'app/types'; +import { StoreState, ThunkDispatch } from 'app/types/store'; import { selectExploreDSMaps } from './selectors'; import { createDefaultInitialState } from './testHelpers'; diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts index 9f5ea7f86df..c184d3745c5 100644 --- a/public/app/features/explore/state/selectors.ts +++ b/public/app/features/explore/state/selectors.ts @@ -2,7 +2,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { flatten, uniqBy } from 'lodash'; import { DataSourceRef } from '@grafana/schema'; -import { ExploreItemState, StoreState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; export const selectPanes = (state: Pick<StoreState, 'explore'>) => state.explore.panes; export const selectExploreRoot = (state: Pick<StoreState, 'explore'>) => state.explore; diff --git a/public/app/features/explore/state/time.test.ts b/public/app/features/explore/state/time.test.ts index 1a382acbf17..43e0643ab9b 100644 --- a/public/app/features/explore/state/time.test.ts +++ b/public/app/features/explore/state/time.test.ts @@ -2,7 +2,7 @@ import { reducerTester } from 'test/core/redux/reducerTester'; import { dateTime } from '@grafana/data'; import { configureStore } from 'app/store/configureStore'; -import { ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; import { createDefaultInitialState } from './testHelpers'; import { changeRangeAction, timeReducer, updateTime } from './time'; diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index f975a59d44f..536c51f9f96 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -17,7 +17,8 @@ import { getCopiedTimeRange, getShiftedTimeRange, getZoomedTimeRange } from 'app import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { sortLogsResult } from 'app/features/logs/utils'; import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors'; -import { ExploreItemState, ThunkDispatch, ThunkResult } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { ThunkDispatch, ThunkResult } from 'app/types/store'; import { syncTimesAction } from './main'; import { runLoadMoreLogsQueries, runQueries } from './query'; diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 45b8b39e0e4..4aba4a85f35 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -25,8 +25,8 @@ import { DataQuery, DataSourceJsonData, DataSourceRef, TimeZone } from '@grafana import { getLocalRichHistoryStorage } from 'app/core/history/richHistoryStorageProvider'; import { SortOrder } from 'app/core/utils/richHistoryTypes'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { ExplorePanelData, StoreState } from 'app/types'; -import { ExploreItemState, RichHistoryQuery } from 'app/types/explore'; +import { ExploreItemState, ExplorePanelData, RichHistoryQuery } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import store from '../../../core/store'; import { setLastUsedDatasourceUID } from '../../../core/utils/explore'; diff --git a/public/app/features/explore/useLiveTailControls.ts b/public/app/features/explore/useLiveTailControls.ts index 42837521e05..e78c1272ad7 100644 --- a/public/app/features/explore/useLiveTailControls.ts +++ b/public/app/features/explore/useLiveTailControls.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import * as React from 'react'; import { RefreshPicker } from '@grafana/ui'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { setPausedStateAction, runQueries, clearLogs } from './state/query'; import { changeRefreshInterval } from './state/time'; diff --git a/public/app/features/explore/utils/decorators.test.ts b/public/app/features/explore/utils/decorators.test.ts index 5e2e1517eee..3be590a31f5 100644 --- a/public/app/features/explore/utils/decorators.test.ts +++ b/public/app/features/explore/utils/decorators.test.ts @@ -13,7 +13,7 @@ import { import { CorrelationData } from '@grafana/runtime'; import { DataSourceJsonData, DataQuery } from '@grafana/schema'; import TableModel from 'app/core/TableModel'; -import { ExplorePanelData } from 'app/types'; +import { ExplorePanelData } from 'app/types/explore'; import { decorateWithCorrelations, diff --git a/public/app/features/explore/utils/decorators.ts b/public/app/features/explore/utils/decorators.ts index 08dc24b6a7d..5a877294649 100644 --- a/public/app/features/explore/utils/decorators.ts +++ b/public/app/features/explore/utils/decorators.ts @@ -16,9 +16,9 @@ import { } from '@grafana/data'; import { config, CorrelationData } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; +import { ExplorePanelData } from 'app/types/explore'; import { refreshIntervalToSortOrder } from '../../../core/utils/explore'; -import { ExplorePanelData } from '../../../types'; import { attachCorrelationsToDataFrames } from '../../correlations/utils'; import { dataFrameToLogsModel } from '../../logs/logsModel'; import { sortLogsResult } from '../../logs/utils'; diff --git a/public/app/features/explore/utils/supplementaryQueries.test.ts b/public/app/features/explore/utils/supplementaryQueries.test.ts index 8d274f162b1..ebc1be38292 100644 --- a/public/app/features/explore/utils/supplementaryQueries.test.ts +++ b/public/app/features/explore/utils/supplementaryQueries.test.ts @@ -17,10 +17,10 @@ import { DataQueryResponse, } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; +import { ExplorePanelData } from 'app/types/explore'; import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; import { MockDataQueryRequest, MockQuery } from '../../../../test/mocks/query'; -import { ExplorePanelData } from '../../../types'; import { mockExplorePanelData } from '../mocks/data'; import { getSupplementaryQueryProvider } from './supplementaryQueries'; diff --git a/public/app/features/explore/utils/supplementaryQueries.ts b/public/app/features/explore/utils/supplementaryQueries.ts index ae25b8902b4..a705c8193fb 100644 --- a/public/app/features/explore/utils/supplementaryQueries.ts +++ b/public/app/features/explore/utils/supplementaryQueries.ts @@ -16,7 +16,7 @@ import { SupplementaryQueryType, } from '@grafana/data'; import store from 'app/core/store'; -import { ExplorePanelData, SupplementaryQueries } from 'app/types'; +import { ExplorePanelData, SupplementaryQueries } from 'app/types/explore'; import { makeDataFramesForLogs, queryLogsSample, queryLogsVolume } from '../../logs/logsModel'; diff --git a/public/app/features/explore/utils/supplementaryQueries_legacy.test.ts b/public/app/features/explore/utils/supplementaryQueries_legacy.test.ts index 6d6735a12c5..da43739db79 100644 --- a/public/app/features/explore/utils/supplementaryQueries_legacy.test.ts +++ b/public/app/features/explore/utils/supplementaryQueries_legacy.test.ts @@ -22,10 +22,10 @@ import { } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; +import { ExplorePanelData } from 'app/types/explore'; import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; import { MockDataQueryRequest, MockQuery } from '../../../../test/mocks/query'; -import { ExplorePanelData } from '../../../types'; import { mockExplorePanelData } from '../mocks/data'; import { getSupplementaryQueryProvider } from './supplementaryQueries'; diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx index 75261904c60..b1f07c253bb 100644 --- a/public/app/features/expressions/ExpressionQueryEditor.tsx +++ b/public/app/features/expressions/ExpressionQueryEditor.tsx @@ -1,10 +1,12 @@ +import { css } from '@emotion/css'; import { useCallback, useEffect, useRef } from 'react'; -import { DataSourceApi, QueryEditorProps, SelectableValue } from '@grafana/data'; -import { t } from '@grafana/i18n'; -import { InlineField, Select } from '@grafana/ui'; +import { DataSourceApi, GrafanaTheme2, QueryEditorProps } from '@grafana/data'; +import { t, Trans } from '@grafana/i18n'; +import { Button, IconButton, InlineField, PopoverContent, useStyles2 } from '@grafana/ui'; import { ClassicConditions } from './components/ClassicConditions'; +import { ExpressionTypeDropdown } from './components/ExpressionTypeDropdown'; import { Math } from './components/Math'; import { Reduce } from './components/Reduce'; import { Resample } from './components/Resample'; @@ -20,6 +22,24 @@ const labelWidth = 15; type NonClassicExpressionType = Exclude<ExpressionQueryType, ExpressionQueryType.classic>; type ExpressionTypeConfigStorage = Partial<Record<NonClassicExpressionType, string>>; +// Help text for each expression type - can be expanded with more detailed content +const getExpressionHelpText = (type: ExpressionQueryType): PopoverContent | string => { + const description = expressionTypes.find(({ value }) => value === type)?.description; + + switch (type) { + case ExpressionQueryType.sql: + return ( + <Trans i18nKey="expressions.expression-query-editor.helper-text-sql"> + Run MySQL-dialect SQL against the tables returned from your data sources. Data source queries (ie "A", "B") + are available as tables and referenced by query-name. Fields are available as columns, as returned from the + data source. + </Trans> + ); + default: + return description ?? ''; + } +}; + function useExpressionsCache() { const expressionCache = useRef<ExpressionTypeConfigStorage>({}); @@ -62,14 +82,16 @@ export function ExpressionQueryEditor(props: Props) { const { query, queries, onRunQuery, onChange, app } = props; const { getCachedExpression, setCachedExpression } = useExpressionsCache(); + const styles = useStyles2(getStyles); + useEffect(() => { setCachedExpression(query.type, query.expression); }, [query.expression, query.type, setCachedExpression]); const onSelectExpressionType = useCallback( - (item: SelectableValue<ExpressionQueryType>) => { - const cachedExpression = getCachedExpression(item.value!); - const defaults = getDefaults({ ...query, type: item.value! }); + (value: ExpressionQueryType) => { + const cachedExpression = getCachedExpression(value!); + const defaults = getDefaults({ ...query, type: value! }); onChange({ ...defaults, expression: cachedExpression ?? defaults.expression }); }, @@ -100,17 +122,35 @@ export function ExpressionQueryEditor(props: Props) { } }; - const selected = expressionTypes.find((o) => o.value === query.type); + const helperText = getExpressionHelpText(query.type); return ( <div> - <InlineField - label={t('expressions.expression-query-editor.label-operation', 'Operation')} - labelWidth={labelWidth} - > - <Select options={expressionTypes} value={selected} onChange={onSelectExpressionType} width={25} /> - </InlineField> + <div className={styles.operationRow}> + <InlineField + label={t('expressions.expression-query-editor.label-operation', 'Operation')} + labelWidth={labelWidth} + > + <ExpressionTypeDropdown handleOnSelect={onSelectExpressionType}> + <Button fill="outline" icon="angle-down" iconPlacement="right" variant="secondary"> + {expressionTypes.find(({ value }) => value === query.type)?.label} + </Button> + </ExpressionTypeDropdown> + </InlineField> + {helperText && <IconButton className={styles.infoIcon} name="info-circle" tooltip={helperText} />} + </div> {renderExpressionType()} </div> ); } + +const getStyles = (theme: GrafanaTheme2) => ({ + operationRow: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + infoIcon: css({ + marginBottom: theme.spacing(0.5), // Align with the select field + }), +}); diff --git a/public/app/features/expressions/components/ExpressionTypeDropdown.tsx b/public/app/features/expressions/components/ExpressionTypeDropdown.tsx new file mode 100644 index 00000000000..2fc78ba53d2 --- /dev/null +++ b/public/app/features/expressions/components/ExpressionTypeDropdown.tsx @@ -0,0 +1,100 @@ +import { css } from '@emotion/css'; +import { ReactElement, useCallback, useMemo, memo } from 'react'; + +import { FeatureState, GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { Dropdown, FeatureBadge, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui'; +import { ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; + +const EXPRESSION_ICON_MAP = { + [ExpressionQueryType.math]: 'calculator-alt', + [ExpressionQueryType.reduce]: 'compress-arrows', + [ExpressionQueryType.resample]: 'sync', + [ExpressionQueryType.classic]: 'cog', + [ExpressionQueryType.threshold]: 'sliders-v-alt', + [ExpressionQueryType.sql]: 'database', +} as const satisfies Record<ExpressionQueryType, string>; + +interface ExpressionTypeDropdownProps { + children: ReactElement; + handleOnSelect: (value: ExpressionQueryType) => void; +} + +interface ExpressionMenuItemProps { + item: SelectableValue<ExpressionQueryType>; + onSelect: (value: ExpressionQueryType) => void; +} + +const ExpressionMenuItem = memo<ExpressionMenuItemProps>(({ item, onSelect }) => { + const { value, label, description } = item; + const styles = useStyles2(getStyles); + + const handleClick = useCallback(() => onSelect(value!), [value, onSelect]); + + return ( + <Menu.Item + component={() => ( + <div className={styles.expressionTypeItem} role="menuitem"> + <div className={styles.expressionTypeItemContent} data-testid={`expression-type-${value}`}> + <Icon className={styles.icon} name={EXPRESSION_ICON_MAP[value!]} aria-hidden="true" /> + {label} + {value === ExpressionQueryType.sql && <FeatureBadge featureState={FeatureState.new} />} + </div> + <Tooltip placement="right" content={description!}> + <Icon className={styles.infoIcon} name="info-circle" /> + </Tooltip> + </div> + )} + key={value} + label="" + onClick={handleClick} + /> + ); +}); + +ExpressionMenuItem.displayName = 'ExpressionMenuItem'; + +export const ExpressionTypeDropdown = memo<ExpressionTypeDropdownProps>(({ handleOnSelect, children }) => { + const menuItems = useMemo( + () => expressionTypes.map((item) => <ExpressionMenuItem key={item.value} item={item} onSelect={handleOnSelect} />), + [handleOnSelect] + ); + + const menuOverlay = useMemo(() => <Menu role="menu">{menuItems}</Menu>, [menuItems]); + + return ( + <Dropdown placement="bottom-start" overlay={menuOverlay}> + {children} + </Dropdown> + ); +}); + +ExpressionTypeDropdown.displayName = 'ExpressionTypeDropdown'; + +const getStyles = (theme: GrafanaTheme2) => { + return { + expressionTypeItem: css({ + width: '100%', + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + + expressionTypeItemContent: css({ + flexGrow: 1, + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + + icon: css({ + color: theme.colors.text.secondary, + flexShrink: 0, + }), + + infoIcon: css({ + opacity: 0.7, + color: theme.colors.text.secondary, + flexShrink: 0, + }), + }; +}; diff --git a/public/app/features/expressions/components/SqlExpr.tsx b/public/app/features/expressions/components/SqlExpr.tsx index 09c43551de4..1f61a558471 100644 --- a/public/app/features/expressions/components/SqlExpr.tsx +++ b/public/app/features/expressions/components/SqlExpr.tsx @@ -30,12 +30,10 @@ interface Props { export const SqlExpr = ({ onChange, refIds, query, alerting = false }: Props) => { const vars = useMemo(() => refIds.map((v) => v.value!), [refIds]); - const initialQuery = `-- Run MySQL-dialect SQL against the tables returned from your data sources. --- Data source queries (ie "${vars[0]}") are available as tables and referenced by query-name --- Fields are available as columns, as returned from the data source. -SELECT * -FROM ${vars[0]} -LIMIT 10`; + const initialQuery = `SELECT * + FROM ${vars[0]} + LIMIT 10`; + const styles = useStyles2(getStyles); const containerRef = useRef<HTMLDivElement>(null); const [dimensions, setDimensions] = useState({ height: 0 }); diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index e79c97f97f5..27b22a3102d 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,6 +1,7 @@ import { updateNavIndex } from 'app/core/actions'; import { backendSrv } from 'app/core/services/backend_srv'; -import { FolderDTO, ThunkResult } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; +import { ThunkResult } from 'app/types/store'; import { buildNavModel } from './navModel'; import { loadFolder } from './reducers'; diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index 7db065815b2..49a5167652f 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -3,7 +3,8 @@ import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { getNavSubTitle } from 'app/core/utils/navBarItem-translations'; -import { AccessControlAction, FolderDTO, FolderParent } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { FolderDTO, FolderParent } from 'app/types/folders'; export const FOLDER_ID = 'manage-folder'; diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index 2665316f39e..33f9fe0e4e9 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -1,4 +1,4 @@ -import { FolderDTO, FolderState } from 'app/types'; +import { FolderDTO, FolderState } from 'app/types/folders'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index f544fde391a..849f57e7631 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { endpoints } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { FolderDTO, FolderState } from 'app/types'; +import { FolderState, FolderDTO } from 'app/types/folders'; export const initialState: FolderState = { id: 0, diff --git a/public/app/features/invites/InviteeRow.tsx b/public/app/features/invites/InviteeRow.tsx index dd819e7ddbb..d5fd484e083 100644 --- a/public/app/features/invites/InviteeRow.tsx +++ b/public/app/features/invites/InviteeRow.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { t } from '@grafana/i18n'; import { Button, ClipboardButton } from '@grafana/ui'; -import { Invitee } from 'app/types'; +import { Invitee } from 'app/types/user'; import { revokeInvite } from './state/actions'; diff --git a/public/app/features/invites/InviteesTable.tsx b/public/app/features/invites/InviteesTable.tsx index 2bab93ecfdb..04fc9544562 100644 --- a/public/app/features/invites/InviteesTable.tsx +++ b/public/app/features/invites/InviteesTable.tsx @@ -1,7 +1,7 @@ import { PureComponent } from 'react'; import { Trans } from '@grafana/i18n'; -import { Invitee } from 'app/types'; +import { Invitee } from 'app/types/user'; import InviteeRow from './InviteeRow'; diff --git a/public/app/features/invites/state/actions.ts b/public/app/features/invites/state/actions.ts index d4af5d085b1..72a40840bd6 100644 --- a/public/app/features/invites/state/actions.ts +++ b/public/app/features/invites/state/actions.ts @@ -1,7 +1,9 @@ import { getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { FormModel } from 'app/features/org/UserInviteForm'; -import { AccessControlAction, createAsyncThunk, Invitee } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { createAsyncThunk } from 'app/types/store'; +import { Invitee } from 'app/types/user'; export const fetchInvitees = createAsyncThunk('users/fetchInvitees', async () => { if (!contextSrv.hasPermission(AccessControlAction.OrgUsersAdd)) { diff --git a/public/app/features/invites/state/reducers.ts b/public/app/features/invites/state/reducers.ts index 35ac54eeca0..00a5701cba8 100644 --- a/public/app/features/invites/state/reducers.ts +++ b/public/app/features/invites/state/reducers.ts @@ -1,6 +1,6 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; -import { Invitee } from 'app/types'; +import { Invitee } from 'app/types/user'; import { fetchInvitees, revokeInvite } from './actions'; diff --git a/public/app/features/invites/state/selectors.ts b/public/app/features/invites/state/selectors.ts index 5722ecb263f..4f5af894ee9 100644 --- a/public/app/features/invites/state/selectors.ts +++ b/public/app/features/invites/state/selectors.ts @@ -1,6 +1,6 @@ import { EntityState, createSelector } from '@reduxjs/toolkit'; -import { Invitee } from 'app/types'; +import { Invitee } from 'app/types/user'; import { selectors } from './reducers'; diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx index 9c6ca73c898..d86f7a55517 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx @@ -5,7 +5,7 @@ import { useDebounce } from 'react-use'; import { GrafanaTheme2, PanelPluginMeta, SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { useStyles2, VerticalGroup, FilterInput } from '@grafana/ui'; -import { FolderInfo } from 'app/types'; +import { FolderInfo } from 'app/types/folders'; import { FolderFilter } from '../../../../core/components/FolderFilter/FolderFilter'; import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter'; diff --git a/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx b/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx index 4d3820e6609..d9dafffe82d 100644 --- a/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx +++ b/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx @@ -7,7 +7,7 @@ import { Button, VerticalGroup } from '@grafana/ui'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { changeToLibraryPanel } from 'app/features/panel/state/actions'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter'; import { LibraryElementDTO } from '../../types'; diff --git a/public/app/features/logs/components/log-context/LogRowContextModal.test.tsx b/public/app/features/logs/components/log-context/LogRowContextModal.test.tsx index 29cf6c6c5e3..f6446e804e1 100644 --- a/public/app/features/logs/components/log-context/LogRowContextModal.test.tsx +++ b/public/app/features/logs/components/log-context/LogRowContextModal.test.tsx @@ -57,8 +57,8 @@ const dfAfter = createDataFrame({ let getRowContext = jest.fn(); const dispatchMock = jest.fn(); -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => dispatchMock, })); diff --git a/public/app/features/logs/components/log-context/LogRowContextModal.tsx b/public/app/features/logs/components/log-context/LogRowContextModal.tsx index 5956c892c40..c7aaa58a3af 100644 --- a/public/app/features/logs/components/log-context/LogRowContextModal.tsx +++ b/public/app/features/logs/components/log-context/LogRowContextModal.tsx @@ -24,7 +24,7 @@ import { Button, Modal, useTheme2 } from '@grafana/ui'; import store from 'app/core/store'; import { SETTINGS_KEYS } from 'app/features/explore/Logs/utils/logs'; import { splitOpen } from 'app/features/explore/state/main'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { dataFrameToLogsModel } from '../../logsModel'; import { sortLogRows } from '../../utils'; diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index cf5b0903fa8..c6c310ec88c 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -154,6 +154,7 @@ export const InfiniteScroll = ({ displayedFields={displayedFields} index={index} log={logs[index]} + logs={logs} onClick={onClick} showTime={showTime} style={style} diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx index 1a099f950e9..e31ba38cabf 100644 --- a/public/app/features/logs/components/panel/LogLine.test.tsx +++ b/public/app/features/logs/components/panel/LogLine.test.tsx @@ -8,13 +8,14 @@ import { createLogLine } from '../mocks/logRow'; import { getGridTemplateColumns, getStyles, LogLine, Props } from './LogLine'; import { LogListFontSize } from './LogList'; -import { LogListContextProvider } from './LogListContext'; +import { LogListContextProvider, LogListContext } from './LogListContext'; import { LogListSearchContext } from './LogListSearchContext'; -import { defaultProps } from './__mocks__/LogListContext'; +import { defaultProps, defaultValue } from './__mocks__/LogListContext'; import { LogListModel } from './processing'; import { LogLineVirtualization } from './virtualization'; jest.mock('./LogListContext'); +jest.mock('../LogDetails'); const theme = createTheme(); const virtualization = new LogLineVirtualization(theme, 'default'); @@ -44,6 +45,7 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => { displayedFields: [], index: 0, log, + logs: [log], onClick: jest.fn(), showTime: true, style: {}, @@ -424,6 +426,40 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => { expect(screen.getByText('un')).toBeInTheDocument(); }); }); + + describe('Inline details', () => { + test('Details are not rendered if details mode is not inline', () => { + render( + <LogListContext.Provider + value={{ + ...defaultValue, + showDetails: [log], + detailsMode: 'sidebar', + detailsDisplayed: jest.fn().mockReturnValue(true), + }} + > + <LogLine {...defaultProps} /> + </LogListContext.Provider> + ); + expect(screen.queryByPlaceholderText('Search field names and values')).not.toBeInTheDocument(); + }); + + test('Details are rendered if details mode is inline', () => { + render( + <LogListContext.Provider + value={{ + ...defaultValue, + showDetails: [log], + detailsMode: 'inline', + detailsDisplayed: jest.fn().mockReturnValue(true), + }} + > + <LogLine {...defaultProps} /> + </LogListContext.Provider> + ); + expect(screen.getByPlaceholderText('Search field names and values')).toBeInTheDocument(); + }); + }); }); describe('getGridTemplateColumns', () => { diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx index ec3062ee3b5..7bc7edd9cd6 100644 --- a/public/app/features/logs/components/panel/LogLine.tsx +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -10,6 +10,7 @@ import { Button, Icon, Tooltip } from '@grafana/ui'; import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; import { LogMessageAnsi } from '../LogMessageAnsi'; +import { InlineLogLineDetails } from './LogLineDetails'; import { LogLineMenu } from './LogLineMenu'; import { useLogIsPermalinked, useLogIsPinned, useLogListContext } from './LogListContext'; import { useLogListSearchContext } from './LogListSearchContext'; @@ -26,6 +27,7 @@ export interface Props { displayedFields: string[]; index: number; log: LogListModel; + logs: LogListModel[]; showTime: boolean; style: CSSProperties; styles: LogLineStyles; @@ -40,6 +42,7 @@ export const LogLine = ({ displayedFields, index, log, + logs, style, styles, onClick, @@ -50,12 +53,13 @@ export const LogLine = ({ wrapLogMessage, }: Props) => { return ( - <div style={style}> + <div style={wrapLogMessage ? style : { ...style, width: 'max-content', minWidth: '100%' }}> <LogLineComponent displayedFields={displayedFields} height={style.height} index={index} log={log} + logs={logs} styles={styles} onClick={onClick} onOverflow={onOverflow} @@ -78,6 +82,7 @@ const LogLineComponent = memo( height, index, log, + logs, styles, onClick, onOverflow, @@ -88,6 +93,7 @@ const LogLineComponent = memo( }: LogLineComponentProps) => { const { detailsDisplayed, + detailsMode, dedupStrategy, enableLogDetails, fontSize, @@ -235,6 +241,7 @@ const LogLineComponent = memo( </Button> </div> )} + {detailsMode === 'inline' && detailsShown && <InlineLogLineDetails logs={logs} />} </> ); } diff --git a/public/app/features/logs/components/panel/LogLineDetails.tsx b/public/app/features/logs/components/panel/LogLineDetails.tsx index 212da50add7..dbd2990ea6b 100644 --- a/public/app/features/logs/components/panel/LogLineDetails.tsx +++ b/public/app/features/logs/components/panel/LogLineDetails.tsx @@ -17,9 +17,11 @@ export interface Props { onResize(): void; } +export type LogLineDetailsMode = 'inline' | 'sidebar'; + export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize }: Props) => { - const { detailsWidth, logOptionsStorageKey, setDetailsWidth, showDetails } = useLogListContext(); - const styles = useStyles2(getStyles); + const { detailsWidth, setDetailsWidth, showDetails } = useLogListContext(); + const styles = useStyles2(getStyles, 'sidebar'); const dragStyles = useStyles2(getDragStyles); const containerRef = useRef<HTMLDivElement | null>(null); @@ -54,20 +56,51 @@ export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize > <div className={styles.container} ref={containerRef}> <div className={styles.scrollContainer}> - <LogLineDetailsComponent log={showDetails[0]} logOptionsStorageKey={logOptionsStorageKey} logs={logs} /> + <LogLineDetailsComponent log={showDetails[0]} logs={logs} /> </div> </div> </Resizable> ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +export interface InlineLogLineDetailsProps { + logs: LogListModel[]; +} + +export const InlineLogLineDetails = ({ logs }: InlineLogLineDetailsProps) => { + const { showDetails } = useLogListContext(); + const styles = useStyles2(getStyles, 'inline'); + + if (!showDetails.length) { + return null; + } + + return ( + <div className={`${styles.inlineWrapper} log-line-inline-details`}> + <div className={styles.container}> + <div className={styles.scrollContainer}> + <LogLineDetailsComponent log={showDetails[0]} logs={logs} /> + </div> + </div> + </div> + ); +}; + +export const LOG_LINE_DETAILS_HEIGHT = 35; + +const getStyles = (theme: GrafanaTheme2, mode: LogLineDetailsMode) => ({ + inlineWrapper: css({ + gridColumn: '1 / -1', + height: `${LOG_LINE_DETAILS_HEIGHT}vh`, + paddingBottom: theme.spacing(0.5), + marginRight: 1, + }), container: css({ overflow: 'auto', height: '100%', boxShadow: theme.shadows.z1, border: `1px solid ${theme.colors.border.medium}`, - borderRight: 'none', + borderRight: mode === 'sidebar' ? 'none' : undefined, }), scrollContainer: css({ overflow: 'auto', diff --git a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx index fefd7004879..424266d7b09 100644 --- a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx +++ b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx @@ -13,17 +13,17 @@ import { createLogLineLinks } from '../logParser'; import { LogLineDetailsDisplayedFields } from './LogLineDetailsDisplayedFields'; import { LabelWithLinks, LogLineDetailsFields, LogLineDetailsLabelFields } from './LogLineDetailsFields'; import { LogLineDetailsHeader } from './LogLineDetailsHeader'; +import { LogLineDetailsLog } from './LogLineDetailsLog'; import { useLogListContext } from './LogListContext'; import { LogListModel } from './processing'; interface LogLineDetailsComponentProps { log: LogListModel; - logOptionsStorageKey?: string; logs: LogListModel[]; } -export const LogLineDetailsComponent = ({ log, logOptionsStorageKey, logs }: LogLineDetailsComponentProps) => { - const { displayedFields, setDisplayedFields } = useLogListContext(); +export const LogLineDetailsComponent = ({ log, logs }: LogLineDetailsComponentProps) => { + const { displayedFields, logOptionsStorageKey, setDisplayedFields } = useLogListContext(); const [search, setSearch] = useState(''); const inputRef = useRef(''); const styles = useStyles2(getStyles); @@ -103,7 +103,7 @@ export const LogLineDetailsComponent = ({ log, logOptionsStorageKey, logs }: Log isOpen={logLineOpen} onToggle={(isOpen: boolean) => handleToggle('logLineOpen', isOpen)} > - <div className={styles.logLineWrapper}>{log.raw}</div> + <LogLineDetailsLog log={log} /> </ControlledCollapse> {displayedFields.length > 0 && setDisplayedFields && ( <ControlledCollapse @@ -190,8 +190,4 @@ const getStyles = (theme: GrafanaTheme2) => ({ componentWrapper: css({ padding: theme.spacing(0, 1, 1, 1), }), - logLineWrapper: css({ - maxHeight: '50vh', - overflow: 'auto', - }), }); diff --git a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx index 2c76d814cb6..140a7f86fca 100644 --- a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx +++ b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx @@ -1,13 +1,14 @@ import { css } from '@emotion/css'; import { useCallback, useMemo, MouseEvent, useRef, ChangeEvent } from 'react'; -import { colorManipulator, GrafanaTheme2, LogRowModel } from '@grafana/data'; +import { colorManipulator, GrafanaTheme2, LogRowModel, store } from '@grafana/data'; import { t } from '@grafana/i18n'; import { IconButton, Input, useStyles2 } from '@grafana/ui'; import { copyText, handleOpenLogsContextClick } from '../../utils'; import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { LogLineDetailsMode } from './LogLineDetails'; import { useLogIsPinned, useLogListContext } from './LogListContext'; import { LogListModel } from './processing'; @@ -20,18 +21,22 @@ interface Props { export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => { const { closeDetails, + detailsMode, displayedFields, getRowContextQuery, + logOptionsStorageKey, logSupportsContext, + setDetailsMode, onClickHideField, onClickShowField, onOpenContext, onPermalinkClick, onPinLine, onUnpinLine, + wrapLogMessage, } = useLogListContext(); const pinned = useLogIsPinned(log); - const styles = useStyles2(getStyles); + const styles = useStyles2(getStyles, detailsMode, wrapLogMessage); const containerRef = useRef<HTMLDivElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null); @@ -66,6 +71,15 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => { const showLogLineToggle = onClickHideField && onClickShowField && displayedFields.length > 0; const logLineDisplayed = displayedFields.includes(LOG_LINE_BODY_FIELD_NAME); + const toggleDetailsMode = useCallback(() => { + const newMode = detailsMode === 'inline' ? 'sidebar' : 'inline'; + if (logOptionsStorageKey) { + store.set(`${logOptionsStorageKey}.detailsMode`, newMode); + } + + setDetailsMode(newMode); + }, [detailsMode, logOptionsStorageKey, setDetailsMode]); + const toggleLogLine = useCallback(() => { if (logLineDisplayed) { onClickHideField?.(LOG_LINE_BODY_FIELD_NAME); @@ -121,65 +135,76 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => { variant={logLineDisplayed ? 'primary' : undefined} /> )} - <IconButton - tooltip={t('logs.log-line-details.copy-to-clipboard', 'Copy to clipboard')} - tooltipPlacement="top" - size="md" - name="copy" - onClick={copyLogLine} - tabIndex={0} - /> - {onPermalinkClick && log.rowId !== undefined && log.uid && ( + <div className={styles.icons}> <IconButton - tooltip={t('logs.log-line-details.copy-shortlink', 'Copy shortlink')} + tooltip={t('logs.log-line-details.copy-to-clipboard', 'Copy to clipboard')} tooltipPlacement="top" size="md" - name="share-alt" - onClick={copyLinkToLogLine} + name="copy" + onClick={copyLogLine} tabIndex={0} /> - )} - {pinned && onUnpinLine && ( + {onPermalinkClick && log.rowId !== undefined && log.uid && ( + <IconButton + tooltip={t('logs.log-line-details.copy-shortlink', 'Copy shortlink')} + tooltipPlacement="top" + size="md" + name="share-alt" + onClick={copyLinkToLogLine} + tabIndex={0} + /> + )} + {pinned && onUnpinLine && ( + <IconButton + size="md" + name="gf-pin" + onClick={togglePinning} + tooltip={t('logs.log-line-details.unpin-line', 'Unpin log')} + tooltipPlacement="top" + tabIndex={0} + variant="primary" + /> + )} + {!pinned && onPinLine && ( + <IconButton + size="md" + name="gf-pin" + onClick={togglePinning} + tooltip={t('logs.log-line-details.pin-line', 'Pin log')} + tooltipPlacement="top" + tabIndex={0} + /> + )} + {shouldlogSupportsContext && ( + <IconButton + size="md" + name="gf-show-context" + onClick={showContext} + tooltip={t('logs.log-line-details.show-context', 'Show context')} + tooltipPlacement="top" + tabIndex={0} + /> + )} <IconButton - size="md" - name="gf-pin" - onClick={togglePinning} - tooltip={t('logs.log-line-details.unpin-line', 'Unpin log')} - tooltipPlacement="top" - tabIndex={0} - variant="primary" + name={detailsMode === 'inline' ? 'columns' : 'gf-layout-simple'} + tooltip={ + detailsMode === 'inline' + ? t('logs.log-line-details.sidebar-mode', 'Anchor to the right') + : t('logs.log-line-details.inline-mode', 'Display inline') + } + onClick={toggleDetailsMode} /> - )} - {!pinned && onPinLine && ( <IconButton - size="md" - name="gf-pin" - onClick={togglePinning} - tooltip={t('logs.log-line-details.pin-line', 'Pin log')} - tooltipPlacement="top" - tabIndex={0} + name="times" + aria-label={t('logs.log-line-details.close', 'Close log details')} + onClick={closeDetails} /> - )} - {shouldlogSupportsContext && ( - <IconButton - size="md" - name="gf-show-context" - onClick={showContext} - tooltip={t('logs.log-line-details.show-context', 'Show context')} - tooltipPlacement="top" - tabIndex={0} - /> - )} - <IconButton - name="times" - aria-label={t('logs.log-line-details.close', 'Close log details')} - onClick={closeDetails} - /> + </div> </div> ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = (theme: GrafanaTheme2, mode: LogLineDetailsMode, wrapLogMessage: boolean) => ({ container: css({ overflow: 'auto', height: '100%', @@ -192,7 +217,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ alignItems: 'center', background: theme.colors.background.canvas, display: 'flex', - flexDirection: 'row', + flexDirection: !wrapLogMessage && mode === 'inline' ? 'row-reverse' : 'row', gap: theme.spacing(0.75), zIndex: theme.zIndex.navbarFixed, height: theme.spacing(5.5), @@ -201,6 +226,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ position: 'sticky', top: 0, }), + icons: css({ + display: 'flex', + gap: theme.spacing(0.75), + }), copyLogButton: css({ padding: 0, height: theme.spacing(4), diff --git a/public/app/features/logs/components/panel/LogLineDetailsLog.tsx b/public/app/features/logs/components/panel/LogLineDetailsLog.tsx new file mode 100644 index 00000000000..259d7ca27c5 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineDetailsLog.tsx @@ -0,0 +1,44 @@ +import { css } from '@emotion/css'; +import { memo, useMemo } from 'react'; + +import { useStyles2 } from '@grafana/ui'; + +import { getStyles } from './LogLine'; +import { useLogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +interface Props { + log: LogListModel; +} + +export const LogLineDetailsLog = memo(({ log: originalLog }: Props) => { + const { syntaxHighlighting } = useLogListContext(); + const logStyles = useStyles2(getStyles); + const log = useMemo(() => { + const log = originalLog.clone(); + return log; + }, [originalLog]); + + return ( + <div className={styles.logLineWrapper}> + {!syntaxHighlighting ? ( + <div className="field no-highlighting">{log.body}</div> + ) : ( + <div className={logStyles.logLine}> + <div className={logStyles.wrappedLogLine}> + <div className="field log-syntax-highlight" dangerouslySetInnerHTML={{ __html: log.highlightedBody }} /> + </div> + </div> + )} + </div> + ); +}); + +LogLineDetailsLog.displayName = 'LogLineDetailsLog'; + +const styles = { + logLineWrapper: css({ + maxHeight: '50vh', + overflow: 'auto', + }), +}; diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx index 17eda97f4f6..fde0ea89dec 100644 --- a/public/app/features/logs/components/panel/LogList.test.tsx +++ b/public/app/features/logs/components/panel/LogList.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; +import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder, store } from '@grafana/data'; import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils'; import { createLogRow } from '../mocks/logRow'; @@ -75,6 +75,12 @@ describe('LogList', () => { }); test('Supports showing log details', async () => { + jest.spyOn(store, 'get').mockImplementation((option: string) => { + if (option === 'storage-key.detailsMode') { + return 'sidebar'; + } + return undefined; + }); const onClickFilterLabel = jest.fn(); const onClickFilterOutLabel = jest.fn(); const onClickShowField = jest.fn(); @@ -86,6 +92,50 @@ describe('LogList', () => { onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} onClickShowField={onClickShowField} + logOptionsStorageKey="storage-key" + /> + ); + + await userEvent.click(screen.getByText('log message 1')); + await screen.findByText('Fields'); + + expect(screen.getByText('name_of_the_label')).toBeInTheDocument(); + expect(screen.getByText('value of the label')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Filter for value in query A')); + expect(onClickFilterLabel).toHaveBeenCalledTimes(1); + + await userEvent.click(screen.getByLabelText('Filter out value in query A')); + expect(onClickFilterOutLabel).toHaveBeenCalledTimes(1); + + await userEvent.click(screen.getByLabelText('Show this field instead of the message')); + expect(onClickShowField).toHaveBeenCalledTimes(1); + + await userEvent.click(screen.getByLabelText('Close log details')); + + expect(screen.queryByText('Fields')).not.toBeInTheDocument(); + expect(screen.queryByText('Close log details')).not.toBeInTheDocument(); + }); + + test('Supports showing inline log details', async () => { + jest.spyOn(store, 'get').mockImplementation((option: string) => { + if (option === 'storage-key.detailsMode') { + return 'inline'; + } + return undefined; + }); + const onClickFilterLabel = jest.fn(); + const onClickFilterOutLabel = jest.fn(); + const onClickShowField = jest.fn(); + + render( + <LogList + {...defaultProps} + enableLogDetails={true} + onClickFilterLabel={onClickFilterLabel} + onClickFilterOutLabel={onClickFilterOutLabel} + onClickShowField={onClickShowField} + logOptionsStorageKey="storage-key" /> ); diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index 489741fd59b..a5115f5e447 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -26,7 +26,7 @@ import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { InfiniteScroll } from './InfiniteScroll'; import { getGridTemplateColumns } from './LogLine'; -import { LogLineDetails } from './LogLineDetails'; +import { LogLineDetails, LogLineDetailsMode } from './LogLineDetails'; import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu'; import { LogListContextProvider, LogListState, useLogListContext } from './LogListContext'; import { LogListControls } from './LogListControls'; @@ -41,6 +41,7 @@ export interface Props { app: CoreApp; containerElement: HTMLDivElement; dedupStrategy: LogsDedupStrategy; + detailsMode?: LogLineDetailsMode; displayedFields: string[]; enableLogDetails: boolean; eventBus?: EventBus; @@ -105,11 +106,12 @@ export const LogList = ({ app, displayedFields, containerElement, + logOptionsStorageKey, + detailsMode = logOptionsStorageKey ? (store.get(`${logOptionsStorageKey}.detailsMode`) ?? 'sidebar') : 'sidebar', dedupStrategy, enableLogDetails, eventBus, filterLevels, - logOptionsStorageKey, fontSize = logOptionsStorageKey ? (store.get(`${logOptionsStorageKey}.fontSize`) ?? 'default') : 'default', getFieldLinks, getRowContextQuery, @@ -152,6 +154,7 @@ export const LogList = ({ app={app} containerElement={containerElement} dedupStrategy={dedupStrategy} + detailsMode={detailsMode} displayedFields={displayedFields} enableLogDetails={enableLogDetails} filterLevels={filterLevels} @@ -222,6 +225,7 @@ const LogListComponent = ({ app, displayedFields, dedupStrategy, + detailsMode, filterLevels, fontSize, forceEscape, @@ -456,9 +460,11 @@ const LogListComponent = ({ height={listHeight} itemCount={itemCount} itemSize={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, { + detailsMode, hasLogsWithErrors, hasSampledLogs, showDuplicates: dedupStrategy !== LogsDedupStrategy.none, + showDetails, showTime, wrap: wrapLogMessage, })} @@ -476,7 +482,7 @@ const LogListComponent = ({ )} </InfiniteScroll> </div> - {showDetails.length > 0 && ( + {detailsMode === 'sidebar' && showDetails.length > 0 && ( <LogLineDetails containerElement={containerElement} focusLogLine={focusLogLine} @@ -501,6 +507,9 @@ function getStyles( '& .unwrapped-log-line': { display: 'grid', gridTemplateColumns: getGridTemplateColumns(columns, displayedFields), + '& .field': { + overflow: 'hidden', + }, }, }), logListContainer: css({ diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index 1ca6100a1d1..d27fe27d56c 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -26,6 +26,7 @@ import { PopoverContent } from '@grafana/ui'; import { DownloadFormat, checkLogsError, checkLogsSampled, downloadLogs as download } from '../../utils'; +import { LogLineDetailsMode } from './LogLineDetails'; import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu'; import { LogListFontSize } from './LogList'; import { LogListModel } from './processing'; @@ -34,6 +35,7 @@ import { LOG_LIST_MIN_WIDTH } from './virtualization'; export interface LogListContextData extends Omit<Props, 'containerElement' | 'logs' | 'logsMeta' | 'showControls'> { closeDetails: () => void; detailsDisplayed: (log: LogListModel) => boolean; + detailsMode: LogLineDetailsMode; detailsWidth: number; downloadLogs: (format: DownloadFormat) => void; enableLogDetails: boolean; @@ -43,6 +45,7 @@ export interface LogListContextData extends Omit<Props, 'containerElement' | 'lo hasUnescapedContent?: boolean; logLineMenuCustomItems?: LogLineMenuCustomItem[]; setDedupStrategy: (dedupStrategy: LogsDedupStrategy) => void; + setDetailsMode: (mode: LogLineDetailsMode) => void; setDetailsWidth: (width: number) => void; setFilterLevels: (filterLevels: LogLevel[]) => void; setFontSize: (size: LogListFontSize) => void; @@ -64,6 +67,7 @@ export const LogListContext = createContext<LogListContextData>({ closeDetails: () => {}, dedupStrategy: LogsDedupStrategy.none, detailsDisplayed: () => false, + detailsMode: 'sidebar', detailsWidth: 0, displayedFields: [], downloadLogs: () => {}, @@ -72,6 +76,7 @@ export const LogListContext = createContext<LogListContextData>({ fontSize: 'default', hasUnescapedContent: false, setDedupStrategy: () => {}, + setDetailsMode: () => {}, setDetailsWidth: () => {}, setFilterLevels: () => {}, setFontSize: () => {}, @@ -132,6 +137,7 @@ export interface Props { children?: ReactNode; // Only ControlledLogRows can send an undefined containerElement. See LogList.tsx containerElement?: HTMLDivElement; + detailsMode?: LogLineDetailsMode; dedupStrategy: LogsDedupStrategy; displayedFields: string[]; enableLogDetails: boolean; @@ -176,6 +182,7 @@ export const LogListContextProvider = ({ children, containerElement, enableLogDetails, + detailsMode: detailsModeProp, dedupStrategy, displayedFields, filterLevels, @@ -230,6 +237,7 @@ export const LogListContextProvider = ({ }); const [showDetails, setShowDetails] = useState<LogListModel[]>([]); const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey)); + const [detailsMode, setDetailsMode] = useState<LogLineDetailsMode>(detailsModeProp ?? 'sidebar'); useEffect(() => { // Props are updated in the context only of the panel is being externally controlled. @@ -480,6 +488,7 @@ export const LogListContextProvider = ({ closeDetails, detailsDisplayed, dedupStrategy: logListState.dedupStrategy, + detailsMode, detailsWidth, displayedFields, downloadLogs, @@ -511,6 +520,7 @@ export const LogListContextProvider = ({ pinnedLogs: logListState.pinnedLogs, prettifyJSON: logListState.prettifyJSON, setDedupStrategy, + setDetailsMode, setDetailsWidth, setDisplayedFields, setFilterLevels, @@ -563,7 +573,9 @@ function getDetailsWidth( const defaultWidth = containerElement.clientWidth * 0.4; const detailsWidth = currentWidth || - (logOptionsStorageKey ? parseInt(store.get(`${logOptionsStorageKey}.detailsWidth`), 10) : defaultWidth); + (logOptionsStorageKey + ? parseInt(store.get(`${logOptionsStorageKey}.detailsWidth`) ?? defaultWidth, 10) + : defaultWidth); const maxWidth = containerElement.clientWidth - LOG_LIST_MIN_WIDTH; diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx index f0cb9be3ce6..1a991f79ecf 100644 --- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx +++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx @@ -3,6 +3,7 @@ import { createContext, useContext } from 'react'; import { CoreApp, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; import { checkLogsError, checkLogsSampled } from 'app/features/logs/utils'; +import { LogLineDetailsMode } from '../LogLineDetails'; import { LogListContextData, Props } from '../LogListContext'; import { LogListModel } from '../processing'; @@ -37,6 +38,10 @@ export const LogListContext = createContext<LogListContextData>({ syntaxHighlighting: true, toggleDetails: () => {}, wrapLogMessage: false, + detailsMode: 'sidebar', + setDetailsMode: function (mode: LogLineDetailsMode): void { + throw new Error('Function not implemented.'); + }, }); export const useLogListContextData = (key: keyof LogListContextData) => { @@ -59,6 +64,8 @@ export const useLogIsPermalinked = (log: LogListModel) => { }; export const defaultValue: LogListContextData = { + detailsMode: 'sidebar', + setDetailsMode: jest.fn(), setDedupStrategy: jest.fn(), setFilterLevels: jest.fn(), setFontSize: jest.fn(), diff --git a/public/app/features/logs/components/panel/processing.test.ts b/public/app/features/logs/components/panel/processing.test.ts index 906d2f4a7c5..7ef61741624 100644 --- a/public/app/features/logs/components/panel/processing.test.ts +++ b/public/app/features/logs/components/panel/processing.test.ts @@ -140,6 +140,36 @@ describe('preProcessLogs', () => { ); expect(logListModel.getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME, true)).toBe('log message 1'); }); + + test('Prettifies JSON', () => { + const entry = '{"key": "value", "otherKey": "otherValue"}'; + const logListModel = createLogLine( + { entry }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: false, // unwrapped + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).not.toBe(entry); + }); + + test('Uses lossless parsing', () => { + const entry = '{"number": 90071992547409911}'; + const logListModel = createLogLine( + { entry }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: false, // unwrapped + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).toContain('90071992547409911'); + }); }); test('Orders logs', () => { @@ -243,6 +273,35 @@ describe('preProcessLogs', () => { expect(entry).toContain(longLog.body); }); + test('Sets the collapsed state based on the new lines count', () => { + const entry = new Array(virtualization.getTruncationLineCount()).fill('test\n').join(''); + const multilineLog = createLogLine( + { entry, labels: { field: 'value' } }, + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } + ); + + expect(multilineLog.collapsed).toBeUndefined(); + + multilineLog.updateCollapsedState([], container); + + expect(multilineLog.collapsed).toBe(true); + expect(entry).toContain(multilineLog.body); + }); + + test('Correctly counts new lines', () => { + const entry = new Array(virtualization.getTruncationLineCount() - 1).fill('test\n').join(''); + const multilineLog = createLogLine( + { entry, labels: { field: 'value' } }, + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } + ); + + expect(multilineLog.collapsed).toBeUndefined(); + + multilineLog.updateCollapsedState([], container); + + expect(multilineLog.collapsed).toBeUndefined(); + }); + test('Considers the displayed fields to set the collapsed state', () => { // Make container half of the size jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(100); @@ -255,6 +314,22 @@ describe('preProcessLogs', () => { expect(longLog.collapsed).toBeUndefined(); }); + test('Considers new lines in displayed fields to set the collapsed state', () => { + const entry = new Array(virtualization.getTruncationLineCount() - 1).fill('test\n').join(''); + const field = 'test\n'; + const multilineLog = createLogLine( + { entry, labels: { field } }, + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } + ); + + expect(multilineLog.collapsed).toBeUndefined(); + + multilineLog.updateCollapsedState(['field', LOG_LINE_BODY_FIELD_NAME], container); + + expect(multilineLog.collapsed).toBe(true); + expect(entry).toContain(multilineLog.body); + }); + test('Updates the body based on the collapsed state', () => { expect(longLog.collapsed).toBeUndefined(); expect(longLog.body).toBe(entry); diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 60e51ca91d4..d46b09db490 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -1,7 +1,8 @@ import ansicolor from 'ansicolor'; +import { parse, stringify } from 'lossless-json'; import Prism, { Grammar } from 'prismjs'; -import { DataFrame, dateTimeFormat, Labels, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data'; +import { DataFrame, dateTimeFormat, Labels, LogLevel, LogRowModel, LogsSortOrder, textUtil } from '@grafana/data'; import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { checkLogsError, checkLogsSampled, escapeUnescapedString, sortLogRows } from '../../utils'; @@ -97,8 +98,23 @@ export class LogListModel implements LogRowModel { this.raw = raw; } + clone() { + const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this); + // Unless this function is required outside of <LogLineDetailsLog />, we create a wrapped clone, so new lines are not stripped. + clone._wrapLogMessage = true; + clone._body = undefined; + clone._highlightedBody = undefined; + return clone; + } + get body(): string { if (this._body === undefined) { + try { + const parsed = stringify(parse(this.raw), undefined, 2); + if (parsed) { + this.raw = parsed; + } + } catch (error) {} this._body = this.collapsed ? this.raw.substring(0, this._virtualization?.getTruncationLength(null) ?? TRUNCATION_DEFAULT_LENGTH) : this.raw; @@ -124,7 +140,11 @@ export class LogListModel implements LogRowModel { if (this._highlightedBody === undefined) { this._grammar = this._grammar ?? generateLogGrammar(this); const extraGrammar = generateTextMatchGrammar(this.searchWords, this._currentSearch); - this._highlightedBody = Prism.highlight(this.body, { ...extraGrammar, ...this._grammar }, 'lokiql'); + this._highlightedBody = Prism.highlight( + textUtil.sanitize(this.body), + { ...extraGrammar, ...this._grammar }, + 'lokiql' + ); } return this._highlightedBody; } @@ -154,16 +174,27 @@ export class LogListModel implements LogRowModel { } updateCollapsedState(displayedFields: string[], container: HTMLDivElement | null) { - const lineLength = + const line = displayedFields.length > 0 - ? displayedFields.map((field) => this.getDisplayedFieldValue(field, true)).join('').length - : this.entry.length; - const collapsed = - lineLength >= (this._virtualization?.getTruncationLength(container) ?? TRUNCATION_DEFAULT_LENGTH) + ? displayedFields.map((field) => this.getDisplayedFieldValue(field, true)).join('') + : this.body; + + // Length truncation + let collapsed = + line.length >= (this._virtualization?.getTruncationLength(container) ?? TRUNCATION_DEFAULT_LENGTH) ? true : undefined; + + // Newlines truncation + if (!collapsed && this._virtualization) { + const truncationLimit = this._virtualization.getTruncationLineCount(); + collapsed = countNewLines(line, truncationLimit) >= truncationLimit ? true : collapsed; + } + if (this.collapsed === undefined || collapsed === undefined) { this.collapsed = collapsed; + this._body = undefined; + this._highlightedBody = undefined; } return this.collapsed; } @@ -226,3 +257,23 @@ function logLevelToDisplayLevel(level = '') { return level; } } + +function countNewLines(log: string, limit = Infinity) { + let count = 0; + for (let i = 0; i < log.length; ++i) { + // No need to iterate further + if (count > Infinity) { + return count; + } + if (log[i] === '\n') { + count += 1; + } else if (log[i] === '\r') { + count += 1; + // skip LF in CRLF + if (log[i] === '\n') { + i += 1; + } + } + } + return count; +} diff --git a/public/app/features/logs/components/panel/virtualization.test.ts b/public/app/features/logs/components/panel/virtualization.test.ts index 75534d4e170..342262aafd1 100644 --- a/public/app/features/logs/components/panel/virtualization.test.ts +++ b/public/app/features/logs/components/panel/virtualization.test.ts @@ -3,14 +3,17 @@ import { createTheme, LogsSortOrder } from '@grafana/data'; import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; import { createLogLine } from '../mocks/logRow'; +import { LOG_LINE_DETAILS_HEIGHT } from './LogLineDetails'; import { LogListModel, PreProcessOptions } from './processing'; -import { LogLineVirtualization, getLogLineSize, DisplayOptions } from './virtualization'; +import { LogLineVirtualization, getLogLineSize, DisplayOptions, FIELD_GAP_MULTIPLIER } from './virtualization'; describe('Virtualization', () => { let log: LogListModel, container: HTMLDivElement; let virtualization = new LogLineVirtualization(createTheme(), 'default'); + const GAP = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER; + const DETAILS_HEIGHT = window.innerHeight * (LOG_LINE_DETAILS_HEIGHT / 100) + GAP / 2; const PADDING_BOTTOM = 6; const LINE_HEIGHT = virtualization.getLineHeight(); const SINGLE_LINE_HEIGHT = LINE_HEIGHT + PADDING_BOTTOM; @@ -21,8 +24,10 @@ describe('Virtualization', () => { let TWO_LINES_OF_CHARACTERS: number; const defaultOptions: DisplayOptions = { + detailsMode: 'sidebar', wrap: false, showTime: false, + showDetails: [], showDuplicates: false, hasLogsWithErrors: false, hasSampledLogs: false, @@ -50,6 +55,30 @@ describe('Virtualization', () => { expect(size).toBe(SINGLE_LINE_HEIGHT); }); + test('Returns the a single line plus inline details if the display mode is unwrapped', () => { + const size = getLogLineSize( + virtualization, + [log], + container, + [], + { ...defaultOptions, showTime: true, showDetails: [log], detailsMode: 'inline' }, + 0 + ); + expect(size).toBe(SINGLE_LINE_HEIGHT + DETAILS_HEIGHT); + }); + + test('Should not throw when an undefined index is passed', () => { + const size = getLogLineSize( + virtualization, + [log], + container, + [], + { ...defaultOptions, showTime: true, showDetails: [log], detailsMode: 'inline' }, + 1 // Index out of bounds + ); + expect(size).toBe(SINGLE_LINE_HEIGHT); + }); + test('Returns the a single line if the line is not loaded yet', () => { const logs = [log]; const size = getLogLineSize( @@ -78,6 +107,21 @@ describe('Virtualization', () => { expect(size).toBe((virtualization.getTruncationLineCount() + 1) * LINE_HEIGHT); }); + test('Returns the size of a truncated long line with inline details', () => { + // Very small container + log.collapsed = true; + jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(10); + const size = getLogLineSize( + virtualization, + [log], + container, + [], + { ...defaultOptions, wrap: true, showTime: true, showDetails: [log], detailsMode: 'inline' }, + 0 + ); + expect(size).toBe((virtualization.getTruncationLineCount() + 1) * LINE_HEIGHT + DETAILS_HEIGHT); + }); + test.each([true, false])('Measures a log line with controls %s and displayed time %s', (showTime: boolean) => { const size = getLogLineSize(virtualization, [log], container, [], { ...defaultOptions, wrap: true, showTime }, 0); expect(size).toBe(SINGLE_LINE_HEIGHT); @@ -115,6 +159,24 @@ describe('Virtualization', () => { expect(size).toBe(THREE_LINES_HEIGHT); }); + test('Measures a multi-line log line with level, controls, displayed time, and inline details', () => { + log = createLogLine( + { labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') }, + preProcessOptions + ); + + const size = getLogLineSize( + virtualization, + [log], + container, + [], + { ...defaultOptions, wrap: true, showTime: true, showDetails: [log], detailsMode: 'inline' }, + 0 + ); + // Two lines for the log and one extra for level and time + expect(size).toBe(THREE_LINES_HEIGHT + DETAILS_HEIGHT); + }); + test('Measures a multi-line log line with displayed fields', () => { log = createLogLine( { diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts index 2f9b6b6773a..bf84f5fe3ff 100644 --- a/public/app/features/logs/components/panel/virtualization.ts +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -2,6 +2,7 @@ import ansicolor from 'ansicolor'; import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data'; +import { LOG_LINE_DETAILS_HEIGHT, LogLineDetailsMode } from './LogLineDetails'; import { LogListFontSize } from './LogList'; import { LogListModel } from './processing'; @@ -54,7 +55,6 @@ export class LogLineVirtualization { getGridSize = () => this.gridSize; getPaddingBottom = () => this.paddingBottom; - // 2/3 of the viewport height getTruncationLineCount = () => Math.round(window.innerHeight / this.getLineHeight() / 1.5); getTruncationLength = (container: HTMLDivElement | null) => { @@ -232,8 +232,10 @@ export class LogLineVirtualization { } export interface DisplayOptions { + detailsMode: LogLineDetailsMode; hasLogsWithErrors?: boolean; hasSampledLogs?: boolean; + showDetails: LogListModel[]; showDuplicates: boolean; showTime: boolean; wrap: boolean; @@ -244,20 +246,25 @@ export function getLogLineSize( logs: LogListModel[], container: HTMLDivElement | null, displayedFields: string[], - { hasLogsWithErrors, hasSampledLogs, showDuplicates, showTime, wrap }: DisplayOptions, + { detailsMode, hasLogsWithErrors, hasSampledLogs, showDuplicates, showDetails, showTime, wrap }: DisplayOptions, index: number ) { if (!container) { return 0; } + const gap = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER; + const detailsHeight = + detailsMode === 'inline' && logs[index] && showDetails.findIndex((log) => log.uid === logs[index].uid) >= 0 + ? window.innerHeight * (LOG_LINE_DETAILS_HEIGHT / 100) + gap / 2 + : 0; // !logs[index] means the line is not yet loaded by infinite scrolling if (!wrap || !logs[index]) { - return virtualization.getLineHeight() + virtualization.getPaddingBottom(); + return virtualization.getLineHeight() + virtualization.getPaddingBottom() + detailsHeight; } // If a long line is collapsed, we show the line count + an extra line for the expand/collapse control logs[index].updateCollapsedState(displayedFields, container); if (logs[index].collapsed) { - return (virtualization.getTruncationLineCount() + 1) * virtualization.getLineHeight(); + return (virtualization.getTruncationLineCount() + 1) * virtualization.getLineHeight() + detailsHeight; } const storedSize = virtualization.retrieveLogLineSize(logs[index].uid, container); @@ -266,7 +273,6 @@ export function getLogLineSize( } let textToMeasure = ''; - const gap = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER; const iconsGap = virtualization.getGridSize() * 0.5; let optionsWidth = 0; if (showDuplicates) { @@ -296,7 +302,9 @@ export function getLogLineSize( const { height } = virtualization.measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth); // When the log is collapsed, add an extra line for the expand/collapse control - return logs[index].collapsed === false ? height + virtualization.getLineHeight() : height; + return logs[index].collapsed === false + ? height + virtualization.getLineHeight() + detailsHeight + : height + detailsHeight; } export interface LogFieldDimension { @@ -313,14 +321,28 @@ export function hasUnderOrOverflow( if (collapsed !== undefined && calculatedHeight) { calculatedHeight -= virtualization.getLineHeight(); } + const inlineDetails = element.parentElement + ? Array.from(element.parentElement.children).filter((element) => + element.classList.contains('log-line-inline-details') + ) + : undefined; + const detailsHeight = inlineDetails?.length ? inlineDetails[0].clientHeight : 0; + + // Line overflows container + let measuredHeight = element.scrollHeight + detailsHeight; const height = calculatedHeight ?? element.clientHeight; - if (element.scrollHeight > height) { - return collapsed !== undefined ? element.scrollHeight + virtualization.getLineHeight() : element.scrollHeight; + if (measuredHeight > height) { + return collapsed !== undefined ? measuredHeight + virtualization.getLineHeight() : measuredHeight; } + + // Line is smaller than container const child = element.children[1]; - if (child instanceof HTMLDivElement && child.clientHeight < height) { - return collapsed !== undefined ? child.clientHeight + virtualization.getLineHeight() : child.clientHeight; + measuredHeight = child.clientHeight + detailsHeight; + if (child instanceof HTMLDivElement && measuredHeight < height) { + return collapsed !== undefined ? measuredHeight + virtualization.getLineHeight() : measuredHeight; } + + // No overflow or undermeasurement return null; } diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx index cf23c94f73e..4652a4811bd 100644 --- a/public/app/features/manage-dashboards/DashboardImportPage.tsx +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -28,7 +28,7 @@ import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { dispatch } from 'app/store/store'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { cleanUpAction } from '../../core/actions/cleanUp'; import { ImportDashboardOverviewV2 } from '../dashboard-scene/v2schema/ImportDashboardOverviewV2'; diff --git a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx index 314954ca16c..66508d4b07f 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx @@ -6,7 +6,7 @@ import { Trans } from '@grafana/i18n'; import { locationService, reportInteraction } from '@grafana/runtime'; import { Box, Legend, TextLink } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { clearLoadedDashboard, importDashboard } from '../state/actions'; import { DashboardSource, ImportDashboardDTO } from '../state/reducers'; diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx index 79676bc4a19..282227e4f31 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx @@ -27,7 +27,7 @@ import { generatePublicDashboardConfigUrl, generatePublicDashboardUrl, } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PublicDashboardListResponse } from '../../types'; diff --git a/public/app/features/manage-dashboards/components/SnapshotListTableRow.test.tsx b/public/app/features/manage-dashboards/components/SnapshotListTableRow.test.tsx index f6268790dcf..29d062da35d 100644 --- a/public/app/features/manage-dashboards/components/SnapshotListTableRow.test.tsx +++ b/public/app/features/manage-dashboards/components/SnapshotListTableRow.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { contextSrv } from 'app/core/services/context_srv'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { Snapshot } from 'app/features/dashboard/services/SnapshotSrv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { SnapshotListTableRow } from './SnapshotListTableRow'; diff --git a/public/app/features/manage-dashboards/components/SnapshotListTableRow.tsx b/public/app/features/manage-dashboards/components/SnapshotListTableRow.tsx index 5e0c08b7395..6a217ca63ce 100644 --- a/public/app/features/manage-dashboards/components/SnapshotListTableRow.tsx +++ b/public/app/features/manage-dashboards/components/SnapshotListTableRow.tsx @@ -6,7 +6,7 @@ import { Button, LinkButton, useStyles2 } from '@grafana/ui'; import { SkeletonComponent, attachSkeleton } from '@grafana/ui/unstable'; import { contextSrv } from 'app/core/services/context_srv'; import { Snapshot } from 'app/features/dashboard/services/SnapshotSrv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export interface Props { snapshot: Snapshot; diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index fad40fbec18..191ec9c2657 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -10,7 +10,8 @@ import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types'; +import { PermissionLevelString, SearchQueryType } from 'app/types/acl'; +import { ThunkResult } from 'app/types/store'; import { Input, diff --git a/public/app/features/manage-dashboards/utils/validation.test.ts b/public/app/features/manage-dashboards/utils/validation.test.ts index 40d1a8ee2b4..d6fb178bf80 100644 --- a/public/app/features/manage-dashboards/utils/validation.test.ts +++ b/public/app/features/manage-dashboards/utils/validation.test.ts @@ -5,7 +5,7 @@ import { import { AnnoKeyFolder, AnnoKeyFolderTitle } from 'app/features/apiserver/types'; import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { validateUid } from './validation'; diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index c6a3bae02da..339184f1635 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -206,6 +206,8 @@ export type CreateSnapshotRequestDto = { }; export type MigrateDataResponseItemDto = { errorCode?: + | 'ALERT_RULES_QUOTA_REACHED' + | 'ALERT_RULES_GROUP_QUOTA_REACHED' | 'DATASOURCE_NAME_CONFLICT' | 'DATASOURCE_INVALID_URL' | 'DATASOURCE_ALREADY_MANAGED' diff --git a/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx index a47efe48d43..63c194fbf73 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx @@ -13,6 +13,16 @@ interface ResourceDetailsModalProps { function getTMessage(errorCode: MigrateDataResponseItemDto['errorCode']): string { switch (errorCode) { + case 'ALERT_RULES_QUOTA_REACHED': + return t( + 'migrate-to-cloud.resource-details.error-messages.alert-rules-quota-reached', + 'Maximum number of alert rules reached: Delete some alert rules or upgrade your plan and try again.' + ); + case 'ALERT_RULES_GROUP_QUOTA_REACHED': + return t( + 'migrate-to-cloud.resource-details.error-messages.alert-rules-group-quota-reached', + 'Maximum number of alert rule groups reached: Delete some alert rule groups or upgrade your plan and try again.' + ); case 'DATASOURCE_NAME_CONFLICT': return t( 'migrate-to-cloud.resource-details.error-messages.datasource-name-conflict', @@ -106,9 +116,11 @@ export function ResourceDetailsModal(props: ResourceDetailsModalProps) { <> <Text element="p">{msgTitle}</Text> <Text element="p"> - {getTMessage(resource?.errorCode) || - resource?.message || - 'There has been an error while migrating. Please check the cloud migration logs for more information.'} + {getTMessage(resource?.errorCode) || resource?.message || ( + <Trans i18nKey="migrate-to-cloud.resource-details.error-messages.generic-error"> + There has been an error while migrating. Please check the cloud migration logs for more information. + </Trans> + )} </Text> </> ) : ( diff --git a/public/app/features/notifications/StoredNotifications.tsx b/public/app/features/notifications/StoredNotifications.tsx index 5443458036e..b5db2d1327a 100644 --- a/public/app/features/notifications/StoredNotifications.tsx +++ b/public/app/features/notifications/StoredNotifications.tsx @@ -14,7 +14,7 @@ import { selectWarningsAndErrors, selectLastReadTimestamp, } from 'app/core/reducers/appNotification'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; export function StoredNotifications() { const dispatch = useDispatch(); diff --git a/public/app/features/org/OrgDetailsPage.test.tsx b/public/app/features/org/OrgDetailsPage.test.tsx index 0fc7ab9e728..c71bfd1eff8 100644 --- a/public/app/features/org/OrgDetailsPage.test.tsx +++ b/public/app/features/org/OrgDetailsPage.test.tsx @@ -4,9 +4,9 @@ import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { TestProvider } from 'test/helpers/TestProvider'; import { NavModel } from '@grafana/data'; +import { Organization } from 'app/types/organization'; import { backendSrv } from '../../core/services/backend_srv'; -import { Organization } from '../../types'; import { OrgDetailsPage, Props } from './OrgDetailsPage'; import { setOrganizationName } from './state/reducers'; diff --git a/public/app/features/org/OrgDetailsPage.tsx b/public/app/features/org/OrgDetailsPage.tsx index d97faf6636b..bffc18ef3c7 100644 --- a/public/app/features/org/OrgDetailsPage.tsx +++ b/public/app/features/org/OrgDetailsPage.tsx @@ -7,8 +7,9 @@ import { Page } from 'app/core/components/Page/Page'; import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; import { appEvents, contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; -import { AccessControlAction, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { StoreState } from 'app/types/store'; import OrgProfile from './OrgProfile'; import { loadOrganization, updateOrganization } from './state/actions'; diff --git a/public/app/features/org/OrgProfile.tsx b/public/app/features/org/OrgProfile.tsx index 23d56e2d92a..46b5cfa0c9b 100644 --- a/public/app/features/org/OrgProfile.tsx +++ b/public/app/features/org/OrgProfile.tsx @@ -2,7 +2,7 @@ import { Trans, t } from '@grafana/i18n'; import { Input, Field, FieldSet, Button } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export interface Props { orgName: string; diff --git a/public/app/features/org/SelectOrgPage.tsx b/public/app/features/org/SelectOrgPage.tsx index f9d93c63e93..3990613cbf8 100644 --- a/public/app/features/org/SelectOrgPage.tsx +++ b/public/app/features/org/SelectOrgPage.tsx @@ -5,7 +5,8 @@ import { Trans } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Button, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { StoreState, UserOrg } from 'app/types'; +import { StoreState } from 'app/types/store'; +import { UserOrg } from 'app/types/user'; import { getUserOrganizations, setUserOrganization } from './state/actions'; diff --git a/public/app/features/org/UserInviteForm.tsx b/public/app/features/org/UserInviteForm.tsx index b4bc4bf67c2..1a0ad769b76 100644 --- a/public/app/features/org/UserInviteForm.tsx +++ b/public/app/features/org/UserInviteForm.tsx @@ -1,6 +1,6 @@ import { Controller } from 'react-hook-form'; -import { locationUtil, SelectableValue } from '@grafana/data'; +import { locationUtil, OrgRole, SelectableValue } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { @@ -18,7 +18,7 @@ import { Stack, } from '@grafana/ui'; import { getConfig } from 'app/core/config'; -import { OrgRole, useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { Form } from '../../core/components/Form/Form'; import { addInvitee } from '../invites/state/actions'; diff --git a/public/app/features/org/state/actions.test.ts b/public/app/features/org/state/actions.test.ts index dedda1a8fb5..b624430697a 100644 --- a/public/app/features/org/state/actions.test.ts +++ b/public/app/features/org/state/actions.test.ts @@ -1,8 +1,8 @@ import { thunkTester } from 'test/core/thunk/thunkTester'; +import { OrgRole } from '@grafana/data'; import { BackendSrv } from '@grafana/runtime'; import { updateConfigurationSubtitle } from 'app/core/actions'; -import { OrgRole } from 'app/types'; import { updateOrganization, setUserOrganization, getUserOrganizations } from './actions'; diff --git a/public/app/features/org/state/actions.ts b/public/app/features/org/state/actions.ts index af1b46f49b0..1d580c5b3ae 100644 --- a/public/app/features/org/state/actions.ts +++ b/public/app/features/org/state/actions.ts @@ -1,6 +1,7 @@ import { getBackendSrv } from '@grafana/runtime'; import { updateConfigurationSubtitle } from 'app/core/actions'; -import { ThunkResult, UserOrg } from 'app/types'; +import { ThunkResult } from 'app/types/store'; +import { UserOrg } from 'app/types/user'; import { organizationLoaded, userOrganizationsLoaded } from './reducers'; diff --git a/public/app/features/org/state/reducers.test.ts b/public/app/features/org/state/reducers.test.ts index b99e2ce762f..6bcc44a6aac 100644 --- a/public/app/features/org/state/reducers.test.ts +++ b/public/app/features/org/state/reducers.test.ts @@ -1,5 +1,7 @@ +import { OrgRole } from '@grafana/data'; +import { OrganizationState } from 'app/types/organization'; + import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { OrganizationState, OrgRole } from '../../../types'; import { initialState, diff --git a/public/app/features/org/state/reducers.ts b/public/app/features/org/state/reducers.ts index d43850ffb2b..151c8cb40f0 100644 --- a/public/app/features/org/state/reducers.ts +++ b/public/app/features/org/state/reducers.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Organization, OrganizationState, UserOrg } from 'app/types'; +import { OrganizationState, Organization } from 'app/types/organization'; +import { UserOrg } from 'app/types/user'; export const initialState: OrganizationState = { organization: {} as Organization, diff --git a/public/app/features/panel/components/PanelDataErrorView.tsx b/public/app/features/panel/components/PanelDataErrorView.tsx index 339a33d84a2..ba2ddcf7f8d 100644 --- a/public/app/features/panel/components/PanelDataErrorView.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.tsx @@ -20,7 +20,7 @@ import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/Pa import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { findVizPanelByKey, getVizPanelKeyForPanelId } from 'app/features/dashboard-scene/utils/utils'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { changePanelPlugin } from '../state/actions'; diff --git a/public/app/features/panel/components/PanelPluginError.tsx b/public/app/features/panel/components/PanelPluginError.tsx index d4edf2d1027..39ec1eec5d0 100644 --- a/public/app/features/panel/components/PanelPluginError.tsx +++ b/public/app/features/panel/components/PanelPluginError.tsx @@ -5,7 +5,7 @@ import { PureComponent, ReactNode } from 'react'; import { PanelProps, PanelPlugin, PluginType, PanelPluginMeta } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Alert } from '@grafana/ui'; -import { AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; import grafanaIconSvg from 'img/grafana_icon.svg'; interface Props { diff --git a/public/app/features/panel/state/actions.ts b/public/app/features/panel/state/actions.ts index 6bde8fc0480..24d7c2b1a92 100644 --- a/public/app/features/panel/state/actions.ts +++ b/public/app/features/panel/state/actions.ts @@ -4,8 +4,8 @@ import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { LibraryElementDTO } from 'app/features/library-panels/types'; import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError'; import { loadPanelPlugin } from 'app/features/plugins/admin/state/actions'; -import { ThunkResult } from 'app/types'; import { DashboardPanelsChangedEvent, PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events'; +import { ThunkResult } from 'app/types/store'; import { changePanelKey, panelModelAndPluginReady, removePanel } from './reducers'; diff --git a/public/app/features/panel/state/selectors.ts b/public/app/features/panel/state/selectors.ts index 6851c061940..492e73af8e0 100644 --- a/public/app/features/panel/state/selectors.ts +++ b/public/app/features/panel/state/selectors.ts @@ -1,5 +1,5 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { PanelState } from './reducers'; diff --git a/public/app/features/plugins/admin/components/ConnectionsTab.test.tsx b/public/app/features/plugins/admin/components/ConnectionsTab.test.tsx index 340e455e14b..c7e4bb6cb02 100644 --- a/public/app/features/plugins/admin/components/ConnectionsTab.test.tsx +++ b/public/app/features/plugins/admin/components/ConnectionsTab.test.tsx @@ -5,7 +5,7 @@ import { DataSourceSettings } from '@grafana/data'; import { config } from '@grafana/runtime'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; import { getMockDataSources } from 'app/features/datasources/mocks/dataSourcesMocks'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { datasourcePlugin } from '../mocks/catalogPlugin.mock'; diff --git a/public/app/features/plugins/admin/components/ConnectionsTab.tsx b/public/app/features/plugins/admin/components/ConnectionsTab.tsx index a7b82b35774..70a2a26f900 100644 --- a/public/app/features/plugins/admin/components/ConnectionsTab.tsx +++ b/public/app/features/plugins/admin/components/ConnectionsTab.tsx @@ -8,7 +8,8 @@ import { ViewProps } from 'app/features/datasources/components/DataSourcesList'; import { DataSourcesListCard } from 'app/features/datasources/components/DataSourcesListCard'; import { useLoadDataSources } from 'app/features/datasources/state/hooks'; import { getDataSources } from 'app/features/datasources/state/selectors'; -import { AccessControlAction, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useSelector } from 'app/types/store'; import { CatalogPlugin } from '../types'; diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx index 6e0ab172f3a..f15eefad873 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx @@ -5,7 +5,7 @@ import { Trans } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { Button } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { updatePluginSettings } from '../../api'; import { usePluginConfig } from '../../hooks/usePluginConfig'; diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx index a1b80773d8a..b36b4a936b9 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx @@ -4,7 +4,7 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { PluginSignatureStatus } from '@grafana/data'; import { config } from '@grafana/runtime'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CatalogPlugin } from '../../types'; diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx index 13b490c3797..eba64519719 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx @@ -7,7 +7,7 @@ import { config } from '@grafana/runtime'; import { Button } from '@grafana/ui'; import { ROUTES } from 'app/features/connections/constants'; import { addDataSource } from 'app/features/datasources/state/actions'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { isDataSourceEditor } from '../../permissions'; import { CatalogPlugin } from '../../types'; diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index b4774e0dadd..5e5a61b0365 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -9,7 +9,7 @@ import { Button, ConfirmModal, LinkButton, Stack } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { removePluginFromNavTree } from 'app/core/reducers/navBarTree'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { getExternalManageLink, isDisabledAngularPlugin } from '../../helpers'; import { diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx index 1ccdc6e5311..99a198c1455 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { config, featureEnabled } from '@grafana/runtime'; import { LinkButton, useStyles2, Alert, TextLink, Stack } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { getExternalManageLink } from '../../helpers'; import { useIsRemotePluginsAvailable } from '../../state/hooks'; diff --git a/public/app/features/plugins/admin/components/PluginDashboards.tsx b/public/app/features/plugins/admin/components/PluginDashboards.tsx index fafd1b19874..8d5caba5861 100644 --- a/public/app/features/plugins/admin/components/PluginDashboards.tsx +++ b/public/app/features/plugins/admin/components/PluginDashboards.tsx @@ -6,7 +6,7 @@ import { Trans } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { appEvents } from 'app/core/core'; import DashboardsTable from 'app/features/datasources/components/DashboardsTable'; -import { PluginDashboard } from 'app/types'; +import { PluginDashboard } from 'app/types/plugins'; interface Props { plugin: PluginMeta; diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index ef6f56c0559..d5a69de88ec 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -8,7 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Alert, Box, Stack, TabContent, TextLink, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; import { Loader } from '../components/Loader'; import { PluginDetailsBody } from '../components/PluginDetailsBody'; diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index b13716b8ca2..171644b2546 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -3,7 +3,7 @@ import uFuzzy from '@leeoniya/ufuzzy'; import { PluginSignatureStatus, dateTimeParse, PluginError, PluginType, PluginErrorCode } from '@grafana/data'; import { config, featureEnabled } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CatalogPlugin, diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 26c700658ff..f0eebd3bd0a 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -5,7 +5,7 @@ import { GrafanaPlugin, NavModelItem, PluginIncludeType, PluginType } from '@gra import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { usePluginConfig } from '../hooks/usePluginConfig'; import { CatalogPlugin, PluginTabIds, PluginTabLabels } from '../types'; diff --git a/public/app/features/plugins/admin/pages/Browse.tsx b/public/app/features/plugins/admin/pages/Browse.tsx index 4cc44785f5b..a8b0ee559e7 100644 --- a/public/app/features/plugins/admin/pages/Browse.tsx +++ b/public/app/features/plugins/admin/pages/Browse.tsx @@ -10,7 +10,7 @@ import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { AdvisorRedirectNotice } from 'app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { HorizontalGroup } from '../components/HorizontalGroup'; import { PluginList } from '../components/PluginList'; diff --git a/public/app/features/plugins/admin/permissions.ts b/public/app/features/plugins/admin/permissions.ts index e0b7d752881..4ea5eccdd34 100644 --- a/public/app/features/plugins/admin/permissions.ts +++ b/public/app/features/plugins/admin/permissions.ts @@ -1,6 +1,6 @@ import { config } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export function isGrafanaAdmin(): boolean { return config.bootData.user.isGrafanaAdmin; diff --git a/public/app/features/plugins/admin/routes.tsx b/public/app/features/plugins/admin/routes.tsx index ab7fb2d13ce..6b85ddc14e7 100644 --- a/public/app/features/plugins/admin/routes.tsx +++ b/public/app/features/plugins/admin/routes.tsx @@ -1,7 +1,7 @@ import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { contextSrv } from 'app/core/core'; import { RouteDescriptor } from 'app/core/navigation/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PluginAdminRoutes } from './types'; diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index 6f2c5400cf0..599a588d160 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -5,7 +5,7 @@ import { PanelPlugin, PluginError } from '@grafana/data'; import { config, getBackendSrv, isFetchError } from '@grafana/runtime'; import { Settings } from 'app/core/config'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; -import { StoreState, ThunkResult } from 'app/types'; +import { StoreState, ThunkResult } from 'app/types/store'; import { invalidatePluginInCache } from '../../loader/cache'; import { diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts index 457f2f1d73e..2185ec99465 100644 --- a/public/app/features/plugins/admin/state/hooks.ts +++ b/public/app/features/plugins/admin/state/hooks.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react'; import { PluginError, PluginType } from '@grafana/data'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { sortPlugins, Sorters, isPluginUpdatable } from '../helpers'; import { CatalogPlugin, PluginStatus } from '../types'; diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 791a6d4eb28..eaedf1766ee 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -9,7 +9,8 @@ import { WithAccessControlMetadata, } from '@grafana/data'; import { IconName } from '@grafana/ui'; -import { StoreState, PluginsState } from 'app/types'; +import { PluginsState } from 'app/types/plugins'; +import { StoreState } from 'app/types/store'; export type PluginTypeCode = 'app' | 'panel' | 'datasource'; diff --git a/public/app/features/plugins/components/AppRootPage.test.tsx b/public/app/features/plugins/components/AppRootPage.test.tsx index bef9ed62306..1b054508724 100644 --- a/public/app/features/plugins/components/AppRootPage.test.tsx +++ b/public/app/features/plugins/components/AppRootPage.test.tsx @@ -15,15 +15,15 @@ import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsR import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry'; +import { importAppPlugin } from '../pluginLoader'; import { getPluginSettings } from '../pluginSettings'; -import { importAppPlugin } from '../plugin_loader'; import AppRootPage from './AppRootPage'; jest.mock('../pluginSettings', () => ({ getPluginSettings: jest.fn(), })); -jest.mock('../plugin_loader', () => ({ +jest.mock('../pluginLoader', () => ({ importAppPlugin: jest.fn(), })); diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index c6cdc47e810..7210f0ff471 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -32,8 +32,8 @@ import { useExposedComponentsRegistry, useAddedFunctionsRegistry, } from '../extensions/ExtensionRegistriesContext'; +import { importAppPlugin } from '../pluginLoader'; import { getPluginSettings } from '../pluginSettings'; -import { importAppPlugin } from '../plugin_loader'; import { buildPluginSectionNav, pluginsLogger } from '../utils'; import { PluginErrorBoundary } from './PluginErrorBoundary'; diff --git a/public/app/features/plugins/datasource_srv.test.ts b/public/app/features/plugins/datasource_srv.test.ts index 94e714ce35c..9734aaa9b02 100644 --- a/public/app/features/plugins/datasource_srv.test.ts +++ b/public/app/features/plugins/datasource_srv.test.ts @@ -61,7 +61,7 @@ class TestRuntimeDataSource extends RuntimeDataSource { } } -jest.mock('./plugin_loader', () => ({ +jest.mock('./pluginLoader', () => ({ importDataSourcePlugin: (meta: DataSourcePluginMeta) => { return Promise.resolve(new DataSourcePlugin(TestDataSource as any)); }, diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index f434681943f..a7d081244c6 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -29,7 +29,7 @@ import { } from 'app/features/expressions/ExpressionDatasource'; import { ExpressionDatasourceUID } from 'app/features/expressions/types'; -import { importDataSourcePlugin } from './plugin_loader'; +import { importDataSourcePlugin } from './pluginLoader'; export class DatasourceSrv implements DataSourceService { private datasources: Record<string, DataSourceApi> = {}; // UID diff --git a/public/app/features/plugins/importPanelPlugin.ts b/public/app/features/plugins/importPanelPlugin.ts index a8f770acdd5..3afee6fa4c3 100644 --- a/public/app/features/plugins/importPanelPlugin.ts +++ b/public/app/features/plugins/importPanelPlugin.ts @@ -3,7 +3,7 @@ import config from 'app/core/config'; import { getPanelPluginLoadError } from '../panel/components/PanelPluginError'; -import { importPluginModule } from './plugin_loader'; +import { importPluginModule } from './pluginLoader'; const promiseCache: Record<string, Promise<PanelPlugin>> = {}; const panelPluginCache: Record<string, PanelPlugin> = {}; diff --git a/public/app/features/plugins/loader/utils.ts b/public/app/features/plugins/loader/utils.ts index 2407852f6ee..b8986e8ade3 100644 --- a/public/app/features/plugins/loader/utils.ts +++ b/public/app/features/plugins/loader/utils.ts @@ -1,6 +1,6 @@ import { config } from '@grafana/runtime'; -import { sandboxPluginDependencies } from '../sandbox/plugin_dependencies'; +import { sandboxPluginDependencies } from '../sandbox/pluginDependencies'; import { SHARED_DEPENDENCY_PREFIX } from './constants'; import { SystemJS } from './systemjs'; diff --git a/public/app/features/plugins/plugin_loader.test.ts b/public/app/features/plugins/pluginLoader.test.ts similarity index 98% rename from public/app/features/plugins/plugin_loader.test.ts rename to public/app/features/plugins/pluginLoader.test.ts index 8fa6b6901e2..e715f1b9d35 100644 --- a/public/app/features/plugins/plugin_loader.test.ts +++ b/public/app/features/plugins/pluginLoader.test.ts @@ -5,9 +5,9 @@ import * as i18nModule from '@grafana/i18n/internal'; import { server } from './loader/pluginLoader.mock'; import { SystemJS } from './loader/systemjs'; import { SystemJSWithLoaderHooks } from './loader/types'; -import { addTranslationsToI18n } from './plugin_loader'; +import { addTranslationsToI18n } from './pluginLoader'; -describe('plugin_loader', () => { +describe('pluginLoader', () => { describe('addTranslationsToI18n', () => { const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype; const originalFetch = systemJSPrototype.fetch; diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/pluginLoader.ts similarity index 98% rename from public/app/features/plugins/plugin_loader.ts rename to public/app/features/plugins/pluginLoader.ts index a5e642ffe06..15f00ac400f 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/pluginLoader.ts @@ -30,8 +30,8 @@ import { sharedDependenciesMap } from './loader/sharedDependencies'; import { decorateSystemJSFetch, decorateSystemJSResolve, decorateSystemJsOnload } from './loader/systemjsHooks'; import { SystemJSWithLoaderHooks } from './loader/types'; import { buildImportMap, resolveModulePath } from './loader/utils'; -import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader'; -import { shouldLoadPluginInFrontendSandbox } from './sandbox/sandbox_plugin_loader_registry'; +import { importPluginModuleInSandbox } from './sandbox/sandboxPluginLoader'; +import { shouldLoadPluginInFrontendSandbox } from './sandbox/sandboxPluginLoaderRegistry'; import { pluginsLogger } from './utils'; const imports = buildImportMap(sharedDependenciesMap); diff --git a/public/app/features/plugins/pluginPreloader.test.ts b/public/app/features/plugins/pluginPreloader.test.ts index 2aab1cd3dc3..4890184a638 100644 --- a/public/app/features/plugins/pluginPreloader.test.ts +++ b/public/app/features/plugins/pluginPreloader.test.ts @@ -11,8 +11,8 @@ import { import type { AppPluginConfig } from '@grafana/runtime'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; +import { importAppPlugin } from './pluginLoader'; import { clearPreloadedPluginsCache, preloadPlugins } from './pluginPreloader'; -import { importAppPlugin } from './plugin_loader'; jest.mock('app/core/services/context_srv', () => ({ contextSrv: { @@ -26,7 +26,7 @@ jest.mock('app/features/plugins/pluginSettings', () => ({ getPluginSettings: jest.fn(), })); -jest.mock('./plugin_loader', () => ({ +jest.mock('./pluginLoader', () => ({ importAppPlugin: jest.fn(), })); diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index b8284584a5c..f1bec151de1 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -7,7 +7,7 @@ import type { AppPluginConfig } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; -import { importAppPlugin } from './plugin_loader'; +import { importAppPlugin } from './pluginLoader'; export type PluginPreloadResult = { pluginId: string; diff --git a/public/app/features/plugins/sandbox/code_loader.ts b/public/app/features/plugins/sandbox/codeLoader.ts similarity index 100% rename from public/app/features/plugins/sandbox/code_loader.ts rename to public/app/features/plugins/sandbox/codeLoader.ts diff --git a/public/app/features/plugins/sandbox/distortion_map.ts b/public/app/features/plugins/sandbox/distortions.ts similarity index 99% rename from public/app/features/plugins/sandbox/distortion_map.ts rename to public/app/features/plugins/sandbox/distortions.ts index 902f87afc09..b258f36978e 100644 --- a/public/app/features/plugins/sandbox/distortion_map.ts +++ b/public/app/features/plugins/sandbox/distortions.ts @@ -4,9 +4,9 @@ import { cloneDeep, isFunction } from 'lodash'; import { Monaco } from '@grafana/ui'; -import { loadScriptIntoSandbox } from './code_loader'; +import { loadScriptIntoSandbox } from './codeLoader'; import { forbiddenElements } from './constants'; -import { recursivePatchObjectAsLiveTarget } from './document_sandbox'; +import { recursivePatchObjectAsLiveTarget } from './documentSandbox'; import { SandboxEnvironment, SandboxPluginMeta } from './types'; import { logWarning, unboxRegexesFromMembraneProxy } from './utils'; diff --git a/public/app/features/plugins/sandbox/document_sandbox.ts b/public/app/features/plugins/sandbox/documentSandbox.ts similarity index 100% rename from public/app/features/plugins/sandbox/document_sandbox.ts rename to public/app/features/plugins/sandbox/documentSandbox.ts diff --git a/public/app/features/plugins/sandbox/plugin_dependencies.ts b/public/app/features/plugins/sandbox/pluginDependencies.ts similarity index 100% rename from public/app/features/plugins/sandbox/plugin_dependencies.ts rename to public/app/features/plugins/sandbox/pluginDependencies.ts diff --git a/public/app/features/plugins/sandbox/sandbox_components.tsx b/public/app/features/plugins/sandbox/sandboxComponents.tsx similarity index 100% rename from public/app/features/plugins/sandbox/sandbox_components.tsx rename to public/app/features/plugins/sandbox/sandboxComponents.tsx diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandboxPluginLoader.ts similarity index 97% rename from public/app/features/plugins/sandbox/sandbox_plugin_loader.ts rename to public/app/features/plugins/sandbox/sandboxPluginLoader.ts index 791e791ec3a..5b62216ff63 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandboxPluginLoader.ts @@ -5,8 +5,8 @@ import { BootData } from '@grafana/data'; import { config } from '@grafana/runtime'; import { defaultTrustedTypesPolicy } from 'app/core/trustedTypePolicies'; -import { getPluginCode, getPluginLoadData, patchSandboxEnvironmentPrototype } from './code_loader'; -import { getGeneralSandboxDistortionMap, distortLiveApis } from './distortion_map'; +import { getPluginCode, getPluginLoadData, patchSandboxEnvironmentPrototype } from './codeLoader'; +import { getGeneralSandboxDistortionMap, distortLiveApis } from './distortions'; import { getSafeSandboxDomElement, isDomElement, @@ -14,9 +14,9 @@ import { markDomElementStyleAsALiveTarget, patchObjectAsLiveTarget, patchWebAPIs, -} from './document_sandbox'; -import { sandboxPluginDependencies } from './plugin_dependencies'; -import { sandboxPluginComponents } from './sandbox_components'; +} from './documentSandbox'; +import { sandboxPluginDependencies } from './pluginDependencies'; +import { sandboxPluginComponents } from './sandboxComponents'; import { CompartmentDependencyModule, PluginFactoryFunction, SandboxEnvironment, SandboxPluginMeta } from './types'; import { logError, logInfo } from './utils'; diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts similarity index 99% rename from public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts rename to public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts index e7164a5a860..a36480579d7 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts +++ b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts @@ -11,7 +11,7 @@ import { setSandboxEnabledCheck, isPluginFrontendSandboxEnabled, isPluginFrontendSandboxEligible, -} from './sandbox_plugin_loader_registry'; +} from './sandboxPluginLoaderRegistry'; jest.mock('@grafana/runtime', () => ({ config: { diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.ts similarity index 100% rename from public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts rename to public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.ts diff --git a/public/app/features/plugins/tests/plugin_loader.test.ts b/public/app/features/plugins/tests/plugin_loader.test.ts index 9f6c2e3739b..860d509bdcd 100644 --- a/public/app/features/plugins/tests/plugin_loader.test.ts +++ b/public/app/features/plugins/tests/plugin_loader.test.ts @@ -11,7 +11,7 @@ import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/d // Loaded after the `unmock` above import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from '../extensions/registry/setup'; import { SystemJS } from '../loader/systemjs'; -import { importAppPlugin } from '../plugin_loader'; +import { importAppPlugin } from '../pluginLoader'; jest.mock('../extensions/registry/setup'); diff --git a/public/app/features/plugins/utils.ts b/public/app/features/plugins/utils.ts index 845785ce876..9961ee71016 100644 --- a/public/app/features/plugins/utils.ts +++ b/public/app/features/plugins/utils.ts @@ -2,8 +2,8 @@ import { GrafanaPlugin, NavModel, NavModelItem, PanelPluginMeta, PluginType } fr import { createMonitoringLogger } from '@grafana/runtime'; import { importPanelPluginFromMeta } from './importPanelPlugin'; +import { importAppPlugin, importDataSourcePlugin } from './pluginLoader'; import { getPluginSettings } from './pluginSettings'; -import { importAppPlugin, importDataSourcePlugin } from './plugin_loader'; export async function loadPlugin(pluginId: string): Promise<GrafanaPlugin> { const info = await getPluginSettings(pluginId); diff --git a/public/app/features/profile/ChangePasswordForm.tsx b/public/app/features/profile/ChangePasswordForm.tsx index fd7d7e58974..6f4ae223651 100644 --- a/public/app/features/profile/ChangePasswordForm.tsx +++ b/public/app/features/profile/ChangePasswordForm.tsx @@ -9,7 +9,7 @@ import { strongPasswordValidationRegister, } from 'app/core/components/ValidationLabels/ValidationLabels'; import config from 'app/core/config'; -import { UserDTO } from 'app/types'; +import { UserDTO } from 'app/types/user'; import { PasswordField } from '../../core/components/PasswordField/PasswordField'; diff --git a/public/app/features/profile/ChangePasswordPage.tsx b/public/app/features/profile/ChangePasswordPage.tsx index 3b7452cbfc5..0be35c8c0fc 100644 --- a/public/app/features/profile/ChangePasswordPage.tsx +++ b/public/app/features/profile/ChangePasswordPage.tsx @@ -2,7 +2,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { useMount } from 'react-use'; import { Page } from 'app/core/components/Page/Page'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { ChangePasswordForm } from './ChangePasswordForm'; import { changePassword, loadUser } from './state/actions'; diff --git a/public/app/features/profile/UserOrganizations.tsx b/public/app/features/profile/UserOrganizations.tsx index 43638455d41..8f808173438 100644 --- a/public/app/features/profile/UserOrganizations.tsx +++ b/public/app/features/profile/UserOrganizations.tsx @@ -3,7 +3,7 @@ import { PureComponent } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { Button, LoadingPlaceholder, ScrollContainer } from '@grafana/ui'; -import { UserDTO, UserOrg } from 'app/types'; +import { UserDTO, UserOrg } from 'app/types/user'; export interface Props { user: UserDTO | null; diff --git a/public/app/features/profile/UserProfileEditForm.tsx b/public/app/features/profile/UserProfileEditForm.tsx index 3b99618268d..25b8c9a0b2b 100644 --- a/public/app/features/profile/UserProfileEditForm.tsx +++ b/public/app/features/profile/UserProfileEditForm.tsx @@ -3,7 +3,7 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Field, FieldSet, Icon, Input, Tooltip } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; import config from 'app/core/config'; -import { UserDTO } from 'app/types'; +import { UserDTO } from 'app/types/user'; import { ProfileUpdateFields } from './types'; diff --git a/public/app/features/profile/UserProfileEditPage.tsx b/public/app/features/profile/UserProfileEditPage.tsx index c5f84818cb2..1dfa6045e81 100644 --- a/public/app/features/profile/UserProfileEditPage.tsx +++ b/public/app/features/profile/UserProfileEditPage.tsx @@ -6,7 +6,7 @@ import { usePluginComponents } from '@grafana/runtime'; import { Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import UserOrganizations from './UserOrganizations'; import UserProfileEditForm from './UserProfileEditForm'; diff --git a/public/app/features/profile/UserSessions.tsx b/public/app/features/profile/UserSessions.tsx index 0eac0199b33..375db224fd0 100644 --- a/public/app/features/profile/UserSessions.tsx +++ b/public/app/features/profile/UserSessions.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Icon, LoadingPlaceholder, ScrollContainer } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { formatDate } from 'app/core/internationalization/dates'; -import { UserSession } from 'app/types'; +import { UserSession } from 'app/types/user'; interface Props { sessions: UserSession[]; diff --git a/public/app/features/profile/UserTeams.tsx b/public/app/features/profile/UserTeams.tsx index 65d14160d7b..74b9c1e9aff 100644 --- a/public/app/features/profile/UserTeams.tsx +++ b/public/app/features/profile/UserTeams.tsx @@ -2,7 +2,7 @@ import { PureComponent } from 'react'; import { Trans, t } from '@grafana/i18n'; import { LoadingPlaceholder, ScrollContainer } from '@grafana/ui'; -import { Team } from 'app/types'; +import { Team } from 'app/types/teams'; export interface Props { teams: Team[]; diff --git a/public/app/features/profile/api.ts b/public/app/features/profile/api.ts index bb7e30d6b34..a5ab3247c9e 100644 --- a/public/app/features/profile/api.ts +++ b/public/app/features/profile/api.ts @@ -1,6 +1,6 @@ import { getBackendSrv } from '@grafana/runtime'; - -import { Team, UserDTO, UserOrg, UserSession } from '../../types'; +import { Team } from 'app/types/teams'; +import { UserDTO, UserOrg, UserSession } from 'app/types/user'; import { ChangePasswordFields, ProfileUpdateFields } from './types'; diff --git a/public/app/features/profile/state/actions.ts b/public/app/features/profile/state/actions.ts index e1e5f25600c..c98d764be78 100644 --- a/public/app/features/profile/state/actions.ts +++ b/public/app/features/profile/state/actions.ts @@ -1,6 +1,7 @@ import { config } from '@grafana/runtime'; +import { ThunkResult } from 'app/types/store'; +import { UserOrg } from 'app/types/user'; -import { ThunkResult, UserOrg } from '../../../types'; import { api } from '../api'; import { ChangePasswordFields, ProfileUpdateFields } from '../types'; diff --git a/public/app/features/profile/state/reducers.test.ts b/public/app/features/profile/state/reducers.test.ts index 67ff64f9e56..226b8e94d88 100644 --- a/public/app/features/profile/state/reducers.test.ts +++ b/public/app/features/profile/state/reducers.test.ts @@ -1,5 +1,6 @@ +import { OrgRole } from '@grafana/data'; + import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { OrgRole } from '../../../types'; import { getMockTeam } from '../../teams/mocks/teamMocks'; import { diff --git a/public/app/features/profile/state/reducers.ts b/public/app/features/profile/state/reducers.ts index f4de9c34f0b..6d1bb9096e6 100644 --- a/public/app/features/profile/state/reducers.ts +++ b/public/app/features/profile/state/reducers.ts @@ -5,7 +5,9 @@ import { dateTimeFormatTimeAgo, setWeekStart, TimeZone } from '@grafana/data'; import { getWeekStart, WeekStart } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { Team, ThunkResult, UserDTO, UserOrg, UserSession } from 'app/types'; +import { ThunkResult } from 'app/types/store'; +import { Team } from 'app/types/teams'; +import { UserDTO, UserOrg, UserSession } from 'app/types/user'; export interface UserState { orgId: number; diff --git a/public/app/features/provisioning/utils/routes.ts b/public/app/features/provisioning/utils/routes.ts index c0e828a66a6..891ecd090d9 100644 --- a/public/app/features/provisioning/utils/routes.ts +++ b/public/app/features/provisioning/utils/routes.ts @@ -1,6 +1,6 @@ import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { RouteDescriptor } from 'app/core/navigation/types'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { checkRequiredFeatures } from '../GettingStarted/features'; import { PROVISIONING_URL, CONNECT_URL, GETTING_STARTED_URL } from '../constants'; diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index b5275a367e9..8737449101f 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -26,7 +26,7 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; -import { QueryGroupOptions } from 'app/types'; +import { QueryGroupOptions } from 'app/types/query'; import { PanelQueryRunner } from '../state/PanelQueryRunner'; import { updateQueries } from '../state/updateQueries'; diff --git a/public/app/features/query/components/QueryGroupOptions.tsx b/public/app/features/query/components/QueryGroupOptions.tsx index b28c4ce7128..2404236e483 100644 --- a/public/app/features/query/components/QueryGroupOptions.tsx +++ b/public/app/features/query/components/QueryGroupOptions.tsx @@ -5,7 +5,7 @@ import { rangeUtil, PanelData, DataSourceApi, GrafanaTheme2 } from '@grafana/dat import { Trans, t } from '@grafana/i18n'; import { Input, InlineSwitch, useStyles2, InlineLabel } from '@grafana/ui'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; -import { QueryGroupOptions } from 'app/types'; +import { QueryGroupOptions } from 'app/types/query'; interface Props { options: QueryGroupOptions; diff --git a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts index 3c07c21bf5c..4c0f49aacd9 100644 --- a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts +++ b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts @@ -12,7 +12,7 @@ import { import { Annotation } from 'app/features/alerting/unified/utils/constants'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import * as store from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromAlertingRuleState, PromRulesResponse, PromRuleType } from 'app/types/unified-alerting-dto'; import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput'; diff --git a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts index 18c001c983c..8610d84f8e1 100644 --- a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts +++ b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts @@ -11,7 +11,7 @@ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/d import { prometheusRuleType } from 'app/features/alerting/unified/utils/rules'; import { promAlertStateToAlertState } from 'app/features/dashboard-scene/scene/AlertStatesDataLayer'; import { dispatch } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RuleNamespace } from 'app/types/unified-alerting'; import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/scopes/tests/utils/render.tsx b/public/app/features/scopes/tests/utils/render.tsx index e4c6993928b..ed126b5b2d7 100644 --- a/public/app/features/scopes/tests/utils/render.tsx +++ b/public/app/features/scopes/tests/utils/render.tsx @@ -8,7 +8,7 @@ import { sceneGraph } from '@grafana/scenes'; import { defaultDashboard } from '@grafana/schema'; import { AppChrome } from 'app/core/components/AppChrome/AppChrome'; import { transformSaveModelToScene } from 'app/features/dashboard-scene/serialization/transformSaveModelToScene'; -import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types'; +import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types/dashboard'; import { defaultScopesServices, ScopesContextProvider } from '../../ScopesContextProvider'; diff --git a/public/app/features/search/service/deletedDashboardsCache.ts b/public/app/features/search/service/deletedDashboardsCache.ts index cab6774b97b..7559d00605c 100644 --- a/public/app/features/search/service/deletedDashboardsCache.ts +++ b/public/app/features/search/service/deletedDashboardsCache.ts @@ -1,7 +1,6 @@ import { isResourceList } from 'app/features/apiserver/guards'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; - -import { DashboardDataDTO } from '../../../types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { SearchHit } from './unified'; import { resourceToSearchResult } from './utils'; diff --git a/public/app/features/search/service/sql.ts b/public/app/features/search/service/sql.ts index d8f468ee774..ba0dbc9c894 100644 --- a/public/app/features/search/service/sql.ts +++ b/public/app/features/search/service/sql.ts @@ -2,7 +2,7 @@ import { DataFrame, DataFrameView, FieldType, getDisplayProcessor, SelectableVal import { config } from '@grafana/runtime'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; import { backendSrv } from 'app/core/services/backend_srv'; -import { PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; import { DEFAULT_MAX_VALUES, GENERAL_FOLDER_UID, TYPE_KIND_MAP } from '../constants'; import { DashboardSearchHit, DashboardSearchItemType } from '../types'; diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index cb360ba8deb..3ebda023037 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -1,6 +1,6 @@ import { DataFrameView, SelectableValue } from '@grafana/data'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; -import { PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; import { ManagerKind } from '../../apiserver/types'; diff --git a/public/app/features/search/service/utils.ts b/public/app/features/search/service/utils.ts index 94fb1e7d345..981127f6688 100644 --- a/public/app/features/search/service/utils.ts +++ b/public/app/features/search/service/utils.ts @@ -2,8 +2,8 @@ import { DataFrameView, IconName, fuzzySearch } from '@grafana/data'; import { isSharedWithMe } from 'app/features/browse-dashboards/components/utils'; import { DashboardViewItemWithUIItems } from 'app/features/browse-dashboards/types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { DashboardDataDTO } from 'app/types/dashboard'; -import { DashboardDataDTO } from '../../../types'; import { AnnoKeyFolder, ResourceList } from '../../apiserver/types'; import { DashboardSearchHit, DashboardSearchItemType, DashboardViewItem, DashboardViewItemKind } from '../types'; diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index fb210f4b0c0..b65b380cebf 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { config, getBackendSrv, locationService } from '@grafana/runtime'; import { Button, Input, Field, FieldSet } from '@grafana/ui'; @@ -10,7 +11,8 @@ import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { RolePickerSelect } from 'app/core/components/RolePickerDrawer/RolePickerSelect'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, OrgRole, Role, ServiceAccountCreateApiResponse, ServiceAccountDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO, ServiceAccountCreateApiResponse } from 'app/types/serviceaccount'; import { OrgRolePicker } from '../admin/OrgRolePicker'; diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx index ee61c2af792..4de16f29479 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx @@ -2,7 +2,9 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; -import { ApiKey, OrgRole, ServiceAccountDTO } from 'app/types'; +import { OrgRole } from '@grafana/data'; +import { ApiKey } from 'app/types/apiKeys'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; import { ServiceAccountPageUnconnected, Props } from './ServiceAccountPage'; diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index ccc445fae5e..b21f6c9fe6d 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -7,7 +7,10 @@ import { Trans, t } from '@grafana/i18n'; import { Button, ConfirmModal, IconButton, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, ApiKey, ServiceAccountDTO, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ApiKey } from 'app/types/apiKeys'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; +import { StoreState } from 'app/types/store'; import { ServiceAccountPermissions } from './ServiceAccountPermissions'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; diff --git a/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx b/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx index e6f3adc78b3..303dfc79716 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx @@ -1,8 +1,8 @@ import { t } from '@grafana/i18n'; import { Permissions } from 'app/core/components/AccessControl'; import { contextSrv } from 'app/core/services/context_srv'; - -import { AccessControlAction, ServiceAccountDTO } from '../../types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; type ServiceAccountPermissionsProps = { serviceAccount: ServiceAccountDTO; diff --git a/public/app/features/serviceaccounts/ServiceAccountTable.tsx b/public/app/features/serviceaccounts/ServiceAccountTable.tsx index 648cb391fb8..8d7363fa37f 100644 --- a/public/app/features/serviceaccounts/ServiceAccountTable.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountTable.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import Skeleton from 'react-loading-skeleton'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Avatar, @@ -16,7 +17,8 @@ import { } from '@grafana/ui'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, OrgRole, Role, ServiceAccountDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; import { OrgRolePicker } from '../admin/OrgRolePicker'; diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx index f5f563321af..d4b870c2921 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx @@ -2,7 +2,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; -import { OrgRole, ServiceAccountDTO, ServiceAccountStateFilter } from 'app/types'; +import { OrgRole } from '@grafana/data'; +import { ServiceAccountStateFilter, ServiceAccountDTO } from 'app/types/serviceaccount'; import { Props, ServiceAccountsListPageUnconnected } from './ServiceAccountsListPage'; diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index ff6218e43e4..9929f0dfe91 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -19,7 +19,9 @@ import { import { Page } from 'app/core/components/Page/Page'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { StoreState, ServiceAccountDTO, AccessControlAction, ServiceAccountStateFilter } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountStateFilter, ServiceAccountDTO } from 'app/types/serviceaccount'; +import { StoreState } from 'app/types/store'; import { ServiceAccountTable } from './ServiceAccountTable'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx index 9e52f25df92..563d0d8b89d 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx @@ -6,7 +6,8 @@ import { Trans, t } from '@grafana/i18n'; import { Label, TextLink, useStyles2 } from '@grafana/ui'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Role, ServiceAccountDTO } from 'app/types'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; import { ServiceAccountProfileRow } from './ServiceAccountProfileRow'; import { ServiceAccountRoleRow } from './ServiceAccountRoleRow'; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountRoleRow.tsx b/public/app/features/serviceaccounts/components/ServiceAccountRoleRow.tsx index 015fd003b33..33a33229b87 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountRoleRow.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountRoleRow.tsx @@ -1,9 +1,11 @@ +import { OrgRole } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Label } from '@grafana/ui'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { contextSrv } from 'app/core/core'; import { OrgRolePicker } from 'app/features/admin/OrgRolePicker'; -import { AccessControlAction, OrgRole, Role, ServiceAccountDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; interface Props { label: string; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx b/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx index 4c065d0d1df..66b2e818efd 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx @@ -3,7 +3,7 @@ import { css, cx } from '@emotion/css'; import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { DeleteButton, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui'; -import { ApiKey } from 'app/types'; +import { ApiKey } from 'app/types/apiKeys'; interface Props { tokens: ApiKey[]; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx index e9a2478557b..9bf659f1500 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx @@ -9,7 +9,8 @@ import { SkeletonComponent, attachSkeleton } from '@grafana/ui/unstable'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { contextSrv } from 'app/core/core'; import { OrgRolePicker } from 'app/features/admin/OrgRolePicker'; -import { AccessControlAction, Role, ServiceAccountDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; type ServiceAccountListItemProps = { serviceAccount: ServiceAccountDTO; diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 06591867aa4..5e75bc5378f 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -3,7 +3,9 @@ import { debounce } from 'lodash'; import { getBackendSrv } from '@grafana/runtime'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, ServiceAccountDTO, ServiceAccountStateFilter, ThunkResult } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO, ServiceAccountStateFilter } from 'app/types/serviceaccount'; +import { ThunkResult } from 'app/types/store'; import { ServiceAccountToken } from '../components/CreateTokenModal'; diff --git a/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts b/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts index 4644d3ae136..0921d2e28c4 100644 --- a/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts +++ b/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts @@ -1,6 +1,7 @@ import { getBackendSrv, locationService } from '@grafana/runtime'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { ServiceAccountDTO, ThunkResult } from 'app/types'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; +import { ThunkResult } from 'app/types/store'; import { ServiceAccountToken } from '../components/CreateTokenModal'; diff --git a/public/app/features/serviceaccounts/state/reducers.ts b/public/app/features/serviceaccounts/state/reducers.ts index 5794233d434..705c07c4287 100644 --- a/public/app/features/serviceaccounts/state/reducers.ts +++ b/public/app/features/serviceaccounts/state/reducers.ts @@ -1,13 +1,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { Role } from 'app/types/accessControl'; +import { ApiKey } from 'app/types/apiKeys'; import { - ApiKey, - Role, - ServiceAccountDTO, ServiceAccountProfileState, + ServiceAccountDTO, ServiceAccountsState, ServiceAccountStateFilter, -} from 'app/types'; +} from 'app/types/serviceaccount'; // serviceAccountsProfilePage export const initialStateProfile: ServiceAccountProfileState = { diff --git a/public/app/features/support-bundles/SupportBundles.tsx b/public/app/features/support-bundles/SupportBundles.tsx index 2963dad4d0f..b21604caae0 100644 --- a/public/app/features/support-bundles/SupportBundles.tsx +++ b/public/app/features/support-bundles/SupportBundles.tsx @@ -6,7 +6,8 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, Spinner, IconButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState } from 'app/types/store'; import { loadBundles, removeBundle, checkBundles } from './state/actions'; diff --git a/public/app/features/support-bundles/SupportBundlesCreate.tsx b/public/app/features/support-bundles/SupportBundlesCreate.tsx index 1d916884cb8..4461870556a 100644 --- a/public/app/features/support-bundles/SupportBundlesCreate.tsx +++ b/public/app/features/support-bundles/SupportBundlesCreate.tsx @@ -5,7 +5,7 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Field, Checkbox, LinkButton, Stack, Alert } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { loadSupportBundleCollectors, createSupportBundle } from './state/actions'; diff --git a/public/app/features/support-bundles/state/actions.ts b/public/app/features/support-bundles/state/actions.ts index f42babe81ec..d3270e1a5da 100644 --- a/public/app/features/support-bundles/state/actions.ts +++ b/public/app/features/support-bundles/state/actions.ts @@ -1,7 +1,8 @@ import { throttle } from 'lodash'; import { getBackendSrv, locationService } from '@grafana/runtime'; -import { SupportBundle, SupportBundleCollector, SupportBundleCreateRequest, ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; +import { SupportBundle, SupportBundleCollector, SupportBundleCreateRequest } from 'app/types/supportBundles'; import { collectorsFetchBegin, diff --git a/public/app/features/support-bundles/state/reducers.ts b/public/app/features/support-bundles/state/reducers.ts index 21e32402ec0..81a1e0d5e80 100644 --- a/public/app/features/support-bundles/state/reducers.ts +++ b/public/app/features/support-bundles/state/reducers.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { SupportBundle, SupportBundleCollector, SupportBundlesState } from 'app/types'; +import { SupportBundlesState, SupportBundle, SupportBundleCollector } from 'app/types/supportBundles'; export const initialState: SupportBundlesState = { supportBundles: [], diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index c52a71e4bcb..0744271c048 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -10,7 +10,8 @@ import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; import { updateTeamRoles } from 'app/core/components/RolePicker/api'; import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Role, TeamDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { TeamDTO } from 'app/types/teams'; const pageNav: NavModelItem = { icon: 'users-alt', diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index 841a85b5d4b..bda03e89d5d 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { TeamGroup } from '../../types'; +import { TeamGroup } from 'app/types/teams'; import { Props, TeamGroupSync } from './TeamGroupSync'; import { getMockTeamGroups } from './mocks/teamMocks'; diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index e45c525144f..e87a1d4380f 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -9,8 +9,8 @@ import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { UpgradeBox, UpgradeContent, UpgradeContentProps } from 'app/core/components/Upgrade/UpgradeBox'; import { highlightTrial } from 'app/features/admin/utils'; - -import { StoreState, TeamGroup } from '../../types'; +import { StoreState } from 'app/types/store'; +import { TeamGroup } from 'app/types/teams'; import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; import { getTeamGroups } from './state/selectors'; diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 4826c2bc24f..a5f8adc3f6e 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -3,8 +3,7 @@ import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; import { contextSrv } from 'app/core/services/context_srv'; - -import { Team } from '../../types'; +import { Team } from 'app/types/teams'; import { Props, TeamList } from './TeamList'; import { getMockTeam, getMultipleMockTeams } from './mocks/teamMocks'; diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index dd93bd941e7..b1a5952eca8 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -24,7 +24,9 @@ import { import { Page } from 'app/core/components/Page/Page'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, Role, StoreState, TeamWithRoles } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { StoreState } from 'app/types/store'; +import { TeamWithRoles } from 'app/types/teams'; import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker'; import { EnterpriseAuthFeaturesCard } from '../admin/EnterpriseAuthFeaturesCard'; diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 940f6950d1a..89a5cb5e032 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -9,7 +9,8 @@ import { UpgradeBox } from 'app/core/components/Upgrade/UpgradeBox'; import config from 'app/core/config'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, StoreState, useDispatch, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState, useDispatch, useSelector } from 'app/types/store'; import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync'; import TeamPermissions from './TeamPermissions'; diff --git a/public/app/features/teams/TeamPermissions.tsx b/public/app/features/teams/TeamPermissions.tsx index 349b2531dab..47823bfcae6 100644 --- a/public/app/features/teams/TeamPermissions.tsx +++ b/public/app/features/teams/TeamPermissions.tsx @@ -1,7 +1,7 @@ import { Permissions } from 'app/core/components/AccessControl'; import { contextSrv } from 'app/core/services/context_srv'; - -import { AccessControlAction, Team } from '../../types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Team } from 'app/types/teams'; type TeamPermissionsProps = { team: Team; diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index fa960f25336..79e9ff95dd9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -7,7 +7,8 @@ import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, Team } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Team } from 'app/types/teams'; import { updateTeam } from './state/actions'; diff --git a/public/app/features/teams/mocks/teamMocks.ts b/public/app/features/teams/mocks/teamMocks.ts index 3da74f056f1..d6472499cb2 100644 --- a/public/app/features/teams/mocks/teamMocks.ts +++ b/public/app/features/teams/mocks/teamMocks.ts @@ -1,6 +1,7 @@ import { randomBytes } from 'crypto'; -import { Team, TeamGroup, TeamMember, TeamPermissionLevel } from 'app/types'; +import { TeamPermissionLevel } from 'app/types/acl'; +import { Team, TeamMember, TeamGroup } from 'app/types/teams'; function generateShortUid(): string { return randomBytes(3).toString('hex'); // Generate a short UID diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 967c87ac904..e7a6c5a923a 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -5,7 +5,9 @@ import { FetchDataArgs } from '@grafana/ui'; import { updateNavIndex } from 'app/core/actions'; import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { AccessControlAction, TeamWithRoles, TeamMember, ThunkResult, Team } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ThunkResult } from 'app/types/store'; +import { Team, TeamMember, TeamWithRoles } from 'app/types/teams'; import { buildNavModel } from './navModel'; import { diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts index 74decae3e94..b6267c73d0e 100644 --- a/public/app/features/teams/state/navModel.ts +++ b/public/app/features/teams/state/navModel.ts @@ -5,7 +5,9 @@ import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import config from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import { highlightTrial } from 'app/features/admin/utils'; -import { AccessControlAction, Team, TeamPermissionLevel } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { TeamPermissionLevel } from 'app/types/acl'; +import { Team } from 'app/types/teams'; import userProfilePng from 'img/user_profile.png'; const loadingTeam = { diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 483b22700f0..e42e467c6b1 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,5 +1,6 @@ +import { TeamsState, TeamState } from 'app/types/teams'; + import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { TeamsState, TeamState } from '../../../types'; import { getMockTeam, getMockTeamGroups, getMockTeamMember } from '../mocks/teamMocks'; import { diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 4602f33540c..b98c6d21095 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types'; +import { TeamsState, Team, TeamState, TeamMember, TeamGroup } from 'app/types/teams'; export const initialTeamsState: TeamsState = { teams: [], diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 58f4dbbeeef..b1fed3d1ef3 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,4 +1,5 @@ -import { TeamState } from '../../../types'; +import { TeamState } from 'app/types/teams'; + import { getMockTeam } from '../mocks/teamMocks'; import { getTeam } from './selectors'; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index ec8e6eddfc4..ec99297158c 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,4 +1,4 @@ -import { Team, TeamState } from 'app/types'; +import { Team, TeamState } from 'app/types/teams'; export const getTeamGroups = (state: TeamState) => state.groups; diff --git a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx index d9dc1cff6ec..d106bbfadeb 100644 --- a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx @@ -91,4 +91,5 @@ export const concatenateTransformRegistryItem: TransformerRegistryItem<Concatena 'Combine all fields into a single frame. Values will be appended with undefined values if not the same length.', categories: new Set([TransformerCategory.Combine]), help: getTransformationContent(DataTransformerID.concatenate).helperDocs, + tags: new Set(['Combine']), }; diff --git a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx index 9508dfee1e7..14fddeba815 100644 --- a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx @@ -51,7 +51,7 @@ export const ConvertFieldTypeTransformerEditor = ({ const onSelectField = useCallback( (idx: number) => (value: string | undefined) => { - const conversions = options.conversions; + const conversions = [...options.conversions]; conversions[idx] = { ...conversions[idx], targetField: value ?? '', dateFormat: undefined }; onChange({ ...options, @@ -275,4 +275,5 @@ export const convertFieldTypeTransformRegistryItem: TransformerRegistryItem<Conv description: standardTransformers.convertFieldTypeTransformer.description, categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.convertFieldType).helperDocs, + tags: new Set(['Format field']), }; diff --git a/public/app/features/transformers/editors/TransposeTransformerEditor.tsx b/public/app/features/transformers/editors/TransposeTransformerEditor.tsx index 67af215029b..fd9b5c332c5 100644 --- a/public/app/features/transformers/editors/TransposeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/TransposeTransformerEditor.tsx @@ -9,7 +9,7 @@ import { TransposeTransformerOptions } from '@grafana/data/internal'; import { t } from '@grafana/i18n'; import { InlineField, InlineFieldRow, Input } from '@grafana/ui'; -export const TransposeTransfomerEditor = ({ options, onChange }: TransformerUIProps<TransposeTransformerOptions>) => { +export const TransposeTransformerEditor = ({ options, onChange }: TransformerUIProps<TransposeTransformerOptions>) => { return ( <> <InlineFieldRow> @@ -45,9 +45,10 @@ export const TransposeTransfomerEditor = ({ options, onChange }: TransformerUIPr export const transposeTransformerRegistryItem: TransformerRegistryItem<TransposeTransformerOptions> = { id: DataTransformerID.transpose, - editor: TransposeTransfomerEditor, + editor: TransposeTransformerEditor, transformation: standardTransformers.transposeTransformer, name: standardTransformers.transposeTransformer.name, description: standardTransformers.transposeTransformer.description, categories: new Set([TransformerCategory.Reformat]), + tags: new Set(['Pivot', 'Translate', 'Transform']), }; diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 79659a9e54f..18ab669a9f0 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -5,7 +5,8 @@ import { reportInteraction } from '@grafana/runtime'; import { RadioButtonGroup, LinkButton, FilterInput, InlineField } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState } from 'app/types/store'; import { selectTotal } from '../invites/state/selectors'; diff --git a/public/app/features/users/UsersListPage.test.tsx b/public/app/features/users/UsersListPage.test.tsx index 4f8f88c3c4a..91dc6767767 100644 --- a/public/app/features/users/UsersListPage.test.tsx +++ b/public/app/features/users/UsersListPage.test.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { configureStore } from 'app/store/configureStore'; -import { Invitee, OrgUser } from 'app/types'; +import { Invitee, OrgUser } from 'app/types/user'; import { Props, UsersListPageUnconnected } from './UsersListPage'; import { pageChanged, sortChanged } from './state/reducers'; diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index e1992a7f46a..b362bf81621 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { renderMarkdown } from '@grafana/data'; +import { OrgRole, renderMarkdown } from '@grafana/data'; import { Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { OrgUser, OrgRole, StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; +import { OrgUser } from 'app/types/user'; import { OrgUsersTable } from '../admin/Users/OrgUsersTable'; import InviteesTable from '../invites/InviteesTable'; diff --git a/public/app/features/users/mocks/userMocks.ts b/public/app/features/users/mocks/userMocks.ts index 68ce9a4bfba..6c0895f1887 100644 --- a/public/app/features/users/mocks/userMocks.ts +++ b/public/app/features/users/mocks/userMocks.ts @@ -1,4 +1,5 @@ -import { OrgRole, OrgUser } from 'app/types'; +import { OrgRole } from '@grafana/data'; +import { OrgUser } from 'app/types/user'; import { UsersFetchResult, initialState } from '../state/reducers'; diff --git a/public/app/features/users/state/actions.ts b/public/app/features/users/state/actions.ts index 9dbc7672f63..b13bf27e223 100644 --- a/public/app/features/users/state/actions.ts +++ b/public/app/features/users/state/actions.ts @@ -4,9 +4,9 @@ import { getBackendSrv } from '@grafana/runtime'; import { FetchDataArgs } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { OrgUser } from 'app/types'; - -import { AccessControlAction, ThunkResult } from '../../../types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ThunkResult } from 'app/types/store'; +import { OrgUser } from 'app/types/user'; import { usersLoaded, diff --git a/public/app/features/users/state/reducers.test.ts b/public/app/features/users/state/reducers.test.ts index 5c76c88ba5e..abfc484300b 100644 --- a/public/app/features/users/state/reducers.test.ts +++ b/public/app/features/users/state/reducers.test.ts @@ -1,5 +1,6 @@ +import { UsersState } from 'app/types/user'; + import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { UsersState } from '../../../types'; import { getMockUsers, getFetchUsersMock } from '../mocks/userMocks'; import { initialState, searchQueryChanged, usersLoaded, usersReducer } from './reducers'; diff --git a/public/app/features/users/state/reducers.ts b/public/app/features/users/state/reducers.ts index f74fea6ad1d..bb7cc4c8ea0 100644 --- a/public/app/features/users/state/reducers.ts +++ b/public/app/features/users/state/reducers.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import config from 'app/core/config'; -import { OrgUser, UsersState } from 'app/types'; +import { UsersState, OrgUser } from 'app/types/user'; export const initialState: UsersState = { users: [], diff --git a/public/app/features/users/state/selectors.ts b/public/app/features/users/state/selectors.ts index 2d80444a47d..a2ba378aaf4 100644 --- a/public/app/features/users/state/selectors.ts +++ b/public/app/features/users/state/selectors.ts @@ -1,4 +1,4 @@ -import { UsersState } from 'app/types'; +import { UsersState } from 'app/types/user'; export const getUsers = (state: UsersState) => { const regex = new RegExp(state.searchQuery, 'i'); diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx index 90e99866827..65745e8337f 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { AdHocVariableModel, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; import { AdHocVariableForm } from 'app/features/dashboard-scene/settings/variables/components/AdHocVariableForm'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { initialVariableEditorState } from '../editor/reducer'; import { getAdhocVariableEditorState } from '../editor/selectors'; diff --git a/public/app/features/variables/adhoc/actions.ts b/public/app/features/variables/adhoc/actions.ts index 4263bd09d3f..7e57e0d6344 100644 --- a/public/app/features/variables/adhoc/actions.ts +++ b/public/app/features/variables/adhoc/actions.ts @@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'; import { AdHocVariableFilter, AdHocVariableModel, DataSourceRef } from '@grafana/data'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { StoreState, ThunkResult } from 'app/types'; +import { StoreState, ThunkResult } from 'app/types/store'; import { changeVariableEditorExtended } from '../editor/reducer'; import { isAdHoc } from '../guard'; diff --git a/public/app/features/variables/constant/actions.ts b/public/app/features/variables/constant/actions.ts index 87f78229983..17ba4e9699b 100644 --- a/public/app/features/variables/constant/actions.ts +++ b/public/app/features/variables/constant/actions.ts @@ -1,4 +1,4 @@ -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { validateVariableSelectionState } from '../state/actions'; import { toKeyedAction } from '../state/keyedVariablesReducer'; diff --git a/public/app/features/variables/custom/CustomVariableEditor.tsx b/public/app/features/variables/custom/CustomVariableEditor.tsx index 7488326cec7..82b783542e2 100644 --- a/public/app/features/variables/custom/CustomVariableEditor.tsx +++ b/public/app/features/variables/custom/CustomVariableEditor.tsx @@ -4,7 +4,7 @@ import { MapDispatchToProps, MapStateToProps } from 'react-redux'; import { CustomVariableModel, VariableWithMultiSupport } from '@grafana/data'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; import { CustomVariableForm } from 'app/features/dashboard-scene/settings/variables/components/CustomVariableForm'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { OnPropChangeArguments, VariableEditorProps } from '../editor/types'; import { changeVariableMultiValue } from '../state/actions'; diff --git a/public/app/features/variables/custom/actions.ts b/public/app/features/variables/custom/actions.ts index d8751dd56ed..73eff38f5f5 100644 --- a/public/app/features/variables/custom/actions.ts +++ b/public/app/features/variables/custom/actions.ts @@ -1,5 +1,5 @@ import { getTemplateSrv } from '@grafana/runtime'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { validateVariableSelectionState } from '../state/actions'; import { toKeyedAction } from '../state/keyedVariablesReducer'; diff --git a/public/app/features/variables/datasource/DataSourceVariableEditor.tsx b/public/app/features/variables/datasource/DataSourceVariableEditor.tsx index 2f9116d4d7b..256c5ff29fd 100644 --- a/public/app/features/variables/datasource/DataSourceVariableEditor.tsx +++ b/public/app/features/variables/datasource/DataSourceVariableEditor.tsx @@ -3,8 +3,8 @@ import { connect, ConnectedProps } from 'react-redux'; import { DataSourceVariableModel, SelectableValue, VariableWithMultiSupport } from '@grafana/data'; import { DataSourceVariableForm } from 'app/features/dashboard-scene/settings/variables/components/DataSourceVariableForm'; +import { StoreState } from 'app/types/store'; -import { StoreState } from '../../../types'; import { initialVariableEditorState } from '../editor/reducer'; import { getDatasourceVariableEditorState } from '../editor/selectors'; import { OnPropChangeArguments, VariableEditorProps } from '../editor/types'; diff --git a/public/app/features/variables/datasource/actions.ts b/public/app/features/variables/datasource/actions.ts index d2786309f44..8daf15ec8bc 100644 --- a/public/app/features/variables/datasource/actions.ts +++ b/public/app/features/variables/datasource/actions.ts @@ -2,8 +2,8 @@ import { chain } from 'lodash'; import { stringToJsRegex } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { changeVariableEditorExtended } from '../editor/reducer'; import { validateVariableSelectionState } from '../state/actions'; diff --git a/public/app/features/variables/editor/VariableEditorContainer.tsx b/public/app/features/variables/editor/VariableEditorContainer.tsx index f810d4bfc88..07398d88f0b 100644 --- a/public/app/features/variables/editor/VariableEditorContainer.tsx +++ b/public/app/features/variables/editor/VariableEditorContainer.tsx @@ -5,8 +5,8 @@ import { bindActionCreators } from 'redux'; import { locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; import { SettingsPageProps } from 'app/features/dashboard/components/DashboardSettings/types'; +import { StoreState, ThunkDispatch } from 'app/types/store'; -import { StoreState, ThunkDispatch } from '../../../types'; import { VariablesUnknownTable } from '../inspect/VariablesUnknownTable'; import { toKeyedAction } from '../state/keyedVariablesReducer'; import { getEditorVariables, getVariablesState } from '../state/selectors'; diff --git a/public/app/features/variables/editor/VariableEditorEditor.tsx b/public/app/features/variables/editor/VariableEditorEditor.tsx index 07cc03bac26..3a8e4b88c62 100644 --- a/public/app/features/variables/editor/VariableEditorEditor.tsx +++ b/public/app/features/variables/editor/VariableEditorEditor.tsx @@ -8,8 +8,8 @@ import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { Button, HorizontalGroup, Icon, Themeable2, withTheme2 } from '@grafana/ui'; +import { StoreState, ThunkDispatch } from 'app/types/store'; -import { StoreState, ThunkDispatch } from '../../../types'; import { VariableHideSelect } from '../../dashboard-scene/settings/variables/components/VariableHideSelect'; import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend'; import { VariableTextAreaField } from '../../dashboard-scene/settings/variables/components/VariableTextAreaField'; diff --git a/public/app/features/variables/editor/actions.ts b/public/app/features/variables/editor/actions.ts index 9f99d411b8a..827a951d56a 100644 --- a/public/app/features/variables/editor/actions.ts +++ b/public/app/features/variables/editor/actions.ts @@ -2,8 +2,8 @@ import { cloneDeep } from 'lodash'; import { TypedVariableModel, VariableType } from '@grafana/data'; import { locationService } from '@grafana/runtime'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { variableAdapters } from '../adapters'; import { initInspect } from '../inspect/reducer'; import { createUsagesNetwork, transformUsagesToNetwork } from '../inspect/utils'; diff --git a/public/app/features/variables/editor/getVariableQueryEditor.tsx b/public/app/features/variables/editor/getVariableQueryEditor.tsx index 2e4177ef10d..ad96e78cca1 100644 --- a/public/app/features/variables/editor/getVariableQueryEditor.tsx +++ b/public/app/features/variables/editor/getVariableQueryEditor.tsx @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { DataQuery, DataSourceApi, DataSourceJsonData, QueryEditorProps, StandardVariableQuery } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; -import { importDataSourcePlugin } from '../../plugins/plugin_loader'; +import { importDataSourcePlugin } from '../../plugins/pluginLoader'; import { hasCustomVariableSupport, hasDatasourceVariableSupport, diff --git a/public/app/features/variables/interval/actions.ts b/public/app/features/variables/interval/actions.ts index f583eb7df57..3043da16e0d 100644 --- a/public/app/features/variables/interval/actions.ts +++ b/public/app/features/variables/interval/actions.ts @@ -1,6 +1,6 @@ import { rangeUtil } from '@grafana/data'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { getTimeSrv } from '../../dashboard/services/TimeSrv'; import { getTemplateSrv, TemplateSrv } from '../../templating/template_srv'; import { validateVariableSelectionState } from '../state/actions'; diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx index 7a52df41e12..df8736b4935 100644 --- a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx +++ b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx @@ -6,7 +6,7 @@ import { bindActionCreators } from 'redux'; import { LoadingState, VariableOption, VariableWithMultiSupport, VariableWithOptions } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { ClickOutsideWrapper } from '@grafana/ui'; -import { StoreState, ThunkDispatch } from 'app/types'; +import { StoreState, ThunkDispatch } from 'app/types/store'; import { VARIABLE_PREFIX } from '../../constants'; import { isMulti } from '../../guard'; diff --git a/public/app/features/variables/pickers/OptionsPicker/actions.ts b/public/app/features/variables/pickers/OptionsPicker/actions.ts index 43ef1fdec6e..e6049b37187 100644 --- a/public/app/features/variables/pickers/OptionsPicker/actions.ts +++ b/public/app/features/variables/pickers/OptionsPicker/actions.ts @@ -1,7 +1,7 @@ import { debounce, trim } from 'lodash'; import { isEmptyObject, containsSearchFilter, VariableWithOptions, VariableOption } from '@grafana/data'; -import { StoreState, ThunkDispatch, ThunkResult } from 'app/types'; +import { StoreState, ThunkDispatch, ThunkResult } from 'app/types/store'; import { variableAdapters } from '../../adapters'; import { hasOptions } from '../../guard'; diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx index a10700237bd..5d6a56bdb5a 100644 --- a/public/app/features/variables/query/QueryVariableEditor.tsx +++ b/public/app/features/variables/query/QueryVariableEditor.tsx @@ -10,8 +10,8 @@ import { VariableSort, } from '@grafana/data'; import { QueryVariableEditorForm } from 'app/features/dashboard-scene/settings/variables/components/QueryVariableForm'; +import { StoreState } from 'app/types/store'; -import { StoreState } from '../../../types'; import { getTimeSrv } from '../../dashboard/services/TimeSrv'; import { initialVariableEditorState } from '../editor/reducer'; import { getQueryVariableEditorState } from '../editor/selectors'; diff --git a/public/app/features/variables/query/VariableQueryRunner.ts b/public/app/features/variables/query/VariableQueryRunner.ts index 2a21c3c1226..aae4819e01a 100644 --- a/public/app/features/variables/query/VariableQueryRunner.ts +++ b/public/app/features/variables/query/VariableQueryRunner.ts @@ -12,9 +12,9 @@ import { QueryVariableModel, ScopedVars, } from '@grafana/data'; +import { StoreState, ThunkDispatch } from 'app/types/store'; import { dispatch, getState } from '../../../store/store'; -import { StoreState, ThunkDispatch } from '../../../types'; import { getTimeSrv } from '../../dashboard/services/TimeSrv'; import { runRequest } from '../../query/state/runRequest'; import { getLastKey, getVariable } from '../state/selectors'; diff --git a/public/app/features/variables/query/actions.test.tsx b/public/app/features/variables/query/actions.test.tsx index 72cfc067092..7a203bd4b77 100644 --- a/public/app/features/variables/query/actions.test.tsx +++ b/public/app/features/variables/query/actions.test.tsx @@ -69,7 +69,7 @@ const mocks: Record<string, any> = { setDataSourceSrv(mocks.dataSourceSrv as DataSourceSrv); -jest.mock('../../plugins/plugin_loader', () => ({ +jest.mock('../../plugins/pluginLoader', () => ({ importDataSourcePlugin: () => mocks.pluginLoader.importDataSourcePlugin(), })); diff --git a/public/app/features/variables/query/actions.ts b/public/app/features/variables/query/actions.ts index fe494c17560..90fff7f6a46 100644 --- a/public/app/features/variables/query/actions.ts +++ b/public/app/features/variables/query/actions.ts @@ -2,8 +2,8 @@ import { Subscription } from 'rxjs'; import { DataSourceRef } from '@grafana/data'; import { getDataSourceSrv, toDataQueryError } from '@grafana/runtime'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { getVariableQueryEditor } from '../editor/getVariableQueryEditor'; import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer'; import { getQueryVariableEditorState } from '../editor/selectors'; diff --git a/public/app/features/variables/query/operators.ts b/public/app/features/variables/query/operators.ts index c4bb25960bb..a119b4ebd97 100644 --- a/public/app/features/variables/query/operators.ts +++ b/public/app/features/variables/query/operators.ts @@ -10,8 +10,8 @@ import { PanelData, QueryVariableModel, } from '@grafana/data'; +import { ThunkDispatch } from 'app/types/store'; -import { ThunkDispatch } from '../../../types'; import { validateVariableSelectionState } from '../state/actions'; import { toKeyedAction } from '../state/keyedVariablesReducer'; import { getTemplatedRegex, toKeyedVariableIdentifier, toVariablePayload } from '../utils'; diff --git a/public/app/features/variables/state/actions.ts b/public/app/features/variables/state/actions.ts index fdbc54bbbba..ffc45b6b939 100644 --- a/public/app/features/variables/state/actions.ts +++ b/public/app/features/variables/state/actions.ts @@ -26,12 +26,13 @@ import { contextSrv } from 'app/core/services/context_srv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { store } from 'app/store/store'; +import { AppNotification } from 'app/types/appNotifications'; +import { ThunkResult, StoreState } from 'app/types/store'; import { createErrorNotification } from '../../../core/copy/appNotification'; import { appEvents } from '../../../core/core'; import { getBackendSrv } from '../../../core/services/backend_srv'; import { Graph, Node } from '../../../core/utils/dag'; -import { AppNotification, StoreState, ThunkResult } from '../../../types'; import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { getTemplateSrv, TemplateSrv } from '../../templating/template_srv'; import { variableAdapters } from '../adapters'; diff --git a/public/app/features/variables/state/helpers.ts b/public/app/features/variables/state/helpers.ts index 9752d6ecf5a..163c6d7acf9 100644 --- a/public/app/features/variables/state/helpers.ts +++ b/public/app/features/variables/state/helpers.ts @@ -9,8 +9,9 @@ import { } from '@grafana/data'; import { VariableRefresh } from '@grafana/schema'; import { dashboardReducer } from 'app/features/dashboard/state/reducers'; +import { DashboardState } from 'app/types/dashboard'; +import { StoreState } from 'app/types/store'; -import { DashboardState, StoreState } from '../../../types'; import { VariableAdapter } from '../adapters'; import { NEW_VARIABLE_ID } from '../constants'; import { initialVariableModelState } from '../types'; diff --git a/public/app/features/variables/state/onTimeRangeUpdated.test.ts b/public/app/features/variables/state/onTimeRangeUpdated.test.ts index 528db0045ec..efed3f1b5ab 100644 --- a/public/app/features/variables/state/onTimeRangeUpdated.test.ts +++ b/public/app/features/variables/state/onTimeRangeUpdated.test.ts @@ -1,12 +1,12 @@ import { dateTime, TimeRange, VariableRefresh } from '@grafana/data'; import { config, DataSourceSrv } from '@grafana/runtime'; import * as runtime from '@grafana/runtime'; +import { DashboardState } from 'app/types/dashboard'; import { reduxTester } from '../../../../test/core/redux/reduxTester'; import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; import { appEvents } from '../../../core/core'; import { notifyApp } from '../../../core/reducers/appNotification'; -import { DashboardState } from '../../../types'; import { DashboardModel } from '../../dashboard/state/DashboardModel'; import { createDashboardModelFixture } from '../../dashboard/state/__fixtures__/dashboardFixtures'; import { TemplateSrv } from '../../templating/template_srv'; diff --git a/public/app/features/variables/state/selectors.ts b/public/app/features/variables/state/selectors.ts index d9334fd17c0..31e14318d87 100644 --- a/public/app/features/variables/state/selectors.ts +++ b/public/app/features/variables/state/selectors.ts @@ -1,9 +1,9 @@ import memoizeOne from 'memoize-one'; import { TypedVariableModel } from '@grafana/data'; +import { StoreState } from 'app/types/store'; import { getState } from '../../../store/store'; -import { StoreState } from '../../../types'; import { toStateKey } from '../utils'; import { getInitialTemplatingState, TemplatingState } from './reducers'; diff --git a/public/app/features/variables/state/templateVarsChangedInUrl.test.ts b/public/app/features/variables/state/templateVarsChangedInUrl.test.ts index 8e6f6432599..5d6828877c7 100644 --- a/public/app/features/variables/state/templateVarsChangedInUrl.test.ts +++ b/public/app/features/variables/state/templateVarsChangedInUrl.test.ts @@ -1,6 +1,7 @@ import { TypedVariableModel } from '@grafana/data'; +import { DashboardState } from 'app/types/dashboard'; +import { StoreState } from 'app/types/store'; -import { DashboardState, StoreState } from '../../../types'; import { PanelModel } from '../../dashboard/state/PanelModel'; import { createDashboardModelFixture } from '../../dashboard/state/__fixtures__/dashboardFixtures'; import { initialState } from '../../dashboard/state/reducers'; diff --git a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx index 260a57d972e..143cc6f128d 100644 --- a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx +++ b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx @@ -3,7 +3,7 @@ import { ChangeEvent, FocusEvent, KeyboardEvent, ReactElement, useCallback, useE import { TextBoxVariableModel, isEmptyObject } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Input } from '@grafana/ui'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { variableAdapters } from '../adapters'; import { VARIABLE_PREFIX } from '../constants'; diff --git a/public/app/features/variables/textbox/actions.ts b/public/app/features/variables/textbox/actions.ts index c80cc3dd271..0e3c249614f 100644 --- a/public/app/features/variables/textbox/actions.ts +++ b/public/app/features/variables/textbox/actions.ts @@ -1,6 +1,6 @@ import { UrlQueryValue } from '@grafana/data'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { variableAdapters } from '../adapters'; import { setOptionFromUrl } from '../state/actions'; import { toKeyedAction } from '../state/keyedVariablesReducer'; diff --git a/public/app/features/variables/utils.ts b/public/app/features/variables/utils.ts index 4ab42c8ec5c..6908bb9ce57 100644 --- a/public/app/features/variables/utils.ts +++ b/public/app/features/variables/utils.ts @@ -12,9 +12,9 @@ import { } from '@grafana/data'; import { getTemplateSrv, locationService } from '@grafana/runtime'; import { safeStringifyValue } from 'app/core/utils/explore'; +import { StoreState } from 'app/types/store'; import { getState } from '../../store/store'; -import { StoreState } from '../../types'; import { TimeSrv } from '../dashboard/services/TimeSrv'; import { variableAdapters } from './adapters'; diff --git a/public/app/plugins/datasource/azuremonitor/module.ts b/public/app/plugins/datasource/azuremonitor/module.ts index bf96a78c1b3..b723a0bd50c 100644 --- a/public/app/plugins/datasource/azuremonitor/module.ts +++ b/public/app/plugins/datasource/azuremonitor/module.ts @@ -10,7 +10,11 @@ import { trackAzureMonitorDashboardLoaded } from './tracking'; import { AzureMonitorQuery, AzureQueryType, ResultFormat } from './types/query'; import { AzureMonitorDataSourceJsonData } from './types/types'; -initPluginTranslations(pluginJson.id); +// don't load plugin translations in test environments +// we don't use them anyway, and top-level await won't work currently in jest +if (process.env.NODE_ENV !== 'test') { + await initPluginTranslations(pluginJson.id); +} export const plugin = new DataSourcePlugin<Datasource, AzureMonitorQuery, AzureMonitorDataSourceJsonData>(Datasource) .setConfigEditor(ConfigEditor) diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 1cdbd9eae7b..2619deb5e98 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -48,9 +48,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "i18n-extract": "i18next --config locales/i18next-parser.config.cjs" }, "packageManager": "yarn@4.9.2" diff --git a/public/app/plugins/datasource/azuremonitor/webpack.config.ts b/public/app/plugins/datasource/azuremonitor/webpack.config.ts index c831a42f307..875ef0a4893 100644 --- a/public/app/plugins/datasource/azuremonitor/webpack.config.ts +++ b/public/app/plugins/datasource/azuremonitor/webpack.config.ts @@ -1,9 +1,9 @@ import type { Configuration } from 'webpack'; import { merge } from 'webpack-merge'; -import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; -const config = async (env: Record<string, unknown>): Promise<Configuration> => { +const config = async (env: Env): Promise<Configuration> => { const baseConfig = await grafanaConfig(env); return merge(baseConfig, { diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index dcbf4c251d7..f476c47156c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "12.1.0-pre", - "@grafana/google-sdk": "0.3.2", + "@grafana/google-sdk": "0.3.4", "@grafana/plugin-ui": "0.10.7", "@grafana/runtime": "12.1.0-pre", "@grafana/schema": "12.1.0-pre", @@ -48,9 +48,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts b/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts index 4da5a990cfa..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts +++ b/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts @@ -1,3 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts index 38c466afa2b..79e2fddbc2d 100644 --- a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts +++ b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts @@ -2,7 +2,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import { Action } from 'redux'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; type GrafanaReducer<S = StoreState, A extends Action = AnyAction> = (state: S, action: A) => S; diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index cc2fde9a4e3..2acfab5815d 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -33,9 +33,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts b/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts index 55a880fef5c..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts @@ -1,4 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; // eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 65be0f55f6c..3fcf2bb95c9 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -42,9 +42,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/webpack.config.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/webpack.config.ts index 8e03497ddb2..f48f8b1a693 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/webpack.config.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/webpack.config.ts @@ -1,6 +1,6 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; -const configWithFallback = async (env: Record<string, unknown>) => { +const configWithFallback = async (env: Env) => { const response = await config(env); if (response !== undefined && response.resolve !== undefined) { response.resolve.fallback = { diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 5d1fa9adc72..1c71f41c562 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -41,9 +41,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts b/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts index 4da5a990cfa..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts @@ -1,3 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx index db5f9172a67..9e72fbb491e 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx @@ -36,55 +36,55 @@ describe('UrlAndAuthenticationSection', () => { expect(onOptionsChangeMock).toHaveBeenCalled(); }); - it('renders DRBP warning for InfluxDB OSS 1.x and InfluxQL', () => { - const props = { - ...defaultProps, - options: { - ...defaultProps.options, - jsonData: { product: 'InfluxDB OSS 1.x', version: InfluxVersion.InfluxQL }, - }, - }; + const productsRequiringDBRP = [ + 'InfluxDB OSS 1.x', + 'InfluxDB OSS 2.x', + 'InfluxDB Enterprise 1.x', + 'InfluxDB Cloud (TSM)', + 'InfluxDB Cloud Serverless', + ]; - render(<UrlAndAuthenticationSection {...props} />); - expect(screen.getByText(/requires DRBP mapping/i)).toBeInTheDocument(); + describe('UrlAndAuthenticationSection', () => { + it.each(productsRequiringDBRP)('renders DBRP warning for %s and InfluxQL', (product) => { + const props = { + ...defaultProps, + options: { + ...defaultProps.options, + jsonData: { + product, + version: InfluxVersion.InfluxQL, + }, + }, + }; + + render(<UrlAndAuthenticationSection {...props} />); + expect(screen.getByText(/requires DBRP mapping/i)).toBeInTheDocument(); + }); }); - it('renders DRBP warning for InfluxDB OSS 2.x and InfluxQL', () => { + it('does not render DBRP warning for SQL', () => { const props = { ...defaultProps, options: { ...defaultProps.options, - jsonData: { product: 'InfluxDB OSS 2.x', version: InfluxVersion.InfluxQL }, + jsonData: { version: InfluxVersion.SQL }, }, }; render(<UrlAndAuthenticationSection {...props} />); - expect(screen.getByText(/requires DRBP mapping/i)).toBeInTheDocument(); + expect(screen.queryByText(/requires DBRP mapping/i)).not.toBeInTheDocument(); }); - it('does not render DRBP warning for InfluxDB OSS 1.x and Flux', () => { + it('does not render DBRP warning for Flux', () => { const props = { ...defaultProps, options: { ...defaultProps.options, - jsonData: { product: 'InfluxDB OSS 1.x', version: InfluxVersion.Flux }, + jsonData: { version: InfluxVersion.Flux }, }, }; render(<UrlAndAuthenticationSection {...props} />); - expect(screen.queryByText(/requires DRBP mapping/i)).not.toBeInTheDocument(); - }); - - it('does not render DRBP warning for InfluxDB OSS 2.x and Flux', () => { - const props = { - ...defaultProps, - options: { - ...defaultProps.options, - jsonData: { product: 'InfluxDB OSS 2.x', version: InfluxVersion.Flux }, - }, - }; - - render(<UrlAndAuthenticationSection {...props} />); - expect(screen.queryByText(/requires DRBP mapping/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/requires DBRP mapping/i)).not.toBeInTheDocument(); }); }); diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx index 7b0ff3fcd69..263e0b45865 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx @@ -38,10 +38,16 @@ export const UrlAndAuthenticationSection = (props: Props) => { typeof v === 'string' && (v === InfluxVersion.Flux || v === InfluxVersion.InfluxQL || v === InfluxVersion.SQL); // Database + Retention Policy (DBRP) mapping is required for InfluxDB OSS 1.x and 2.x when using InfluxQL - const requiresDrbpMapping = + const requiresDbrpMapping = options.jsonData.product && options.jsonData.version === InfluxVersion.InfluxQL && - ['InfluxDB OSS 1.x', 'InfluxDB OSS 2.x'].includes(options.jsonData.product); + [ + 'InfluxDB OSS 1.x', + 'InfluxDB OSS 2.x', + 'InfluxDB Enterprise 1.x', + 'InfluxDB Cloud (TSM)', + 'InfluxDB Cloud Serverless', + ].includes(options.jsonData.product); const onProductChange = ({ value }: ComboboxOption) => { trackInfluxDBConfigV2ProductSelected({ product: value }); @@ -113,8 +119,8 @@ export const UrlAndAuthenticationSection = (props: Props) => { <Space v={2} /> - {requiresDrbpMapping && ( - <Alert severity="warning" title="InfluxQL requires DRBP mapping"> + {requiresDbrpMapping && ( + <Alert severity="warning" title="InfluxQL requires DBRP mapping"> InfluxDB OSS 1.x and 2.x users must configure a Database + Retention Policy (DBRP) mapping via the CLI or API before data can be queried.{' '} <TextLink href="https://docs.influxdata.com/influxdb/cloud/query-data/influxql/dbrp/" external> diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts b/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts index 3ae3122bffd..07b4b3fc35e 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts @@ -30,8 +30,8 @@ export const CONFIG_SECTION_HEADERS_WITH_PDC = [ ]; export const HTTP_MODES: ComboboxOption[] = [ - { label: 'GET', value: 'GET' }, { label: 'POST', value: 'POST' }, + { label: 'GET', value: 'GET' }, ]; export const getInlineLabelStyles = (theme: GrafanaTheme2, transparent = false, width?: number | 'auto') => { diff --git a/public/app/plugins/datasource/jaeger/datasource.test.ts b/public/app/plugins/datasource/jaeger/datasource.test.ts index 2fa37357816..5c0961d4ac7 100644 --- a/public/app/plugins/datasource/jaeger/datasource.test.ts +++ b/public/app/plugins/datasource/jaeger/datasource.test.ts @@ -1,4 +1,4 @@ -import { lastValueFrom, of, throwError } from 'rxjs'; +import { lastValueFrom, of } from 'rxjs'; import { DataQueryRequest, @@ -9,18 +9,12 @@ import { PluginType, ScopedVars, } from '@grafana/data'; -import { BackendSrv, config, DataSourceWithBackend } from '@grafana/runtime'; +import { BackendSrv, DataSourceWithBackend } from '@grafana/runtime'; -import { ALL_OPERATIONS_KEY } from './components/SearchForm'; import { JaegerDatasource, JaegerJsonData } from './datasource'; -import { createFetchResponse } from './helpers/createFetchResponse'; import mockJson from './mockJsonResponse.json'; -import { - testResponse, - testResponseDataFrameFields, - testResponseEdgesFields, - testResponseNodesFields, -} from './testResponse'; +import mockSearchResponse from './mockSearchResponse.json'; +import mockTraceResponse from './mockTraceResponse.json'; import { JaegerQuery } from './types'; export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv; @@ -38,128 +32,29 @@ jest.mock('@grafana/runtime', () => ({ }), })); -const defaultQuery: DataQueryRequest<JaegerQuery> = { - requestId: '1', - interval: '0', - intervalMs: 10, - panelId: 0, - scopedVars: {}, - range: { - from: dateTime().subtract(1, 'h'), - to: dateTime(), - raw: { from: '1h', to: 'now' }, - }, - timezone: 'browser', - app: 'explore', - startTime: 0, - targets: [ - { - query: '12345', - refId: '1', - }, - ], -}; - -describe('JaegerDatasource', () => { - const defaultSearchRangeParams = `start=${Number(defaultQuery.range.from) * 1000}&end=${Number(defaultQuery.range.to) * 1000}`; - - beforeEach(() => { - jest.clearAllMocks(); - - const fetchMock = jest.spyOn(Date, 'now'); - fetchMock.mockImplementation(() => 1704106800000); // milliseconds for 2024-01-01 at 11:00am UTC - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('returns trace and graph when queried', async () => { - setupFetchMock({ data: [testResponse] }); - - const ds = new JaegerDatasource(defaultSettings); - const response = await lastValueFrom(ds.query(defaultQuery)); - expect(response.data.length).toBe(3); - expect(response.data[0].fields).toMatchObject(testResponseDataFrameFields); - expect(response.data[1].fields).toMatchObject(testResponseNodesFields); - expect(response.data[2].fields).toMatchObject(testResponseEdgesFields); - }); - - it('returns trace when traceId with special characters is queried', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const query = { - ...defaultQuery, - targets: [ - { - query: 'a/b', - refId: '1', - }, - ], - }; - await lastValueFrom(ds.query(query)); - expect(mock).toHaveBeenCalledWith({ url: `${defaultSettings.url}/api/traces/a%2Fb` }); - }); - - it('should trim whitespace from traceid', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const query = { - ...defaultQuery, - targets: [ - { - query: 'a/b ', - refId: '1', - }, - ], - }; - await lastValueFrom(ds.query(query)); - expect(mock).toHaveBeenCalledWith({ url: `${defaultSettings.url}/api/traces/a%2Fb` }); - }); - - it('returns empty response if trace id is not specified', async () => { - const ds = new JaegerDatasource(defaultSettings); - const response = await lastValueFrom( - ds.query({ - ...defaultQuery, - targets: [], - }) - ); - const field = response.data[0].fields[0]; - expect(field.name).toBe('trace'); - expect(field.type).toBe(FieldType.trace); - expect(field.values.length).toBe(0); - }); - - it('should handle json file upload', async () => { +describe('upload, search and trace query types', () => { + it('should process valid JSON file uploads', async () => { const ds = new JaegerDatasource(defaultSettings); ds.uploadedJson = JSON.stringify(mockJson); - const response = await lastValueFrom( - ds.query({ - ...defaultQuery, - targets: [{ queryType: 'upload', refId: 'A' }], - }) - ); + const response = await lastValueFrom(ds.query({ ...defaultQuery, targets: [{ queryType: 'upload', refId: 'A' }] })); const field = response.data[0].fields[0]; expect(field.name).toBe('traceID'); expect(field.type).toBe(FieldType.string); expect(field.values.length).toBe(2); }); - it('should fail on invalid json file upload', async () => { + it('should reject invalid JSON file uploads', async () => { const ds = new JaegerDatasource(defaultSettings); ds.uploadedJson = JSON.stringify({ key: 'value', arr: [] }); const response = await lastValueFrom( - ds.query({ - targets: [{ queryType: 'upload', refId: 'A' }], - } as DataQueryRequest<JaegerQuery>) + ds.query({ targets: [{ queryType: 'upload', refId: 'A' }] } as DataQueryRequest<JaegerQuery>) ); expect(response.error?.message).toBe('The JSON file uploaded is not in a valid Jaeger format'); expect(response.data.length).toBe(0); }); - it('should return search results when the query type is search', async () => { - const mock = setupFetchMock({ data: [testResponse] }); + it('should return search results when query type is search', async () => { + setupQueryMock('search'); const ds = new JaegerDatasource(defaultSettings); const response = await lastValueFrom( ds.query({ @@ -167,185 +62,66 @@ describe('JaegerDatasource', () => { targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: '/api/services' }], }) ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&operation=%2Fapi%2Fservices&${defaultSearchRangeParams}&lookback=custom`, - }); + expect(response.data[0].meta.preferredVisualisationType).toBe('table'); - // Make sure that traceID field has data link configured expect(response.data[0].fields[0].config.links).toHaveLength(1); expect(response.data[0].fields[0].name).toBe('traceID'); }); - it('uses default range when no range is provided for search query,', async () => { - const mock = setupFetchMock({ data: [testResponse] }); + it('should return trace results when query type is trace', async () => { + setupQueryMock('trace'); const ds = new JaegerDatasource(defaultSettings); - const query = { - ...defaultQuery, - targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: ALL_OPERATIONS_KEY }], - // set range to undefined to test default range - range: undefined, - } as unknown as DataQueryRequest<JaegerQuery>; + const response = await lastValueFrom( + ds.query({ ...defaultQuery, targets: [{ queryType: undefined, refId: 'a', query: '12345' }] }) + ); - ds.query(query); - expect(mock).toHaveBeenCalledWith({ - // Check that query has time range from 6 hours ago to now (default range) - url: `${defaultSettings.url}/api/traces?service=jaeger-query&start=1704085200000000&end=1704106800000000&lookback=custom`, - }); + expect(response.data[0].meta.preferredVisualisationType).toBe('trace'); + expect(response.data[0].fields.length).toBe(7); }); +}); + +describe('node graph functionality', () => { + it('should include node graph frames when nodeGraph is enabled for trace queries', async () => { + const settingsWithNodeGraph = { + ...defaultSettings, + jsonData: { + ...defaultSettings.jsonData, + nodeGraph: { enabled: true }, + }, + }; + + const ds = new JaegerDatasource(settingsWithNodeGraph); + setupQueryMock('trace'); - it('should show the correct error message if no service name is selected', async () => { - const ds = new JaegerDatasource(defaultSettings); const response = await lastValueFrom( ds.query({ ...defaultQuery, - targets: [{ queryType: 'search', refId: 'a', service: undefined, operation: '/api/services' }], - }) - ); - expect(response.error?.message).toBe('You must select a service.'); - }); - - it('should remove operation from the query when all is selected', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - await lastValueFrom( - ds.query({ - ...defaultQuery, - targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: ALL_OPERATIONS_KEY }], - }) - ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&${defaultSearchRangeParams}&lookback=custom`, - }); - }); - - it('should convert tags from logfmt format to an object', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - await lastValueFrom( - ds.query({ - ...defaultQuery, - targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', tags: 'error=true' }], - }) - ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&${defaultSearchRangeParams}&lookback=custom`, - }); - }); - - it('should resolve templates in traceID', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - - await lastValueFrom( - ds.query({ - ...defaultQuery, - scopedVars: { - $traceid: { - text: 'traceid', - value: '5311b0dd0ca8df3463df93c99cb805a6', - }, - }, targets: [ { - query: '$traceid', + query: '12345', refId: '1', }, ], }) ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces/5311b0dd0ca8df3463df93c99cb805a6`, - }); + + expect(response.data.length).toBe(3); }); - it('should resolve templates in tags', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - await lastValueFrom( + it('should exclude node graph frames when nodeGraph is disabled for trace queries', async () => { + const settingsWithoutNodeGraph = { + ...defaultSettings, + jsonData: { + ...defaultSettings.jsonData, + nodeGraph: { enabled: false }, + }, + }; + + const ds = new JaegerDatasource(settingsWithoutNodeGraph); + setupQueryMock('trace'); + + const response = await lastValueFrom( ds.query({ - ...defaultQuery, - scopedVars: { - 'error=$error': { - text: 'error', - value: 'error=true', - }, - }, - targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', tags: 'error=$error' }], - }) - ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&${defaultSearchRangeParams}&lookback=custom`, - }); - }); - - it('should interpolate variables correctly', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const text = 'interpolationText'; - await lastValueFrom( - ds.query({ - ...defaultQuery, - scopedVars: { - $interpolationVar: { - text: text, - value: text, - }, - }, - targets: [ - { - queryType: 'search', - refId: 'a', - service: '$interpolationVar', - operation: '$interpolationVar', - minDuration: '$interpolationVar', - maxDuration: '$interpolationVar', - }, - ], - }) - ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=interpolationText&operation=interpolationText&minDuration=interpolationText&maxDuration=interpolationText&${defaultSearchRangeParams}&lookback=custom`, - }); - }); - - describe('when jaegerBackendMigration feature toggle is enabled', () => { - let originalFeatureToggleValue: boolean | undefined; - - beforeEach(() => { - originalFeatureToggleValue = config.featureToggles.jaegerBackendMigration; - config.featureToggles.jaegerBackendMigration = true; - }); - - afterEach(() => { - config.featureToggles.jaegerBackendMigration = originalFeatureToggleValue; - }); - - it('should add node graph frames to response when nodeGraph is enabled and query is a trace ID query', async () => { - // Create a datasource with nodeGraph enabled - const settings = { - ...defaultSettings, - jsonData: { - ...defaultSettings.jsonData, - nodeGraph: { enabled: true }, - }, - }; - - const ds = new JaegerDatasource(settings); - - // Mock the super.query method to return our mock response - jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => { - return of({ - data: [ - { - fields: testResponseDataFrameFields, - values: testResponseDataFrameFields.values, - }, - ], - }); - }); - - // Create a query without queryType (trace ID query) - const query = { ...defaultQuery, targets: [ { @@ -353,189 +129,31 @@ describe('JaegerDatasource', () => { refId: '1', }, ], - }; + }) + ); - // Execute the query - const response = await lastValueFrom(ds.query(query)); - // Verify that the response contains the original data plus node graph frames - expect(response.data.length).toBe(3); - }); - - it('should not add node graph frames when nodeGraph is disabled', async () => { - // Create a datasource with nodeGraph disabled - const settings = { - ...defaultSettings, - jsonData: { - ...defaultSettings.jsonData, - nodeGraph: { enabled: false }, - }, - }; - - const ds = new JaegerDatasource(settings); - - // Mock the super.query method to return our mock response - jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => { - return of({ - data: [ - { - fields: testResponseDataFrameFields, - values: testResponseDataFrameFields.values, - }, - ], - }); - }); - - // Create a query without queryType (trace ID query) - const query = { - ...defaultQuery, - targets: [ - { - query: '12345', - refId: '1', - }, - ], - }; - - // Execute the query - const response = await lastValueFrom(ds.query(query)); - // Verify that the response contains only the original data - expect(response.data.length).toBe(1); - expect(response.data[0].fields).toMatchObject(testResponseDataFrameFields); - }); + expect(response.data.length).toBe(1); }); }); -describe('when performing testDataSource', () => { - describe('and call succeeds', () => { - it('should return successfully', async () => { - setupFetchMock({ data: ['service1'] }); - - const ds = new JaegerDatasource(defaultSettings); - const response = await ds.testDatasource(); - expect(response.status).toEqual('success'); - expect(response.message).toBe('Data source connected and services found.'); - }); - }); - - describe('and call succeeds, but returns no services', () => { - it('should display an error', async () => { - setupFetchMock(undefined); - - const ds = new JaegerDatasource(defaultSettings); - const response = await ds.testDatasource(); - expect(response.status).toEqual('error'); - expect(response.message).toBe( - 'Data source connected, but no services received. Verify that Jaeger is configured properly.' - ); - }); - }); - - describe('and call returns error with message', () => { - it('should return the formatted error', async () => { - setupFetchMock( - undefined, - throwError({ - statusText: 'Not found', - status: 404, - data: { - message: '404 page not found', - }, - }) - ); - - const ds = new JaegerDatasource(defaultSettings); - const response = await ds.testDatasource(); - expect(response.status).toEqual('error'); - expect(response.message).toBe('Jaeger: Not found. 404. 404 page not found'); - }); - }); - - describe('and call returns error without message', () => { - it('should return JSON error', async () => { - setupFetchMock( - undefined, - throwError({ - statusText: 'Bad gateway', - status: 502, - data: { - errors: ['Could not connect to Jaeger backend'], - }, - }) - ); - - const ds = new JaegerDatasource(defaultSettings); - const response = await ds.testDatasource(); - expect(response.status).toEqual('error'); - expect(response.message).toBe('Jaeger: Bad gateway. 502. {"errors":["Could not connect to Jaeger backend"]}'); - }); - }); -}); - -describe('Test behavior with unmocked time', () => { - // Tolerance for checking timestamps. - // Using a lower number seems to cause flaky tests. - const numDigits = -4; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('getTimeRange()', async () => { +describe('time range', () => { + it('should calculate correct time range', async () => { const ds = new JaegerDatasource(defaultSettings); const timeRange = ds.getTimeRange(); const now = Date.now(); - expect(timeRange.end).toBeCloseTo(now * 1000, numDigits); - expect(timeRange.start).toBeCloseTo((now - 6 * 3600 * 1000) * 1000, numDigits); - }); - - it("call for `query()` when `queryType === 'dependencyGraph'`", async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const now = Date.now(); - - ds.query({ ...defaultQuery, targets: [{ queryType: 'dependencyGraph', refId: '1' }] }); - - const url = mock.mock.calls[0][0].url; - const endTsMatch = url.match(/endTs=(\d+)/); - expect(endTsMatch).not.toBeNull(); - expect(parseInt(endTsMatch![1], 10)).toBeCloseTo(now, numDigits); - - const lookbackMatch = url.match(/lookback=(\d+)/); - expect(lookbackMatch).not.toBeNull(); - expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(3600000, -1); // due to rounding, the least significant digit is not reliable - }); - - it("call for `query()` when `queryType === 'dependencyGraph'`, using default range", async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const now = Date.now(); - const query = JSON.parse(JSON.stringify(defaultQuery)); - // @ts-ignore - query.range = undefined; - - ds.query({ ...query, targets: [{ queryType: 'dependencyGraph', refId: '1' }] }); - - const url = mock.mock.calls[0][0].url; - const endTsMatch = url.match(/endTs=(\d+)/); - expect(endTsMatch).not.toBeNull(); - expect(parseInt(endTsMatch![1], 10)).toBeCloseTo(now, numDigits); - - const lookbackMatch = url.match(/lookback=(\d+)/); - expect(lookbackMatch).not.toBeNull(); - expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(21600000, -1); + expect(timeRange.end).toBeCloseTo(now * 1000, -4); + expect(timeRange.start).toBeCloseTo((now - 6 * 3600 * 1000) * 1000, -4); }); }); -function setupFetchMock(response: unknown, mock?: ReturnType<typeof backendSrv.fetch>) { - const defaultMock = () => mock ?? of(createFetchResponse(response)); - - const fetchMock = jest.spyOn(backendSrv, 'fetch'); - fetchMock.mockImplementation(defaultMock); - return fetchMock; +function setupQueryMock(type: 'trace' | 'search') { + return jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => { + if (type === 'search') { + return of(mockSearchResponse); + } else { + return of(mockTraceResponse); + } + }); } const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = { @@ -560,3 +178,25 @@ const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = { }, readOnly: false, }; + +const defaultQuery: DataQueryRequest<JaegerQuery> = { + requestId: '1', + interval: '0', + intervalMs: 10, + panelId: 0, + scopedVars: {}, + range: { + from: dateTime().subtract(1, 'h'), + to: dateTime(), + raw: { from: '1h', to: 'now' }, + }, + timezone: 'browser', + app: 'explore', + startTime: 0, + targets: [ + { + query: '12345', + refId: '1', + }, + ], +}; diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index 6aac84c3ca0..11c6a8a3f3b 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -1,6 +1,5 @@ -import { identity, omit, pick, pickBy } from 'lodash'; -import { lastValueFrom, Observable, of } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; import { DataQueryRequest, @@ -14,25 +13,14 @@ import { MutableDataFrame, ScopedVars, toDataFrame, - urlUtil, } from '@grafana/data'; import { createNodeGraphFrames, NodeGraphOptions, SpanBarOptions } from '@grafana/o11y-ds-frontend'; -import { - BackendSrvRequest, - config, - DataSourceWithBackend, - getBackendSrv, - getTemplateSrv, - TemplateSrv, -} from '@grafana/runtime'; +import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { ALL_OPERATIONS_KEY } from './components/SearchForm'; import { TraceIdTimeParamsOptions } from './configuration/TraceIdTimeParams'; -import { mapJaegerDependenciesResponse } from './dependencyGraphTransform'; import { createGraphFrames } from './graphTransform'; -import { createTableFrame, createTraceFrame } from './responseTransform'; +import { createTraceFrame } from './responseTransform'; import { JaegerQuery } from './types'; -import { convertTagsLogfmt } from './util'; export interface JaegerJsonData extends DataSourceJsonData { nodeGraph?: NodeGraphOptions; @@ -45,7 +33,7 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ traceIdTimeParams?: TraceIdTimeParamsOptions; spanBar?: SpanBarOptions; constructor( - private instanceSettings: DataSourceInstanceSettings<JaegerJsonData>, + instanceSettings: DataSourceInstanceSettings<JaegerJsonData>, private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); @@ -53,25 +41,14 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ this.traceIdTimeParams = instanceSettings.jsonData.traceIdTimeParams; } - /** - * Migrated to backend with feature toggle `jaegerBackendMigration` - */ async metadataRequest(url: string, params?: Record<string, unknown>) { - if (config.featureToggles.jaegerBackendMigration) { - return await this.getResource(url, params); - } - - const res = await lastValueFrom(this._request('/api/' + url, params, { hideFromInspector: true })); - return res.data.data; + return await this.getResource(url, params); } isSearchFormValid(query: JaegerQuery): boolean { return !!query.service; } - /** - * Migrated to backend with feature toggle `jaegerBackendMigration` - */ query(options: DataQueryRequest<JaegerQuery>): Observable<DataQueryResponse> { // At this moment we expect only one target. In case we somehow change the UI to be able to show multiple // traces at one we need to change this. @@ -80,55 +57,6 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ return of({ data: [emptyTraceDataFrame] }); } - if (config.featureToggles.jaegerBackendMigration && target.queryType !== 'upload') { - return super.query({ ...options, targets: [target] }).pipe( - map((response) => { - // If the node graph is enabled and the query is a trace ID query, add the node graph frames to the response - if (this.nodeGraph?.enabled && !target.queryType) { - return addNodeGraphFramesToResponse(response); - } - return response; - }) - ); - } - - // Use the internal Jaeger /dependencies API for rendering the dependency graph. - if (target.queryType === 'dependencyGraph') { - const timeRange = options.range ?? getDefaultTimeRange(); - const endTs = getTime(timeRange.to, true) / 1000; - const lookback = endTs - getTime(timeRange.from, false) / 1000; - return this._request('/api/dependencies', { endTs, lookback }).pipe(map(mapJaegerDependenciesResponse)); - } - - if (target.queryType === 'search' && !this.isSearchFormValid(target)) { - return of({ error: { message: 'You must select a service.' }, data: [] }); - } - - let { start, end } = this.getTimeRange(options.range); - - if (target.queryType !== 'search' && target.query) { - let url = `/api/traces/${encodeURIComponent(this.templateSrv.replace(target.query.trim(), options.scopedVars))}`; - if (this.traceIdTimeParams) { - url += `?start=${start}&end=${end}`; - } - - return this._request(url).pipe( - map((response) => { - const traceData = response?.data?.data?.[0]; - if (!traceData) { - return { data: [emptyTraceDataFrame] }; - } - let data = [createTraceFrame(traceData)]; - if (this.nodeGraph?.enabled) { - data.push(...createGraphFrames(traceData)); - } - return { - data, - }; - }) - ); - } - if (target.queryType === 'upload') { if (!this.uploadedJson) { return of({ data: [] }); @@ -146,38 +74,13 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ } } - let jaegerInterpolated = pick(this.applyTemplateVariables(target, options.scopedVars), [ - 'service', - 'operation', - 'tags', - 'minDuration', - 'maxDuration', - 'limit', - ]); - // remove empty properties - let jaegerQuery = pickBy(jaegerInterpolated, identity); - - if (jaegerQuery.operation === ALL_OPERATIONS_KEY) { - jaegerQuery = omit(jaegerQuery, 'operation'); - } - - if (jaegerQuery.tags) { - jaegerQuery = { - ...jaegerQuery, - tags: convertTagsLogfmt(jaegerQuery.tags.toString()), - }; - } - - // TODO: this api is internal, used in jaeger ui. Officially they have gRPC api that should be used. - return this._request(`/api/traces`, { - ...jaegerQuery, - ...this.getTimeRange(options.range), - lookback: 'custom', - }).pipe( + return super.query({ ...options, targets: [target] }).pipe( map((response) => { - return { - data: [createTableFrame(response.data.data, this.instanceSettings)], - }; + // If the node graph is enabled and the query is a trace ID query, add the node graph frames to the response + if (this.nodeGraph?.enabled && !target.queryType) { + return addNodeGraphFramesToResponse(response); + } + return response; }) ); } @@ -215,49 +118,8 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ }; } - /** - * Migrated to backend with feature toggle `jaegerBackendMigration` - */ async testDatasource() { - if (config.featureToggles.jaegerBackendMigration) { - return await super.testDatasource(); - } - - return lastValueFrom( - this._request('/api/services').pipe( - map((res) => { - const values = res?.data?.data || []; - const testResult = - values.length > 0 - ? { status: 'success', message: 'Data source connected and services found.' } - : { - status: 'error', - message: - 'Data source connected, but no services received. Verify that Jaeger is configured properly.', - }; - return testResult; - }), - catchError((err) => { - let message = 'Jaeger: '; - if (err.statusText) { - message += err.statusText; - } else { - message += 'Cannot connect to Jaeger'; - } - - if (err.status) { - message += `. ${err.status}`; - } - - if (err.data && err.data.message) { - message += `. ${err.data.message}`; - } else if (err.data) { - message += `. ${JSON.stringify(err.data)}`; - } - return of({ status: 'error', message: message }); - }) - ) - ); + return await super.testDatasource(); } getTimeRange(range = getDefaultTimeRange()): { start: number; end: number } { @@ -270,21 +132,6 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ getQueryDisplayText(query: JaegerQuery) { return query.query || ''; } - - private _request( - apiUrl: string, - data?: Record<string, unknown>, - options?: Partial<BackendSrvRequest> - ): Observable<Record<string, any>> { - const params = data ? urlUtil.serializeParams(data) : ''; - const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; - const req = { - ...options, - url, - }; - - return getBackendSrv().fetch(req); - } } function getTime(date: string | DateTime, roundUp: boolean) { diff --git a/public/app/plugins/datasource/jaeger/mockSearchResponse.json b/public/app/plugins/datasource/jaeger/mockSearchResponse.json new file mode 100644 index 00000000000..7ce11be4aea --- /dev/null +++ b/public/app/plugins/datasource/jaeger/mockSearchResponse.json @@ -0,0 +1,52 @@ +{ + "data": [ + { + "fields": [ + { + "name": "traceID", + "values": ["test-trace-id"], + "config": { + "displayName": "Trace ID", + "links": [ + { + "title": "Trace: ${__value.raw}", + "internal": { + "query": { + "query": "${__value.raw}" + }, + "datasourceUid": "test-uid", + "datasourceName": "test-name" + } + } + ] + } + }, + { + "name": "traceName", + "values": ["test-service: test-operation"], + "config": { + "displayName": "Trace name" + } + }, + { + "name": "startTime", + "values": [1605873894680], + "config": { + "displayName": "Start time" + } + }, + { + "name": "duration", + "values": [1000], + "config": { + "displayName": "Duration", + "unit": "µs" + } + } + ], + "meta": { + "preferredVisualisationType": "table" + } + } + ] +} diff --git a/public/app/plugins/datasource/jaeger/mockTraceResponse.json b/public/app/plugins/datasource/jaeger/mockTraceResponse.json new file mode 100644 index 00000000000..08867e2197f --- /dev/null +++ b/public/app/plugins/datasource/jaeger/mockTraceResponse.json @@ -0,0 +1,21 @@ +{ + "data": [ + { + "fields": [ + { "name": "traceID", "values": ["3fa414edcef6ad90", "3fa414edcef6ad90"] }, + { "name": "spanID", "values": ["3fa414edcef6ad90", "0f5c1808567e4403"] }, + { "name": "parentSpanID", "values": [null, "3fa414edcef6ad90"] }, + { "name": "operationName", "values": ["HTTP GET - api_traces_traceid", "/tempopb.Querier/FindTraceByID"] }, + { "name": "serviceName", "values": ["tempo-querier", "tempo-querier"] }, + { "name": "startTime", "values": [1605873894680.409, 1605873894680.587] }, + { "name": "duration", "values": [1049.141, 1.847] } + ], + "meta": { + "preferredVisualisationType": "trace", + "custom": { + "traceFormat": "jaeger" + } + } + } + ] +} diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 383c155d43c..d32fdd415c7 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -44,9 +44,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/jaeger/webpack.config.ts b/public/app/plugins/datasource/jaeger/webpack.config.ts index 10ef5bd69a2..0346a64c5f2 100644 --- a/public/app/plugins/datasource/jaeger/webpack.config.ts +++ b/public/app/plugins/datasource/jaeger/webpack.config.ts @@ -1,6 +1,10 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import { createRequire } from 'node:module'; -const configWithFallback = async (env: Record<string, unknown>) => { +import config, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; + +const require = createRequire(import.meta.url); + +const configWithFallback = async (env: Env) => { const response = await config(env); if (response !== undefined && response.resolve !== undefined) { response.resolve.fallback = { diff --git a/public/app/plugins/datasource/loki/package.json b/public/app/plugins/datasource/loki/package.json index 0b4be6cada6..3cd58989823 100644 --- a/public/app/plugins/datasource/loki/package.json +++ b/public/app/plugins/datasource/loki/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "12.1.0-pre", - "@grafana/lezer-logql": "0.2.7", + "@grafana/lezer-logql": "0.2.8", "@grafana/llm": "0.22.1", "@grafana/monaco-logql": "^0.0.8", "@grafana/runtime": "12.1.0-pre", @@ -44,9 +44,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/loki/webpack.config.ts b/public/app/plugins/datasource/loki/webpack.config.ts index 55a880fef5c..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/loki/webpack.config.ts +++ b/public/app/plugins/datasource/loki/webpack.config.ts @@ -1,4 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; // eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/mssql/module.ts b/public/app/plugins/datasource/mssql/module.ts index 7cf4eeaafe1..7ba2f9c512f 100644 --- a/public/app/plugins/datasource/mssql/module.ts +++ b/public/app/plugins/datasource/mssql/module.ts @@ -8,7 +8,11 @@ import { MssqlDatasource } from './datasource'; import pluginJson from './plugin.json'; import { MssqlOptions } from './types'; -initPluginTranslations(pluginJson.id, [loadSQLResources]); +// don't load plugin translations in test environments +// we don't use them anyway, and top-level await won't work currently in jest +if (process.env.NODE_ENV !== 'test') { + await initPluginTranslations(pluginJson.id, [loadSQLResources]); +} export const plugin = new DataSourcePlugin<MssqlDatasource, SQLQuery, MssqlOptions>(MssqlDatasource) .setQueryEditor(SqlQueryEditorLazy) diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index fa734fad0f5..7b8f678c101 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -35,9 +35,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "i18n-extract": "i18next --config locales/i18next-parser.config.cjs" }, "packageManager": "yarn@4.9.2" diff --git a/public/app/plugins/datasource/mssql/webpack.config.ts b/public/app/plugins/datasource/mssql/webpack.config.ts index 2e981f9ea7c..8a119f88315 100644 --- a/public/app/plugins/datasource/mssql/webpack.config.ts +++ b/public/app/plugins/datasource/mssql/webpack.config.ts @@ -1,9 +1,9 @@ import type { Configuration } from 'webpack'; import { merge } from 'webpack-merge'; -import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; -const config = async (env: Record<string, unknown>): Promise<Configuration> => { +const config = async (env: Env): Promise<Configuration> => { const baseConfig = await grafanaConfig(env); return merge(baseConfig, { diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index 78c0d5c95d3..71f24047aa4 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -33,9 +33,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/mysql/webpack.config.ts b/public/app/plugins/datasource/mysql/webpack.config.ts index 55a880fef5c..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/mysql/webpack.config.ts +++ b/public/app/plugins/datasource/mysql/webpack.config.ts @@ -1,4 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; // eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 18c7a72fad7..5690290bb3a 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -34,9 +34,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/parca/webpack.config.ts b/public/app/plugins/datasource/parca/webpack.config.ts index 4da5a990cfa..31247bb11af 100644 --- a/public/app/plugins/datasource/parca/webpack.config.ts +++ b/public/app/plugins/datasource/parca/webpack.config.ts @@ -1,3 +1,3 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; export default config; diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index e7ac0f7c247..469228c1ce9 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -51,7 +51,7 @@ "@types/react-dom": "18.3.5", "@types/semver": "7.7.0", "@types/uuid": "10.0.0", - "glob": "11.0.1", + "glob": "11.0.3", "react-select-event": "5.5.1", "ts-node": "10.9.2", "typescript": "5.8.3", @@ -61,10 +61,10 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", "build:stats": "yarn build --env stats --profile --json=compilation-stats.json", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/tempo/webpack.config.ts b/public/app/plugins/datasource/tempo/webpack.config.ts index 4da5a990cfa..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/tempo/webpack.config.ts +++ b/public/app/plugins/datasource/tempo/webpack.config.ts @@ -1,3 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index d3e3633e78d..94a2c65a18e 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -37,9 +37,9 @@ "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "webpack -w -c ./webpack.config.ts --env development" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/zipkin/webpack.config.ts b/public/app/plugins/datasource/zipkin/webpack.config.ts index 4da5a990cfa..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/zipkin/webpack.config.ts +++ b/public/app/plugins/datasource/zipkin/webpack.config.ts @@ -1,3 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx index d864dfaba37..48bc6d561fe 100644 --- a/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx +++ b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx @@ -10,7 +10,7 @@ import { isAsyncRequestMapSlicePending, isAsyncRequestMapSliceSettled, } from 'app/features/alerting/unified/utils/redux'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { AlertingRule } from 'app/types/unified-alerting'; import { PromRuleType } from 'app/types/unified-alerting-dto'; diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 6043d715801..913ddbfcda6 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -38,7 +38,7 @@ import { flattenCombinedRules, getFirstActiveAt } from 'app/features/alerting/un import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { Matcher } from 'app/plugins/datasource/alertmanager/types'; -import { ThunkDispatch, useDispatch } from 'app/types'; +import { ThunkDispatch, useDispatch } from 'app/types/store'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { AlertingAction, useAlertingAbility } from '../../../features/alerting/unified/hooks/useAbilities'; diff --git a/public/app/plugins/panel/dashlist/DashList.tsx b/public/app/plugins/panel/dashlist/DashList.tsx index e3044da47d2..d5c13d5627a 100644 --- a/public/app/plugins/panel/dashlist/DashList.tsx +++ b/public/app/plugins/panel/dashlist/DashList.tsx @@ -23,7 +23,7 @@ import impressionSrv from 'app/core/services/impression_srv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardSearchItem } from 'app/features/search/types'; import { VariablesChanged } from 'app/features/variables/types'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { Options } from './panelcfg.gen'; import { getStyles } from './styles'; diff --git a/public/app/plugins/panel/dashlist/migrations.ts b/public/app/plugins/panel/dashlist/migrations.ts index 25f420108d2..a48c8f4f736 100644 --- a/public/app/plugins/panel/dashlist/migrations.ts +++ b/public/app/plugins/panel/dashlist/migrations.ts @@ -1,6 +1,6 @@ import { PanelModel } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { Options } from './panelcfg.gen'; diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 709055ce389..ed375de1044 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -114,6 +114,12 @@ interface LogsPanelProps extends PanelProps<Options> { * When the feature toggle newLogsPanel is enabled, you can pass extra options to the LogLineMenu component. * These options are an array of items with { label, onClick } or { divider: true } for dividers. * logLineMenuCustomItems?: LogLineMenuCustomItem[]; + * + * Use the default, bigger, font size, or a smaller one. Defaults to "default". + * fontSize?: 'default' | 'small' + * + * Set the mode used by the Log Details panel. Displayed as a sidebar, or inline below the log line. Defaults to "inline". + * detailsMode?: 'inline' | 'sidebar' */ } interface LogsPermalinkUrlState { @@ -153,6 +159,7 @@ export const LogsPanel = ({ onNewLogsReceived, fontSize, syntaxHighlighting, + detailsMode, ...options }, id, @@ -535,6 +542,7 @@ export const LogsPanel = ({ app={isCoreApp(app) ? app : CoreApp.Dashboard} containerElement={scrollElement} dedupStrategy={dedupStrategy} + detailsMode={detailsMode} displayedFields={displayedFields} enableLogDetails={enableLogDetails} fontSize={fontSize} diff --git a/public/app/plugins/panel/logs/module.tsx b/public/app/plugins/panel/logs/module.tsx index 0e339918d65..de1562c4898 100644 --- a/public/app/plugins/panel/logs/module.tsx +++ b/public/app/plugins/panel/logs/module.tsx @@ -108,6 +108,22 @@ export const plugin = new PanelPlugin<Options>(LogsPanel) }, ], }, + }) + .addRadio({ + path: 'detailsMode', + name: t('logs.name-details-mode', 'Log Details panel mode'), + category, + description: '', + settings: { + options: [ + { value: 'inline', label: t('logs.name-details-options.label-inline', 'Inline') }, + { + value: 'sidebar', + label: t('logs.name-details-options.label-sidebar', 'Sidebar'), + }, + ], + }, + defaultValue: 'inline', }); } diff --git a/public/app/plugins/panel/logs/panelcfg.cue b/public/app/plugins/panel/logs/panelcfg.cue index cd21fc2e041..70ddf33b412 100644 --- a/public/app/plugins/panel/logs/panelcfg.cue +++ b/public/app/plugins/panel/logs/panelcfg.cue @@ -40,6 +40,7 @@ composableKinds: PanelCfg: { dedupStrategy: common.LogsDedupStrategy enableInfiniteScrolling?: bool fontSize?: "default" | "small" @cuetsy(kind="enum", memberNames="default|small") + detailsMode?: "inline" | "sidebar" @cuetsy(kind="enum", memberNames="inline|sidebar") // TODO: figure out how to define callbacks onClickFilterLabel?: _ onClickFilterOutLabel?: _ diff --git a/public/app/plugins/panel/logs/panelcfg.gen.ts b/public/app/plugins/panel/logs/panelcfg.gen.ts index 5af4d61f578..a07151c7fda 100644 --- a/public/app/plugins/panel/logs/panelcfg.gen.ts +++ b/public/app/plugins/panel/logs/panelcfg.gen.ts @@ -13,6 +13,7 @@ import * as common from '@grafana/schema'; export interface Options { controlsStorageKey?: string; dedupStrategy: common.LogsDedupStrategy; + detailsMode?: ('inline' | 'sidebar'); displayedFields?: Array<string>; enableInfiniteScrolling?: boolean; enableLogDetails: boolean; diff --git a/public/app/plugins/panel/nodeGraph/Edge.tsx b/public/app/plugins/panel/nodeGraph/Edge.tsx index 7a8a2911c67..ea1fa18f963 100644 --- a/public/app/plugins/panel/nodeGraph/Edge.tsx +++ b/public/app/plugins/panel/nodeGraph/Edge.tsx @@ -17,11 +17,10 @@ interface Props { onClick: (event: MouseEvent<SVGElement>, link: EdgeDatumLayout) => void; onMouseEnter: (id: string) => void; onMouseLeave: (id: string) => void; - processedNodesLength: number; } export const Edge = memo(function Edge(props: Props) { - const { edge, onClick, onMouseEnter, onMouseLeave, hovering, svgIdNamespace, processedNodesLength } = props; + const { edge, onClick, onMouseEnter, onMouseLeave, hovering, svgIdNamespace } = props; // Not great typing but after we do layout these properties are full objects not just references const { source, target, sourceNodeRadius, targetNodeRadius } = edge as { @@ -59,7 +58,6 @@ export const Edge = memo(function Edge(props: Props) { <EdgeArrowMarker id={markerId} fill={edgeColor} headHeight={arrowHeadHeight} /> <EdgeArrowMarker id={coloredMarkerId} fill={highlightedEdgeColor} headHeight={arrowHeadHeight} /> <g - key={`${edge.id}-${edge.source.y ?? ''}-${processedNodesLength}-g`} onClick={(event) => onClick(event, edge)} style={{ cursor: 'pointer' }} aria-label={t('nodeGraph.edge.aria-label-from-to', 'Edge from: {{from}} to: {{to}}', { diff --git a/public/app/plugins/panel/nodeGraph/NodeGraph.tsx b/public/app/plugins/panel/nodeGraph/NodeGraph.tsx index 08a06af7b27..4a398abe7a7 100644 --- a/public/app/plugins/panel/nodeGraph/NodeGraph.tsx +++ b/public/app/plugins/panel/nodeGraph/NodeGraph.tsx @@ -290,6 +290,7 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode, onMouseLeave={clearEdgeHover} svgIdNamespace={svgIdNamespace} processedNodesLength={processed.nodes.length} + processedEdgesLength={processed.edges.length} /> )} <Nodes @@ -430,26 +431,28 @@ interface EdgesProps { onMouseEnter: (id: string) => void; onMouseLeave: (id: string) => void; processedNodesLength: number; + processedEdgesLength: number; } const Edges = memo(function Edges(props: EdgesProps) { return ( <> - {props.edges.map((e) => ( - <Edge - key={`${e.id}-${e.source.y ?? ''}-${props.processedNodesLength}`} - edge={e} - hovering={ - (e.source as NodeDatum).id === props.nodeHoveringId || - (e.target as NodeDatum).id === props.nodeHoveringId || - props.edgeHoveringId === e.id - } - onClick={props.onClick} - onMouseEnter={props.onMouseEnter} - onMouseLeave={props.onMouseLeave} - svgIdNamespace={props.svgIdNamespace} - processedNodesLength={props.processedNodesLength} - /> - ))} + {props.edges.map((e, index) => { + return ( + <Edge + key={`${e.id}-${e.source.y ?? ''}-${props.processedNodesLength}-${props.processedEdgesLength}-${index}`} + edge={e} + hovering={ + (e.source as NodeDatum).id === props.nodeHoveringId || + (e.target as NodeDatum).id === props.nodeHoveringId || + props.edgeHoveringId === e.id + } + onClick={props.onClick} + onMouseEnter={props.onMouseEnter} + onMouseLeave={props.onMouseLeave} + svgIdNamespace={props.svgIdNamespace} + /> + ); + })} </> ); }); @@ -462,7 +465,7 @@ interface EdgeLabelsProps { const EdgeLabels = memo(function EdgeLabels(props: EdgeLabelsProps) { return ( <> - {props.edges.map((e, index) => { + {props.edges.map((e) => { // We show the edge label in case user hovers over the edge directly or if they hover over node edge is // connected to. const shouldShow = diff --git a/public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx index ad5d9e400be..d8752c8e503 100644 --- a/public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx @@ -10,6 +10,7 @@ import { AutoCellOptionsEditor } from './cells/AutoCellOptionsEditor'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; import { ImageCellOptionsEditor } from './cells/ImageCellOptionsEditor'; +import { PillCellOptionsEditor } from './cells/PillCellOptionsEditor'; import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor'; // The props that any cell type editor are expected @@ -77,6 +78,9 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => { {cellType === TableCellDisplayMode.Image && ( <ImageCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} /> )} + {cellType === TableCellDisplayMode.Pill && ( + <PillCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} /> + )} </div> ); }; @@ -91,6 +95,7 @@ let cellDisplayModeOptions: Array<SelectableValue<TableCellOptions>> = [ { value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' }, { value: { type: TableCellDisplayMode.Image }, label: 'Image' }, { value: { type: TableCellDisplayMode.Actions }, label: 'Actions' }, + { value: { type: TableCellDisplayMode.Pill }, label: 'Pill' }, ]; const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/plugins/panel/table/table-new/TablePanel.tsx b/public/app/plugins/panel/table/table-new/TablePanel.tsx index 903a74f65c3..78f97127d9c 100644 --- a/public/app/plugins/panel/table/table-new/TablePanel.tsx +++ b/public/app/plugins/panel/table/table-new/TablePanel.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { ActionModel, @@ -26,7 +26,7 @@ import { Options } from './panelcfg.gen'; interface Props extends PanelProps<Options> {} export function TablePanel(props: Props) { - const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables } = props; + const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables, transparent } = props; useMemo(() => { cacheFieldDisplayNames(data.series); @@ -57,6 +57,11 @@ export function TablePanel(props: Props) { const enableSharedCrosshair = panelContext.sync && panelContext.sync() !== DashboardCursorSync.Off; + const _getActions = useCallback( + (frame: DataFrame, field: Field, rowIndex: number) => getCellActions(frame, field, rowIndex, replaceVariables), + [replaceVariables] + ); + const tableElement = ( <TableNG height={tableHeight} @@ -75,9 +80,9 @@ export function TablePanel(props: Props) { timeRange={timeRange} enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair} fieldConfig={fieldConfig} - getActions={getCellActions} - replaceVariables={replaceVariables} + getActions={_getActions} structureRev={data.structureRev} + transparent={transparent} /> ); @@ -159,28 +164,39 @@ const getCellActions = ( field: Field, rowIndex: number, replaceVariables: InterpolateFunction | undefined -) => { - const actions: Array<ActionModel<Field>> = []; - const actionLookup = new Set<string>(); +): Array<ActionModel<Field>> => { + const numActions = field.config.actions?.length ?? 0; - const actionsModel = getActions( - dataFrame, - field, - field.state!.scopedVars!, - replaceVariables ?? replaceVars, - field.config.actions ?? [], - { valueRowIndex: rowIndex } - ); + if (numActions > 0) { + const actions = getActions( + dataFrame, + field, + field.state!.scopedVars!, + replaceVariables ?? replaceVars, + field.config.actions ?? [], + { valueRowIndex: rowIndex } + ); - actionsModel.forEach((action) => { - const key = `${action.title}`; - if (!actionLookup.has(key)) { - actions.push(action); - actionLookup.add(key); + if (actions.length === 1) { + return actions; + } else { + const actionsOut: Array<ActionModel<Field>> = []; + const actionLookup = new Set<string>(); + + actions.forEach((action) => { + const key = action.title; + + if (!actionLookup.has(key)) { + actionsOut.push(action); + actionLookup.add(key); + } + }); + + return actionsOut; } - }); + } - return actions; + return []; }; const tableStyles = { diff --git a/public/app/plugins/panel/table/table-new/cells/PillCellOptionsEditor.tsx b/public/app/plugins/panel/table/table-new/cells/PillCellOptionsEditor.tsx new file mode 100644 index 00000000000..0ff38f33ea9 --- /dev/null +++ b/public/app/plugins/panel/table/table-new/cells/PillCellOptionsEditor.tsx @@ -0,0 +1,66 @@ +import { t } from '@grafana/i18n'; +import { TablePillCellOptions } from '@grafana/schema'; +import { Field, ColorPicker, RadioButtonGroup, Stack } from '@grafana/ui'; + +import { TableCellEditorProps } from '../TableCellOptionEditor'; + +const colorModeOptions: Array<{ value: 'auto' | 'fixed' | 'mapped'; label: string }> = [ + { value: 'auto', label: 'Auto' }, + { value: 'fixed', label: 'Fixed color' }, + { value: 'mapped', label: 'Value mapping' }, +]; + +export const PillCellOptionsEditor = ({ cellOptions, onChange }: TableCellEditorProps<TablePillCellOptions>) => { + const colorMode = cellOptions.colorMode || 'auto'; + + const onColorModeChange = (mode: 'auto' | 'fixed' | 'mapped') => { + const updatedOptions = { ...cellOptions, colorMode: mode }; + onChange(updatedOptions); + }; + + const onColorChange = (color: string) => { + const updatedOptions = { ...cellOptions, color }; + onChange(updatedOptions); + }; + + return ( + <Stack direction="column" gap={1}> + <Field + label={t('table.pill-cell-options-editor.label-color-mode', 'Color Mode')} + description={t( + 'table.pill-cell-options-editor.description-color-mode', + 'Choose how colors are assigned to pills' + )} + noMargin + > + <RadioButtonGroup value={colorMode} onChange={onColorModeChange} options={colorModeOptions} /> + </Field> + + {colorMode === 'fixed' && ( + <Field + label={t('table.pill-cell-options-editor.label-fixed-color', 'Fixed Color')} + description={t( + 'table.pill-cell-options-editor.description-fixed-color', + 'All pills in this column will use this color' + )} + noMargin + > + <ColorPicker color={cellOptions.color || '#FF780A'} onChange={onColorChange} enableNamedColors={false} /> + </Field> + )} + + {colorMode === 'mapped' && ( + <Field + label={t('table.pill-cell-options-editor.label-value-mappings-info', 'Value Mappings')} + description={t( + 'table.pill-cell-options-editor.description-value-mappings-info', + 'For Value Mappings either use the global table Value Mappings or the Field overrides Value Mappings. The default will fall back to the Color Scheme. ' + )} + noMargin + > + <div> </div> + </Field> + )} + </Stack> + ); +}; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index e1baf824794..41221a4e772 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -17,7 +17,8 @@ import { ConfigureIRM } from 'app/features/gops/configuration-tracker/components import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes'; import { getAppPluginRoutes } from 'app/features/plugins/routes'; import { getProfileRoutes } from 'app/features/profile/routes'; -import { AccessControlAction, DashboardRoutes } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { DashboardRoutes } from 'app/types/dashboard'; import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamicImport'; import { RouteDescriptor } from '../core/navigation/types'; diff --git a/public/app/store/store.ts b/public/app/store/store.ts index 496a3e4b081..557ec7566f3 100644 --- a/public/app/store/store.ts +++ b/public/app/store/store.ts @@ -1,7 +1,7 @@ import { Store } from 'redux'; import { initialKeyedVariablesState } from 'app/features/variables/state/keyedVariablesReducer'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; export let store: Store<StoreState>; diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index 684a3a158b1..a3662165c92 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -1,12 +1,8 @@ -import { OrgRole } from '@grafana/data'; - export enum TeamPermissionLevel { Admin = 4, Member = 0, } -export { OrgRole as OrgRole }; - export type PermissionLevel = 'view' | 'edit' | 'admin'; /** @deprecated Use PermissionLevel instead */ diff --git a/public/app/types/apiKeys.ts b/public/app/types/apiKeys.ts index 37778e66a7a..e732ecc06cd 100644 --- a/public/app/types/apiKeys.ts +++ b/public/app/types/apiKeys.ts @@ -1,6 +1,4 @@ -import { WithAccessControlMetadata } from '@grafana/data'; - -import { OrgRole } from './acl'; +import { OrgRole, WithAccessControlMetadata } from '@grafana/data'; export interface ApiKey extends WithAccessControlMetadata { id?: number; diff --git a/public/app/types/index.ts b/public/app/types/index.ts deleted file mode 100644 index 546c2ab628d..00000000000 --- a/public/app/types/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -export * from './teams'; -export * from './alerting'; -export * from './location'; -export * from './folders'; -export * from './dashboard'; -export * from './acl'; -export * from './apiKeys'; -export * from './user'; -export * from './serviceaccount'; -export * from './datasources'; -export * from './plugins'; -export * from './organization'; -export * from './appNotifications'; -export * from './explore'; -export * from './store'; -export * from './ldap'; -export * from './query'; -export * from './accessControl'; -export * from './supportBundles'; -export * from './settings'; - -import * as CoreEvents from './events'; -export { CoreEvents }; diff --git a/public/app/types/organization.ts b/public/app/types/organization.ts index 926bced7bd5..367c7fcd55c 100644 --- a/public/app/types/organization.ts +++ b/public/app/types/organization.ts @@ -1,4 +1,4 @@ -import { UserOrg } from 'app/types'; +import { UserOrg } from 'app/types/user'; export interface Organization { name: string; diff --git a/public/app/types/serviceaccount.ts b/public/app/types/serviceaccount.ts index 85c2fe1c0de..2d9a56e95b7 100644 --- a/public/app/types/serviceaccount.ts +++ b/public/app/types/serviceaccount.ts @@ -1,6 +1,7 @@ -import { WithAccessControlMetadata } from '@grafana/data'; +import { OrgRole, WithAccessControlMetadata } from '@grafana/data'; -import { ApiKey, OrgRole, Role } from '.'; +import { Role } from './accessControl'; +import { ApiKey } from './apiKeys'; export interface OrgServiceAccount extends WithAccessControlMetadata { serviceAccountId: number; diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 67264b0f298..e5cd1805e7f 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,7 +1,6 @@ -import { SelectableValue, WithAccessControlMetadata } from '@grafana/data'; -import { Role } from 'app/types'; +import { OrgRole, SelectableValue, WithAccessControlMetadata } from '@grafana/data'; -import { OrgRole } from '.'; +import { Role } from './accessControl'; export interface OrgUser extends WithAccessControlMetadata { avatarUrl: string; diff --git a/public/img/plugins/logicmonitor.svg b/public/img/plugins/logicmonitor.svg index f533e96b843..20855d2bb3b 100644 --- a/public/img/plugins/logicmonitor.svg +++ b/public/img/plugins/logicmonitor.svg @@ -1 +1,5 @@ -<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#fff;}.cls-4{fill:#0076ff;}</style><clipPath id="clip-path"><rect class="cls-1" x="-5" y="29.5" width="1511" height="408"></rect></clipPath></defs><title>svg \ No newline at end of file + + + + + diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index e6027d7ec7c..913c87e01d0 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -516,6 +516,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Upravit YAML", @@ -1517,15 +1520,12 @@ "group-details": { "ds-features-error": "Chyba při načítání podrobností o zdroji dat", "edit": "Upravit", - "evaluations-to-fire": "Cykly hodnocení ke spuštění", "export": "Exportovat", "folder": "Složka", "group-loading-error": "Chyba při načítání skupiny", "interval": "Interval", "namespace": "Jmenný prostor", - "pending-period": "Období čekání", - "recording": "Probíhá záznam", - "rule-name": "Jméno pravidla" + "new": "" }, "group-edit": { "ds-error": "Chyba při načítání podrobností o zdroji dat", @@ -2255,6 +2255,9 @@ "label-export-all": "Exportovat vše" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Nelze načíst editor dotazů, důvod: {{errorMessage}}" }, @@ -2676,6 +2679,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Nenašli jste požadovaný zdroj dat? Některé zdroje dat nepodporují upozorňování. Další informace zobrazíte kliknutím na ikonu." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Přidat nového správce výstrah", "builtin-alertmanager": "Vestavěný správce výstrah", @@ -2911,6 +2919,11 @@ "timestamp": { "time-ago": "(před {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Cílová složka není prázdná, některá pravidla mohou být přepsána nebo odstraněna. Opravdu chcete importovat tato pravidla výstrah do pravidel spravovaných Grafanou?", @@ -4435,7 +4448,6 @@ } }, "render-left-actions": { - "text-public": "Veřejná", "tooltip-view-as-scene": "Zobrazit jako scénu" } }, @@ -5211,7 +5223,6 @@ "playlist-next": "Přejít na další nástěnku", "playlist-previous": "Přejít na předchozí nástěnku", "playlist-stop": "Zastavit playlist", - "public-dashboard": "Veřejná", "refresh": "Obnovit nástěnku", "save": "Uložit nástěnku", "save-dashboard": { @@ -5512,15 +5523,9 @@ "type": "Typ" }, "dashboard-preview-banner": { - "not-saved": "Hodnota ještě není uložena v databázi Grafany", "not-yet-saved": "Hodnota není uložena v databázi Grafany", - "open-pull-request-in-git-hub": "Otevřít pull request v GitHubu", - "title-dashboard-loaded-branch-git-hub": "Tato nástěnka je načtena z větve na GitHubu.", "title-dashboard-loaded-external-repository": "Tato nástěnka je načtena z externího úložiště", - "title-dashboard-loaded-request-git-hub": "Tato nástěnka je načtena z pull request na GitHubu.", - "title-error-loading-dashboard": "Chyba při načítání nástěnky", - "value-not-saved": "Hodnota ještě není uložena v databázi Grafany", - "view-pull-request-in-git-hub": "Zobrazit pull request v GitHubu" + "title-error-loading-dashboard": "Chyba při načítání nástěnky" }, "dashboard-scene": { "text": { @@ -5704,6 +5709,20 @@ "usage-count_many": "Použito na {{count}} nástěnkách", "usage-count_other": "Použito na {{count}} nástěnkách" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Ve vybrané složce už existuje nástěnka se stejným názvem. Chcete přesto tuto nástěnku uložit?", "title-name-already-exists": "Název již existuje" @@ -7259,6 +7278,7 @@ "when": "KDY" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Činnost" }, "math": { @@ -8033,6 +8053,9 @@ "filter-popup-match-case": "Rozlišovat malá a velká písmena", "inspect-drawer-title": "Zkontrolovat hodnotu", "inspect-menu-label": "Zkontrolovat hodnotu", + "nested-table": { + "no-data": "" + }, "no-values-label": "Žádné hodnoty", "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} z {{numRows}} řádků" }, @@ -8551,7 +8574,7 @@ "usage-count_other": "Použito na {{count}} nástěnkách" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Hledat podle názvu nebo popisu" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Načítání panelu knihovny…" @@ -8696,12 +8719,18 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_few": "", + "indexed-label_many": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_few": "", + "parsedl-label_many": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_few": "", + "structured-metadata_many": "", + "structured-metadata_other": "" } } }, @@ -8758,6 +8787,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8770,16 +8800,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8882,6 +8916,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9140,6 +9179,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Nástěnku už zajišťuje a spravuje Grafana v cloudové instanci. Doporučujeme používat zajištěnou nástěnku. Pokud si přesto přejete zkopírovat nástěnku do cloudové instance, změňte ID nástěnky v JSON nástěnky, uložte nový snímek a nahrajte ji znovu.", "datasource-already-managed": "Zdroj dat už zajišťuje a spravuje Grafana v cloudové instanci. Pokud se jedná o jiný zdroj, nastavte jiné UID a zkuste to znovu.", "datasource-invalid-url": "Existuje zdroj dat s neplatnou adresou URL. Zadejte platnou adresu URL a zkuste to znovu.", @@ -10369,6 +10410,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Tato funkce je v současné době v aktivním vývoji. Pro nejlepší zážitek a nejnovější vylepšení doporučujeme použít <2>noční sestavení Grafany." @@ -11834,6 +11884,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index df7913debc3..d55bb859eda 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML bearbeiten", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Fehler beim Laden der Datenquellendetails", "edit": "Bearbeiten", - "evaluations-to-fire": "Auszulösende Evaluierungszyklen", "export": "Exportieren", "folder": "Ordner", "group-loading-error": "Fehler beim Laden der Gruppe", "interval": "Intervall", "namespace": "Namensraum", - "pending-period": "Wartezeit", - "recording": "Aufzeichnung", - "rule-name": "Regelname" + "new": "" }, "group-edit": { "ds-error": "Fehler beim Laden der Datenquellendetails", @@ -2237,6 +2237,9 @@ "label-export-all": "Alles exportieren" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Der Abfrage-Editor konnte nicht geladen werden wegen: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Sie finden Ihre gewünschte Datenquelle nicht? Einige Datenquellen werden für Warnungen nicht unterstützt. Klicken Sie für weitere Informationen auf das Symbol." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Neuen Alertmanager hinzufügen", "builtin-alertmanager": "Integrierter Alertmanager", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "(vor {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Der Zielordner ist nicht leer, einige Regeln können überschrieben oder entfernt werden. Sind Sie sicher, dass Sie diese Warnregeln zu von Grafana verwalteten Regeln importieren möchten?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Öffentlich", "tooltip-view-as-scene": "Als Szene anzeigen" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Zum nächsten Dashboard", "playlist-previous": "Zum vorherigen Dashboard", "playlist-stop": "Wiedergabeliste stoppen", - "public-dashboard": "Öffentlich", "refresh": "Dashboard aktualisieren", "save": "Dashboard speichern", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Typ" }, "dashboard-preview-banner": { - "not-saved": "Der Wert ist noch nicht in der Grafana-Datenbank gespeichert", "not-yet-saved": "Der Wert wird nicht in der Grafana-Datenbank gespeichert", - "open-pull-request-in-git-hub": "Pull-Anfrage in GitHub öffnen", - "title-dashboard-loaded-branch-git-hub": "Dieses Dashboard wird von einem Branch in GitHub geladen.", "title-dashboard-loaded-external-repository": "Dieses Dashboard wird von einem externen Repository geladen", - "title-dashboard-loaded-request-git-hub": "Dieses Dashboard wird von einer Pull-Anfrage in GitHub geladen.", - "title-error-loading-dashboard": "Fehler beim Laden des Dashboards", - "value-not-saved": "Der Wert ist noch nicht in der Grafana-Datenbank gespeichert", - "view-pull-request-in-git-hub": "Pull-Anfrage in GitHub anzeigen" + "title-error-loading-dashboard": "Fehler beim Laden des Dashboards" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "Verwendet bei {{count}} Dashboards", "usage-count_other": "Verwendet bei {{count}} Dashboards" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Ein Dashboard mit demselben Namen im ausgewählten Ordner existiert bereits. Möchten Sie dieses Dashboard trotzdem speichern?", "title-name-already-exists": "Der Name existiert bereits" @@ -7217,6 +7236,7 @@ "when": "WANN" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operation" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Groß-/Kleinschreibung beachten", "inspect-drawer-title": "Wert prüfen", "inspect-menu-label": "Wert prüfen", + "nested-table": { + "no-data": "" + }, "no-values-label": "Keine Werte", "pagination-summary": "{{itemsRangeStart}}–{{displayedEnd}} von {{numRows}} Zeilen" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "Verwendet bei {{count}} Dashboards" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Suche nach Name oder Beschreibung" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Bibliotheks-Panel wird geladen ..." @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Das Dashboard wird bereits von Grafana in der Cloud-Instanz bereitgestellt und verwaltet. Wir empfehlen, in Zukunft das bereitgestellte Dashboard zu verwenden. Wenn Sie das Dashboard weiterhin in die Cloud-Instanz kopieren möchten, ändern Sie bitte die Dashboard-ID im Dashboard-JSON, speichern Sie einen neuen Snapshot und laden Sie ihn erneut hoch.", "datasource-already-managed": "Die Datenquelle wird bereits von Grafana in der Cloud-Instanz bereitgestellt und verwaltet. Wenn es sich dabei um eine andere Ressource handelt, legen Sie bitte eine andere UID fest und versuchen Sie es erneut.", "datasource-invalid-url": "Es gibt eine Datenquelle mit einer ungültigen URL. Geben Sie eine gültige URL an und versuchen Sie es erneut.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Diese Funktion befindet sich momentan in der aktiven Entwicklung. Für die bestmögliche Nutzererfahrung und die neuesten Verbesserungen empfehlen wir die Nutzung von <2>Nightly Build von Grafana." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 4f517a706ed..ab3d742f57d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "Alert rule" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Edit YAML", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Error loading data source details", "edit": "Edit", - "evaluations-to-fire": "Evaluation cycles to fire", "export": "Export", "folder": "Folder", "group-loading-error": "Error loading the group", "interval": "Interval", "namespace": "Namespace", - "pending-period": "Pending period", - "recording": "Recording", - "rule-name": "Rule name" + "new": "New" }, "group-edit": { "ds-error": "Error loading data source details", @@ -2237,6 +2237,9 @@ "label-export-all": "Export all" } }, + "recording-rule": { + "term": "Recording rule" + }, "recording-rule-editor": { "error-no-query-editor": "Could not load query editor due to: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Not finding the data source you want? Some data sources are not supported for alerting. Click on the icon for more information." }, + "settings": { + "tabs": { + "alert-managers": "Alert managers" + } + }, "settings-content": { "add-new-alertmanager": "Add new Alertmanager", "builtin-alertmanager": "Built-in Alertmanager", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} ago)" }, + "titles": { + "group-view": { + "subtitle": "Manage alert rules, recording rules and evaluation interval" + } + }, "to-gma": { "confirm-modal": { "body": "The target folder is not empty, some rules may be overwritten or removed. Are you sure you want to import these alert rules to Grafana-managed rules?", @@ -3537,11 +3550,11 @@ "button-create": "Create", "button-creating": "Creating...", "cancel": "Cancel", - "error-invalid-folder-name": "Invalid folder name", "error-required": "Folder name is required", "folder-name-input-placeholder-enter-folder-name": "Enter folder name", "label-folder-name": "Folder name", "text-pull-request-created": "A pull request has been created with changes to this folder:", + "text-your-folder-will-be-created-as": "Your folder will be created as {{folderName}}", "title-pull-request-created": "Pull request created", "title-this-repository-is-read-only": "This repository is read only" }, @@ -5656,6 +5669,20 @@ "usage-count_one": "Used on {{count}} dashboards", "usage-count_other": "Used on {{count}} dashboards" }, + "move-provisioned-dashboard-form": { + "api-error": "Failed to move dashboard", + "cancel-action": "Cancel", + "current-file-not-found": "Current dashboard file could not be found", + "drawer-title": "Move Provisioned Dashboard", + "file-load-error": "Error loading dashboard", + "loading-file-data": "Loading dashboard data", + "move-action": "Move dashboard", + "move-read-only-message": "This dashboard cannot be moved directly from Grafana because the repository is read-only. To move this dashboard, please move the file in your Git repository.", + "moving": "Moving...", + "partial-failure-warning": "Dashboard was created at new location but could not be deleted from original location. Please manually remove the old file.", + "target-path-label": "Target path", + "title-this-repository-is-read-only": "This repository is read only" + }, "name-already-exists-error": { "body-name-already-exists": "A dashboard with the same name in selected folder already exists. Would you still like to save this dashboard?", "title-name-already-exists": "Name already exists" @@ -7209,6 +7236,7 @@ "when": "WHEN" }, "expression-query-editor": { + "helper-text-sql": "Run MySQL-dialect SQL against the tables returned from your data sources. Data source queries (ie \"A\", \"B\") are available as tables and referenced by query-name. Fields are available as columns, as returned from the data source.", "label-operation": "Operation" }, "math": { @@ -7983,6 +8011,9 @@ "filter-popup-match-case": "Match case", "inspect-drawer-title": "Inspect value", "inspect-menu-label": "Inspect value", + "nested-table": { + "no-data": "No data" + }, "no-values-label": "No values", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} of {{numRows}} rows" }, @@ -8711,6 +8742,7 @@ }, "fields-section": "Fields", "hide-log-line": "Hide log line", + "inline-mode": "Display inline", "links-section": "Links", "log-line-field": "Log line", "log-line-section": "Log line", @@ -8723,6 +8755,7 @@ "search-placeholder": "Search field names and values", "show-context": "Show context", "show-log-line": "Show log line", + "sidebar-mode": "Anchor to the right", "unpin-line": "Unpin log" }, "log-line-menu": { @@ -8825,6 +8858,11 @@ }, "name-common-labels": "Common labels", "name-deduplication": "Deduplication", + "name-details-mode": "Log Details panel mode", + "name-details-options": { + "label-inline": "Inline", + "label-sidebar": "Sidebar" + }, "name-enable-infinite-scrolling": "Enable infinite scrolling", "name-enable-log-details": "Enable log details", "name-enable-syntax-highlighting": "Enable syntax highlighting", @@ -9083,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "Maximum number of alert rule groups reached: Delete some alert rule groups or upgrade your plan and try again.", + "alert-rules-quota-reached": "Maximum number of alert rules reached: Delete some alert rules or upgrade your plan and try again.", "dashboard-already-managed": "Dashboard is already provisioned and managed by Grafana in the cloud instance. We recommend using the provisioned dashboard going forward. If you still wish to copy the dashboard to the cloud instance, then change the dashboard ID in the dashboard JSON, save a new snapshot and upload again.", "datasource-already-managed": "Data source is already provisioned and managed by Grafana in the cloud instance. If this is a different resource, set another UID and try again.", "datasource-invalid-url": "There is a data source which has an invalid URL. Provide a valid URL and try again.", @@ -10312,10 +10352,8 @@ "open-pull-request-in-git-hub": "Open pull request in GitHub", "view-pull-request-in-git-hub": "View pull request in GitHub" }, - "title-dashboard-loaded-branch-git-hub": "This dashboard is loaded from a branch in GitHub.", - "title-dashboard-loaded-pull-request-git-hub": "This dashboard is loaded from a pull request in GitHub.", - "title-folder-created-branch-git-hub": "A new folder has been created in a branch in GitHub.", - "title-folder-created-pull-request-git-hub": "A new folder has been created in a pull request in GitHub." + "title-created-branch-git-hub": "A new resource has been created in a branch in GitHub.", + "title-loaded-pull-request-git-hub": "This resource is loaded from a pull request in GitHub." }, "provisioning": { "banner": { @@ -11768,6 +11806,14 @@ "name-show-table-footer": "Show table footer", "name-show-table-header": "Show table header", "name-wrap-header-text": "Wrap header text", + "pill-cell-options-editor": { + "description-color-mode": "Choose how colors are assigned to pills", + "description-fixed-color": "All pills in this column will use this color", + "description-value-mappings-info": "For Value Mappings either use the global table Value Mappings or the Field overrides Value Mappings. The default will fall back to the Color Scheme. ", + "label-color-mode": "Color Mode", + "label-fixed-color": "Fixed Color", + "label-value-mappings-info": "Value Mappings" + }, "placeholder-column-width": "auto", "placeholder-fields": "All Numeric Fields" }, @@ -12503,6 +12549,7 @@ "transform": "Transform" } }, + "tag": "{{ tag }}", "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Time field", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index be14e14cfee..fb3b395d998 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Editar YAML", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Error al cargar los detalles de la fuente de datos", "edit": "Editar", - "evaluations-to-fire": "Ciclos de evaluación para activar", "export": "Exportar", "folder": "Carpeta", "group-loading-error": "Error al cargar el grupo", "interval": "Intervalo", "namespace": "Nombre del espacio", - "pending-period": "Periodo pendiente", - "recording": "Registro", - "rule-name": "Nombre de la regla" + "new": "" }, "group-edit": { "ds-error": "Error al cargar los detalles de la fuente de datos", @@ -2237,6 +2237,9 @@ "label-export-all": "Exportar todo" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "No se ha podido cargar el editor de consultas debido a: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "¿No encuentras la fuente de datos que quieres? Algunas fuentes de datos no son compatibles con las alertas. Haz clic en el icono para obtener más información." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Añadir nuevo Alertmanager", "builtin-alertmanager": "Alertmanager integrado", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "(hace {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "La carpeta de destino no está vacía y pueden sobrescribirse o eliminarse algunas reglas. ¿Seguro que quieres importar estas reglas de alerta a las reglas gestionadas por Grafana?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Pública", "tooltip-view-as-scene": "Ver como escena" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Ir al siguiente panel de control", "playlist-previous": "Ir al panel de control anterior", "playlist-stop": "Detener la lista de reproducción", - "public-dashboard": "Pública", "refresh": "Actualizar panel de control", "save": "Guardar panel de control", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Tipo" }, "dashboard-preview-banner": { - "not-saved": "El valor aún no se ha guardado en la base de datos de Grafana", "not-yet-saved": "El valor no se ha guardado en la base de datos de Grafana", - "open-pull-request-in-git-hub": "Abrir solicitud de extracción en GitHub", - "title-dashboard-loaded-branch-git-hub": "Este panel de control se carga desde una rama en GitHub.", "title-dashboard-loaded-external-repository": "Este panel de control se carga desde un repositorio externo", - "title-dashboard-loaded-request-git-hub": "Este panel de control se carga desde una solicitud de extracción en GitHub.", - "title-error-loading-dashboard": "Error al cargar el panel de control", - "value-not-saved": "El valor aún no se ha guardado en la base de datos de Grafana", - "view-pull-request-in-git-hub": "Ver solicitud de extracción en GitHub" + "title-error-loading-dashboard": "Error al cargar el panel de control" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "Utilizado en {{count}} dashboards", "usage-count_other": "Utilizado en {{count}} dashboards" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Ya existe un dashboard con el mismo nombre en la carpeta seleccionada. ¿Seguro que quieres guardar este dashboard?", "title-name-already-exists": "Este nombre ya existe" @@ -7217,6 +7236,7 @@ "when": "CUÁNDO" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operación" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Coincidir mayúsculas/minúsculas", "inspect-drawer-title": "Inspeccionar valor", "inspect-menu-label": "Inspeccionar valor", + "nested-table": { + "no-data": "" + }, "no-values-label": "Sin valores", "pagination-summary": "{{itemsRangeStart}}-{{displayedEnd}} de {{numRows}} filas" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "Utilizado en {{count}} dashboards" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Buscar por nombre o descripción" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Cargando panel de la librería..." @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "Aceptar", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "El panel de control ya está aprovisionado y gestionado por Grafana en la instancia en la nube. A partir de ahora, recomendamos utilizar el panel de control proporcionado. Si todavía quieres copiar el panel de control a la instancia en la nube, cambia el ID del panel de control en el JSON del panel de control, guarda una nueva instantánea y vuelve a subirla.", "datasource-already-managed": "La fuente de datos ya está aprovisionada y gestionada por Grafana en la instancia en la nube. Si se trata de un recurso distinto, configura otro UID e inténtalo de nuevo.", "datasource-invalid-url": "Hay una fuente de datos que tiene una URL no válida. Proporciona una URL válida e inténtalo de nuevo.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Esta función se encuentra actualmente en desarrollo activo. Para obtener la mejor experiencia y las últimas mejoras, recomendamos utilizar la <2>compilación nocturna de Grafana." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index e90b9bfca6e..46c4e18b668 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Modifier le fichier YAML", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Erreur lors du chargement des détails de la source de données", "edit": "Modifier", - "evaluations-to-fire": "Cycles d’évaluation à déclencher", "export": "Exporter", "folder": "Dossier", "group-loading-error": "Erreur lors du chargement du groupe", "interval": "Intervalle", "namespace": "Namespace", - "pending-period": "Période en attente", - "recording": "Enregistrement", - "rule-name": "Nom de la règle" + "new": "" }, "group-edit": { "ds-error": "Erreur lors du chargement des détails de la source de données", @@ -2237,6 +2237,9 @@ "label-export-all": "Tout exporter" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Impossible de charger l’éditeur de requête en raison de : {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Vous ne trouvez pas la source de données que vous voulez ? Certaines sources de données ne sont pas prises en charge pour les alertes. Cliquez sur l’icône pour en savoir plus." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Ajouter un nouvel Alertmanager", "builtin-alertmanager": "Alertmanager intégré", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "(il y a {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Le dossier cible n’est pas vide, certaines règles peuvent être remplacées ou supprimées. Voulez-vous vraiment importer ces règles d’alerte dans les règles gérées par Grafana ?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Public", "tooltip-view-as-scene": "Afficher en tant que scène" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Accéder au tableau de bord suivant", "playlist-previous": "Accéder au tableau de bord précédent", "playlist-stop": "Arrêter la liste de lecture", - "public-dashboard": "Public", "refresh": "Actualiser le tableau de bord", "save": "Enregistrer le tableau de bord", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Type" }, "dashboard-preview-banner": { - "not-saved": "La valeur n'est pas encore enregistrée dans la base de données Grafana", "not-yet-saved": "La valeur n'est pas enregistrée dans la base de données Grafana", - "open-pull-request-in-git-hub": "Ouvrir la demande de fusion dans GitHub", - "title-dashboard-loaded-branch-git-hub": "Ce tableau de bord est chargé à partir d'une branche dans GitHub.", "title-dashboard-loaded-external-repository": "Ce tableau de bord est chargé à partir d'un référentiel externe", - "title-dashboard-loaded-request-git-hub": "Ce tableau de bord est chargé à partir d'une demande de fusion dans GitHub.", - "title-error-loading-dashboard": "Erreur lors du chargement du tableau de bord", - "value-not-saved": "La valeur n'est pas encore enregistrée dans la base de données Grafana", - "view-pull-request-in-git-hub": "Afficher la demande de fusion dans GitHub" + "title-error-loading-dashboard": "Erreur lors du chargement du tableau de bord" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "Utilisé sur {{count}} tableaux de bord", "usage-count_other": "Utilisé sur {{count}} tableaux de bord" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Un tableau de bord du même nom existe déjà dans le dossier sélectionné. Voulez-vous toujours enregistrer ce tableau de bord ?", "title-name-already-exists": "Nom déjà existant" @@ -7217,6 +7236,7 @@ "when": "QUAND" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Opération" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Respecter la casse", "inspect-drawer-title": "Inspecter la valeur", "inspect-menu-label": "Inspecter la valeur", + "nested-table": { + "no-data": "" + }, "no-values-label": "Aucune valeur", "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} sur {{numRows}} lignes" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "Utilisé sur {{count}} tableaux de bord" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Rechercher par nom ou par description" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Chargement du panneau de la bibliothèque..." @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Le tableau de bord est déjà mis en service et géré par Grafana dans l'instance cloud. Nous vous recommandons d'utiliser le tableau de bord mis en service à l'avenir. Si vous souhaitez toujours copier le tableau de bord dans l'instance cloud, modifiez l'ID du tableau de bord dans le JSON du tableau de bord, puis enregistrez un nouvel instantané et téléchargez à nouveau.", "datasource-already-managed": "La source de données est déjà mise en service et gérée par Grafana dans l'instance cloud. S'il s'agit d'une ressource différente, définissez un autre UID et réessayez.", "datasource-invalid-url": "Il y a une source de données dont l'URL n'est pas valide. Fournissez une URL valide et réessayez.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Cette fonctionnalité est actuellement en cours de développement. Pour une expérience optimale et les dernières améliorations, nous vous recommandons d’utiliser la <2>compilation nocturne de Grafana." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index b928b853b78..ffac048a310 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML szerkesztése", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Hiba történt az adatforrás adatainak betöltésekor", "edit": "Szerkesztés", - "evaluations-to-fire": "Aktiválandó értékelési ciklusok", "export": "Exportálás", "folder": "Mappa", "group-loading-error": "Hiba a csoport betöltésekor", "interval": "Intervallum", "namespace": "Névtér", - "pending-period": "Függőben lévő időszak", - "recording": "Felvételkészítés", - "rule-name": "Szabálynév" + "new": "" }, "group-edit": { "ds-error": "Hiba történt az adatforrás adatainak betöltésekor", @@ -2237,6 +2237,9 @@ "label-export-all": "Összes exportálása" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Nem sikerült betölteni a lekérdezésszerkesztőt a következők miatt: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Nem találja a kívánt adatforrást? Egyes adatforrások nem támogatottak a riasztáshoz. További információért kattintson az ikonra." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Új riasztáskezelő hozzáadása", "builtin-alertmanager": "Beépített riasztáskezelő", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "(ennyi ideje: {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "A célmappa nem üres, néhány szabály felül lesz írva vagy el lesz távolítva. Biztosan importálja ezeket a riasztási szabályokat a Grafana által felügyelt szabályokba?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Nyilvános", "tooltip-view-as-scene": "Megtekintés jelenetként" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Ugrás a következő irányítópulthoz", "playlist-previous": "Ugrás az előző irányítópulthoz", "playlist-stop": "Lejátszási lista leállítása", - "public-dashboard": "Nyilvános", "refresh": "Irányítópult frissítése", "save": "Irányítópult mentése", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Típus" }, "dashboard-preview-banner": { - "not-saved": "Az érték még nincs mentve a Grafana-adatbázisban", "not-yet-saved": "Az érték nincs mentve a Grafana-adatbázisban", - "open-pull-request-in-git-hub": "Összefésülési kérelem megnyitása a GitHubon", - "title-dashboard-loaded-branch-git-hub": "Ez az irányítópult a GitHub egyik ágából töltődik be.", "title-dashboard-loaded-external-repository": "Ez az irányítópult külső adattárból töltődik be", - "title-dashboard-loaded-request-git-hub": "Ez az irányítópult a GitHub egyik összefésülési kérelméből töltődik be.", - "title-error-loading-dashboard": "Hiba történt az irányítópult betöltésekor", - "value-not-saved": "Az érték még nincs mentve a Grafana-adatbázisban", - "view-pull-request-in-git-hub": "Összefésülési kérelem megtekintése a GitHubon" + "title-error-loading-dashboard": "Hiba történt az irányítópult betöltésekor" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "{{count}} irányítópulton használatos", "usage-count_other": "{{count}} irányítópulton használatos" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Egy irányítópult már létezik ugyanazzal a névvel a kijelölt mappában. Biztosan menti ezt az irányítópultot?", "title-name-already-exists": "A név már létezik" @@ -7217,6 +7236,7 @@ "when": "WHEN" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Művelet" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Nagybetűérzékeny", "inspect-drawer-title": "Érték ellenőrzése", "inspect-menu-label": "Érték ellenőrzése", + "nested-table": { + "no-data": "" + }, "no-values-label": "Nincsenek értékek", "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} / {{numRows}} sor" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "{{count}} irányítópulton használatos" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Keresés név vagy leírás alapján" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Könyvtárpanel betöltése…" @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Az irányítópultot a Grafana már kiépítette és felügyeli a felhőpéldányban. Javasoljuk, hogy a továbbiakban használja a kiépített irányítópultot. Ha továbbra is át szeretné másolni az irányítópultot a felhőpéldányba, akkor módosítsa az irányítópult azonosítóját az irányítópult JSON-fájljában, mentse az új pillanatképet, és töltse fel újra.", "datasource-already-managed": "Az adatforrást a Grafana már kiépítette és felügyeli a felhőpéldányban. Ha ez egy másik erőforrás, állítson be egy másik UID-t, és próbálkozzon újra.", "datasource-invalid-url": "Egy adatforrás érvénytelen URL-címmel rendelkezik. Adjon meg egy érvényes URL-címet, és próbálkozzon újra.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Ez a funkció jelenleg aktív fejlesztés alatt áll. A legjobb élmény és a legújabb fejlesztések érdekében javasoljuk a Grafana <2>éjszakai buildjének használatát." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index e6b6a904801..4ff84a3c438 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -504,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Edit YAML", @@ -1499,15 +1502,12 @@ "group-details": { "ds-features-error": "Kesalahan saat memuat sumber data", "edit": "Edit", - "evaluations-to-fire": "Siklus evaluasi yang akan dinyalakan", "export": "Ekspor", "folder": "Folder", "group-loading-error": "Kesalahan saat memuat grup", "interval": "Interval", "namespace": "Ruang nama", - "pending-period": "Periode tertunda", - "recording": "Pencatatan", - "rule-name": "Nama aturan" + "new": "" }, "group-edit": { "ds-error": "Kesalahan saat memuat sumber data", @@ -2228,6 +2228,9 @@ "label-export-all": "Ekspor semua" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Tidak dapat memuat editor kueri karena: {{errorMessage}}" }, @@ -2637,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Tidak menemukan sumber data yang Anda inginkan? Beberapa sumber data tidak didukung untuk peringatan. Klik ikon untuk informasi selengkapnya." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Tambah Alertmanager baru", "builtin-alertmanager": "Alertmanager bawaan", @@ -2872,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}} lalu)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Folder target tidak kosong, beberapa aturan mungkin ditimpa atau dihapus. Anda yakin ingin mengimpor aturan peringatan ini ke aturan yang dikelola Grafana?", @@ -4381,7 +4394,6 @@ } }, "render-left-actions": { - "text-public": "Publik", "tooltip-view-as-scene": "Tampilkan sebagai Scene" } }, @@ -5154,7 +5166,6 @@ "playlist-next": "Buka dasbor berikutnya", "playlist-previous": "Buka dasbor sebelumnya", "playlist-stop": "Hentikan daftar putar", - "public-dashboard": "Publik", "refresh": "Muat ulang dasbor", "save": "Simpan dasbor", "save-dashboard": { @@ -5455,15 +5466,9 @@ "type": "Jenis" }, "dashboard-preview-banner": { - "not-saved": "Nilai belum disimpan dalam database Grafana", "not-yet-saved": "Nilai tidak disimpan dalam database Grafana", - "open-pull-request-in-git-hub": "Buka permintaan penggabungan di GitHub", - "title-dashboard-loaded-branch-git-hub": "Dasbor ini dimuat dari cabang di GitHub.", "title-dashboard-loaded-external-repository": "Dasbor ini dimuat dari repositori eksternal", - "title-dashboard-loaded-request-git-hub": "Dasbor ini dimuat dari permintaan penggabungan di GitHub.", - "title-error-loading-dashboard": "Kesalahan saat memuat dasbor", - "value-not-saved": "Nilai belum disimpan dalam database Grafana", - "view-pull-request-in-git-hub": "Lihat permintaan penggabungan di GitHub" + "title-error-loading-dashboard": "Kesalahan saat memuat dasbor" }, "dashboard-scene": { "text": { @@ -5644,6 +5649,20 @@ "last-edited": "{{timeAgo}} oleh", "usage-count_other": "Digunakan pada {{count}} dasbor" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Dasbor dengan nama yang sama di folder yang dipilih sudah ada. Ingin tetap menyimpan dasbor ini?", "title-name-already-exists": "Nama sudah ada" @@ -7196,6 +7215,7 @@ "when": "KAPAN" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Pengoperasian" }, "math": { @@ -7970,6 +7990,9 @@ "filter-popup-match-case": "Cocok huruf besar kecil", "inspect-drawer-title": "Periksa nilai", "inspect-menu-label": "Periksa nilai", + "nested-table": { + "no-data": "" + }, "no-values-label": "Tidak ada nilai", "pagination-summary": "{{itemsRangeStart}}—{{displayedEnd}} dari {{numRows}} baris" }, @@ -8479,7 +8502,7 @@ "usage-count_other": "Digunakan pada {{count}} dasbor" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Cari berdasarkan nama atau deskripsi" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Memuat panel pustaka..." @@ -8618,12 +8641,9 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" } } }, @@ -8680,6 +8700,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8692,16 +8713,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8804,6 +8829,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9062,6 +9092,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Dasbor sudah disediakan dan dikelola oleh Grafana di instans cloud. Sebaiknya gunakan dasbor yang disediakan ke depannya. Jika Anda masih ingin menyalin dasbor ke instans cloud, ubah ID dasbor di JSON dasbor, simpan snapshot baru, lalu unggah lagi.", "datasource-already-managed": "Dasbor sudah disediakan dan dikelola oleh Grafana di instans cloud. Jika ini adalah sumber daya yang berbeda, atur UID lain dan coba lagi.", "datasource-invalid-url": "Ada sumber data yang memiliki URL tidak valid. Berikan URL yang valid dan coba lagi.", @@ -10282,6 +10314,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Fitur ini saat ini sedang dalam pengembangan aktif. Untuk pengalaman terbaik dan peningkatan terbaru, kami merekomendasikan Anda untuk menggunakan <2>versi nightly Grafana." @@ -11726,6 +11767,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 836d52d7fbb..a336dbddb1e 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Modifica YAML", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Errore durante il caricamento dei dettagli dell'origine dati", "edit": "Modifica", - "evaluations-to-fire": "Cicli di valutazione da attivare", "export": "Esporta", "folder": "Cartella", "group-loading-error": "Errore durante il caricamento del gruppo", "interval": "Intervallo", "namespace": "Namespace", - "pending-period": "Periodo di attesa", - "recording": "Registrazione", - "rule-name": "Nome regola" + "new": "" }, "group-edit": { "ds-error": "Errore durante il caricamento dei dettagli dell'origine dati", @@ -2237,6 +2237,9 @@ "label-export-all": "Esporta tutto" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Impossibile caricare l'editor di query a causa di: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Non trovi l'origine dei dati che desideri? Alcune origini dei dati non sono supportate per gli avvisi. Fai clic sull'icona per ulteriori informazioni." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Aggiungi nuovo Alertmanager", "builtin-alertmanager": "Alertmanager integrato", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} fa)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "La cartella di destinazione non è vuota, alcune regole potrebbero essere sovrascritte o rimosse. Desideri davvero importare queste regole di avviso nelle regole gestite da Grafana?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Pubblico", "tooltip-view-as-scene": "Visualizza come Scena" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Vai al dashboard successivo", "playlist-previous": "Vai al dashboard precedente", "playlist-stop": "Interrompi playlist", - "public-dashboard": "Pubblico", "refresh": "Aggiorna dashboard", "save": "Salva dashboard", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Tipo" }, "dashboard-preview-banner": { - "not-saved": "Il valore non è ancora stato salvato nel database di Grafana", "not-yet-saved": "Il valore non è stato salvato nel database di Grafana", - "open-pull-request-in-git-hub": "Apri richiesta pull in GitHub", - "title-dashboard-loaded-branch-git-hub": "Questo dashboard viene caricato da un ramo in GitHub.", "title-dashboard-loaded-external-repository": "Questo dashboard viene caricato da un repository esterno", - "title-dashboard-loaded-request-git-hub": "Questo dashboard viene caricato da una richiesta pull in GitHub.", - "title-error-loading-dashboard": "Errore durante il caricamento del dashboard", - "value-not-saved": "Il valore non è ancora stato salvato nel database di Grafana", - "view-pull-request-in-git-hub": "Visualizza richiesta pull in GitHub" + "title-error-loading-dashboard": "Errore durante il caricamento del dashboard" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "Utilizzato su {{count}} dashboard", "usage-count_other": "Utilizzato su {{count}} dashboard" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Esiste già una dashboard con lo stesso nome nella cartella selezionata. Desideri comunque salvare questa dashboard?", "title-name-already-exists": "Nome già in uso" @@ -7217,6 +7236,7 @@ "when": "QUANDO" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operazione" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Corrispondenza", "inspect-drawer-title": "Ispeziona valore", "inspect-menu-label": "Ispeziona valore", + "nested-table": { + "no-data": "" + }, "no-values-label": "Nessun valore", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} di {{numRows}} righe" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "Utilizzato su {{count}} dashboard" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Cerca per nome o descrizione" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Caricamento del pannello della libreria in corso..." @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Il dashboard è già stato predisposto e viene gestito da Grafana nell'istanza cloud. In futuro, ti consigliamo di utilizzare il dashboard con provisioning. Se desideri comunque copiare il dashboard nell'istanza cloud, modifica l'ID del dashboard nel JSON del dashboard, salva una nuova istantanea e carica di nuovo.", "datasource-already-managed": "L'origine dati è già stata predisposta e viene gestita da Grafana nell'istanza cloud. Se si tratta di una risorsa diversa, imposta un altro UID e riprova.", "datasource-invalid-url": "È presente un'origine dati con un URL non valido. Fornisci un URL valido e riprova.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Questa funzione è attualmente in fase di sviluppo attivo. Per un'esperienza ottimale e gli ultimi miglioramenti, consigliamo di utilizzare la <2>nightly build di Grafana." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index e7b17865a55..8bbb86a8c04 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -504,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAMLを編集", @@ -1499,15 +1502,12 @@ "group-details": { "ds-features-error": "データソースの読み込み中にエラーが発生しました", "edit": "編集", - "evaluations-to-fire": "発生する評価サイクル", "export": "エクスポート", "folder": "フォルダ", "group-loading-error": "グループの読み込み中にエラーが発生しました", "interval": "間隔", "namespace": "名前空間", - "pending-period": "保留期間", - "recording": "記録", - "rule-name": "ルール名" + "new": "" }, "group-edit": { "ds-error": "データソースの読み込み中にエラーが発生しました", @@ -2228,6 +2228,9 @@ "label-export-all": "すべてをエクスポート" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "次の理由によりクエリエディタを読み込めませんでした:{{errorMessage}}" }, @@ -2637,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "お探しのデータソースが見つかりませんか?一部のデータソースはアラートに対応していません。詳細についてはアイコンをクリックしてください。" }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "新しいAlertmanagerを追加", "builtin-alertmanager": "内蔵Alertmanager", @@ -2872,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}}前)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "対象フォルダが空ではないため、一部のルールが上書きまたは削除される可能性があります。これらのアラートルールをGrafana管理ルールにインポートしてもよろしいですか?", @@ -4381,7 +4394,6 @@ } }, "render-left-actions": { - "text-public": "パブリック", "tooltip-view-as-scene": "シーンとして表示" } }, @@ -5154,7 +5166,6 @@ "playlist-next": "次のダッシュボードに移動", "playlist-previous": "前のダッシュボードに戻る", "playlist-stop": "プレイリストを停止", - "public-dashboard": "パブリック", "refresh": "ダッシュボードを更新", "save": "ダッシュボードを保存", "save-dashboard": { @@ -5455,15 +5466,9 @@ "type": "タイプ" }, "dashboard-preview-banner": { - "not-saved": "値はまだGrafanaデータベースに保存されていません", "not-yet-saved": "値はGrafanaデータベースに保存されていません", - "open-pull-request-in-git-hub": "GitHubでプルリクエストを開く", - "title-dashboard-loaded-branch-git-hub": "このダッシュボードはGitHubのブランチから読み込まれます。", "title-dashboard-loaded-external-repository": "このダッシュボードは外部リポジトリから読み込まれます", - "title-dashboard-loaded-request-git-hub": "このダッシュボードは、GitHubのプルリクエストから読み込まれます。", - "title-error-loading-dashboard": "ダッシュボードの読み込み中にエラーが発生しました", - "value-not-saved": "値はまだGrafanaデータベースに保存されていません", - "view-pull-request-in-git-hub": "GitHubでプルリクエストを表示する" + "title-error-loading-dashboard": "ダッシュボードの読み込み中にエラーが発生しました" }, "dashboard-scene": { "text": { @@ -5644,6 +5649,20 @@ "last-edited": "が{{timeAgo}}に実施", "usage-count_other": "{{count}}件のダッシュボードで使用中" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "選択したフォルダに同じ名前のダッシュボードがすでに存在します。このダッシュボードの保存を続行しますか?", "title-name-already-exists": "名前はすでに存在します" @@ -7196,6 +7215,7 @@ "when": "条件" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "操作" }, "math": { @@ -7970,6 +7990,9 @@ "filter-popup-match-case": "大文字/小文字を区別", "inspect-drawer-title": "値を検査", "inspect-menu-label": "値を検査", + "nested-table": { + "no-data": "" + }, "no-values-label": "値がありません", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}}({{numRows}}行)" }, @@ -8479,7 +8502,7 @@ "usage-count_other": "{{count}}件のダッシュボードで使用中" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "名前と詳細で検索" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "ライブラリパネル読み込み中..." @@ -8618,12 +8641,9 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" } } }, @@ -8680,6 +8700,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8692,16 +8713,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8804,6 +8829,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9062,6 +9092,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "ダッシュボードは、クラウドインスタンスでGrafanaによってすでにプロビジョニングおよび管理されています。今後は、プロビジョニングされたダッシュボードの使用をお勧めします。それでもダッシュボードをクラウドインスタンスにコピーする場合は、ダッシュボードJSONでダッシュボードIDを変更し、新しいスナップショットを保存して再度アップロードしてください。", "datasource-already-managed": "ダッシュボードは、クラウドインスタンスでGrafanaによってすでにプロビジョニングおよび管理されています。これが別のリソースである場合は、別のUIDを設定して再試行してください。", "datasource-invalid-url": "URLが無効なデータソースがあります。有効なURLを入力して、再試行してください。", @@ -10282,6 +10314,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "この機能は現在開発中です。最高の体験と最新の改善を利用するには、Grafanaの<2>ナイトリービルドの使用をお勧めします。" @@ -11726,6 +11767,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 5ffed66f289..79c29d444c2 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -504,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML 편집", @@ -1499,15 +1502,12 @@ "group-details": { "ds-features-error": "데이터 소스 세부 정보 로딩 중 오류 발생", "edit": "편집", - "evaluations-to-fire": "경고 발생 평가 주기", "export": "내보내기", "folder": "폴더", "group-loading-error": "그룹 로딩 중 오류 발생", "interval": "간격", "namespace": "네임스페이스", - "pending-period": "보류 기간", - "recording": "기록", - "rule-name": "규칙 이름" + "new": "" }, "group-edit": { "ds-error": "데이터 소스 세부 정보 로딩 중 오류 발생", @@ -2228,6 +2228,9 @@ "label-export-all": "모두 내보내기" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "{{errorMessage}}(으)로 인해 쿼리 편집기를 로딩할 수 없습니다" }, @@ -2637,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "원하는 데이터 소스를 찾을 수 없나요? 일부 데이터 소스는 경고용으로 지원되지 않습니다. 자세한 내용을 보려면 아이콘을 클릭하세요." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "새로운 Alertmanager 추가", "builtin-alertmanager": "내장 Alertmanager", @@ -2872,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}} 전)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "대상 폴더가 비어 있지 않습니다. 일부 규칙이 덮어쓰이거나 제거될 수 있습니다. 정말 이 경고 규칙을 Grafana 관리 규칙으로 가져오시겠어요?", @@ -4381,7 +4394,6 @@ } }, "render-left-actions": { - "text-public": "공개", "tooltip-view-as-scene": "씬으로 보기" } }, @@ -5154,7 +5166,6 @@ "playlist-next": "다음 대시보드로 이동", "playlist-previous": "이전 대시보드로 이동", "playlist-stop": "플레이리스트 중지", - "public-dashboard": "공개", "refresh": "대시보드 새로 고침", "save": "대시보드 저장", "save-dashboard": { @@ -5455,15 +5466,9 @@ "type": "유형" }, "dashboard-preview-banner": { - "not-saved": "값이 아직 Grafana 데이터베이스에 저장되지 않았습니다.", "not-yet-saved": "값이 Grafana 데이터베이스에 저장되지 않았습니다.", - "open-pull-request-in-git-hub": "GitHub에서 풀 요청 열기", - "title-dashboard-loaded-branch-git-hub": "이 대시보드는 GitHub의 브랜치에서 로딩됩니다.", "title-dashboard-loaded-external-repository": "이 대시보드는 외부 리포지토리에서 로딩됩니다.", - "title-dashboard-loaded-request-git-hub": "이 대시보드는 GitHub의 풀 요청에서 로딩됩니다.", - "title-error-loading-dashboard": "대시보드 로딩 중 오류 발생", - "value-not-saved": "값이 아직 Grafana 데이터베이스에 저장되지 않았습니다.", - "view-pull-request-in-git-hub": "GitHub에서 풀 요청 보기" + "title-error-loading-dashboard": "대시보드 로딩 중 오류 발생" }, "dashboard-scene": { "text": { @@ -5644,6 +5649,20 @@ "last-edited": " 님({{timeAgo}}에)", "usage-count_other": "{{count}}개의 대시보드에서 사용됨" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "선택한 폴더에 동일한 이름의 대시보드가 이미 존재합니다. 그래도 이 대시보드를 저장하시겠어요?", "title-name-already-exists": "이미 존재하는 이름입니다" @@ -7196,6 +7215,7 @@ "when": "다음과 같은 때" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "연산" }, "math": { @@ -7970,6 +7990,9 @@ "filter-popup-match-case": "대소문자 일치", "inspect-drawer-title": "값 검사", "inspect-menu-label": "값 검사", + "nested-table": { + "no-data": "" + }, "no-values-label": "값 없음", "pagination-summary": "{{numRows}}개의 행 중에서 {{itemsRangeStart}}~{{displayedEnd}}" }, @@ -8479,7 +8502,7 @@ "usage-count_other": "{{count}}개의 대시보드에서 사용됨" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "이름 또는 설명으로 검색" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "라이브러리 패널 로딩 중..." @@ -8618,12 +8641,9 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" } } }, @@ -8680,6 +8700,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8692,16 +8713,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8804,6 +8829,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9062,6 +9092,8 @@ "resource-details": { "dismiss-button": "확인", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "대시보드가 이미 클라우드 인스턴스에서 Grafana에 의해 프로비저닝 및 관리되고 있습니다. 앞으로 프로비저닝된 대시보드를 사용하는 것이 좋습니다. 대시보드를 클라우드 인스턴스에 복사하려는 경우, 대시보드 JSON에서 대시보드 ID를 변경하고 새 스냅샷을 저장한 다음 다시 업로드하세요.", "datasource-already-managed": "데이터 소스가 이미 클라우드 인스턴스에서 Grafana에 의해 프로비저닝 및 관리되고 있습니다. 다른 리소스인 경우 다른 UID를 설정하고 다시 시도하세요.", "datasource-invalid-url": "잘못된 URL을 가진 데이터 소스가 있습니다. 유효한 URL을 입력하고 다시 시도해 주세요.", @@ -10282,6 +10314,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "이 기능은 현재 적극적으로 개발 중입니다. 최상의 경험과 최신 개선 사항 적용을 위해 Grafana의 <2>야간 빌드를 사용하는 것이 좋습니다." @@ -11726,6 +11767,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index ae3b81983b0..fcdb655b5d6 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML bewerken", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Er is een fout opgetreden bij het laden van de gegevensbrondetails", "edit": "Bewerken", - "evaluations-to-fire": "Evaluatiecycli tot activering", "export": "Exporteren", "folder": "Map", "group-loading-error": "Er is een fout opgetreden bij het laden van de groep", "interval": "Interval", "namespace": "Namespace", - "pending-period": "Wachttijd", - "recording": "Opnemen", - "rule-name": "Regelnaam" + "new": "" }, "group-edit": { "ds-error": "Er is een fout opgetreden bij het laden van de gegevensbrondetails", @@ -2237,6 +2237,9 @@ "label-export-all": "Alles exporteren" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Kon querybewerker niet laden vanwege: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Kun je de gewenste gegevensbron niet vinden? Sommige gegevensbronnen worden niet ondersteund voor waarschuwingen. Klik op het pictogram voor meer informatie." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Nieuwe Alertmanager toevoegen", "builtin-alertmanager": "Ingebouwde Alertmanager", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} geleden)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "De doelmap is niet leeg, sommige regels kunnen worden overschreven of verwijderd. Weet je zeker dat je deze waarschuwingsregels wilt importeren naar door Grafana beheerde regels?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Openbaar", "tooltip-view-as-scene": "Weergeven als scène" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Naar het volgende dashboard", "playlist-previous": "Naar het vorige dashboard", "playlist-stop": "Afspeellijst stoppen", - "public-dashboard": "Openbaar", "refresh": "Dashboard vernieuwen", "save": "Dashboard opslaan", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Type" }, "dashboard-preview-banner": { - "not-saved": "De waarde is nog niet opgeslagen in de Grafana-database", "not-yet-saved": "De waarde is niet opgeslagen in de Grafana-database", - "open-pull-request-in-git-hub": "Pull-verzoek openen in GitHub", - "title-dashboard-loaded-branch-git-hub": "Dit dashboard wordt geladen vanuit een filiaal in GitHub.", "title-dashboard-loaded-external-repository": "Dit dashboard wordt geladen vanuit een externe repository", - "title-dashboard-loaded-request-git-hub": "Dit dashboard wordt geladen vanuit een pull-verzoek in GitHub.", - "title-error-loading-dashboard": "Er is een fout opgetreden bij het laden van het dashboard", - "value-not-saved": "De waarde is nog niet opgeslagen in de Grafana-database", - "view-pull-request-in-git-hub": "Pull-verzoek bekijken in GitHub" + "title-error-loading-dashboard": "Er is een fout opgetreden bij het laden van het dashboard" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "Gebruikt op {{count}} dashboards", "usage-count_other": "Gebruikt op {{count}} dashboards" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Er bestaat al een dashboard met dezelfde naam in de geselecteerde map. Wil je dit dashboard nog steeds opslaan?", "title-name-already-exists": "Naam bestaat al" @@ -7217,6 +7236,7 @@ "when": "WANNEER" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Werking" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Case matchen", "inspect-drawer-title": "Waarde inspecteren", "inspect-menu-label": "Waarde inspecteren", + "nested-table": { + "no-data": "" + }, "no-values-label": "Geen waarden", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} van {{numRows}} rijen" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "Gebruikt op {{count}} dashboards" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Zoeken op naam of beschrijving" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Bibliotheekpaneel laden..." @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Dashboard is al provisioned en beheerd door Grafana in de cloudinstantie. We raden je aan om voortaan het provisioned dashboard te gebruiken. Als je het dashboard nog steeds naar de cloud-instantie wilt kopiëren, wijzig dan het dashboard-ID in het dashboard-JSON, sla een nieuw snapshot op en upload opnieuw.", "datasource-already-managed": "Dashboard is al provisioned en beheerd door Grafana in de cloudinstantie. Als dit een andere bron is, stel dan een andere UID in en probeer het opnieuw.", "datasource-invalid-url": "Er is een gegevensbron met een ongeldige URL. Geef een geldige URL op en probeer het opnieuw.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Er wordt momenteel actief gewerkt aan de ontwikkeling van deze functie. Voor de beste ervaring en de nieuwste verbeteringen raden we je aan om de <2>nachtelijke versie van Grafana te gebruiken." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 64f0653bbb3..f3936162e51 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -516,6 +516,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Edytuj YAML", @@ -1517,15 +1520,12 @@ "group-details": { "ds-features-error": "Błąd podczas ładowania szczegółów źródła danych", "edit": "Edytuj", - "evaluations-to-fire": "Cykle oceny do uruchomienia", "export": "Eksport", "folder": "Folder", "group-loading-error": "Błąd podczas ładowania grupy", "interval": "Interwał", "namespace": "Przestrzeń nazw", - "pending-period": "Okres oczekiwania", - "recording": "Nagrywanie", - "rule-name": "Nazwa reguły" + "new": "" }, "group-edit": { "ds-error": "Błąd podczas ładowania szczegółów źródła danych", @@ -2255,6 +2255,9 @@ "label-export-all": "Eksportuj wszystkie" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Nie udało się załadować edytora zapytań. Powód: {{errorMessage}}" }, @@ -2676,6 +2679,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Nie możesz znaleźć odpowiedniego źródła danych? Niektóre źródła danych nie są obsługiwane w przypadku alertów. Aby dowiedzieć się więcej, kliknij ikonę." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Dodaj nowego menedżera alertów", "builtin-alertmanager": "Wbudowany menedżer alertów", @@ -2911,6 +2919,11 @@ "timestamp": { "time-ago": "({{time}} temu)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Folder docelowy nie jest pusty. Niektóre reguły mogą zostać zastąpione lub usunięte. Na pewno chcesz zaimportować te reguły alertów do reguł zarządzanych przez usługę Grafana?", @@ -4435,7 +4448,6 @@ } }, "render-left-actions": { - "text-public": "Publiczny", "tooltip-view-as-scene": "Wyświetl jako scenę" } }, @@ -5211,7 +5223,6 @@ "playlist-next": "Przejdź do następnego pulpitu", "playlist-previous": "Przejdź do poprzedniego pulpitu", "playlist-stop": "Zatrzymaj autoodtwarzanie", - "public-dashboard": "Publiczny", "refresh": "Odśwież pulpit", "save": "Zapisz pulpit", "save-dashboard": { @@ -5512,15 +5523,9 @@ "type": "Typ" }, "dashboard-preview-banner": { - "not-saved": "Wartość nie została jeszcze zapisana w bazie danych Grafana", "not-yet-saved": "Wartość nie została zapisana w bazie danych Grafana", - "open-pull-request-in-git-hub": "Otwórz pull request w GitHub", - "title-dashboard-loaded-branch-git-hub": "Ten pulpit nawigacyjny jest ładowany z gałęzi w GitHub.", "title-dashboard-loaded-external-repository": "Ten pulpit nawigacyjny jest ładowany z zewnętrznego repozytorium", - "title-dashboard-loaded-request-git-hub": "Ten pulpit nawigacyjny jest ładowany z pull request w GitHub.", - "title-error-loading-dashboard": "Błąd wczytywania pulpitu", - "value-not-saved": "Wartość nie została jeszcze zapisana w bazie danych Grafana", - "view-pull-request-in-git-hub": "Wyświetl pull request w GitHub" + "title-error-loading-dashboard": "Błąd wczytywania pulpitu" }, "dashboard-scene": { "text": { @@ -5704,6 +5709,20 @@ "usage-count_many": "Używane na {{count}} pulpitach", "usage-count_other": "Używane na {{count}} pulpitach" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "W wybranym folderze istnieje już pulpit o tej samej nazwie. Czy nadal chcesz zapisać ten pulpit?", "title-name-already-exists": "Nazwa już istnieje" @@ -7259,6 +7278,7 @@ "when": "KIEDY" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operacja" }, "math": { @@ -8033,6 +8053,9 @@ "filter-popup-match-case": "Dopasuj wielkość liter", "inspect-drawer-title": "Sprawdź wartość", "inspect-menu-label": "Sprawdź wartość", + "nested-table": { + "no-data": "" + }, "no-values-label": "Brak wartości", "pagination-summary": "{{itemsRangeStart}}–{{displayedEnd}} z {{numRows}} wierszy" }, @@ -8551,7 +8574,7 @@ "usage-count_other": "Używane na {{count}} pulpitu" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Szukaj według nazwy lub opisu" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Wczytywanie panelu biblioteki…" @@ -8696,12 +8719,18 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_few": "", + "indexed-label_many": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_few": "", + "parsedl-label_many": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_few": "", + "structured-metadata_many": "", + "structured-metadata_other": "" } } }, @@ -8758,6 +8787,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8770,16 +8800,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8882,6 +8916,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9140,6 +9179,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Pulpit jest już skonfigurowany w usłudze Grafana i zarządzany z jej poziomu w instancji w chmurze. Zalecamy korzystanie z pulpitu skonfigurowanego. Jeśli nadal chcesz skopiować pulpit do instancji w chmurze, zmień identyfikator pulpitu w jego pliku JSON, zapisz nową migawkę i prześlij ponownie.", "datasource-already-managed": "Źródło danych jest już skonfigurowane w usłudze Grafana i zarządzane z jej poziomu w instancji w chmurze. Jeśli jest to inny zasób, ustaw inny UID i spróbuj ponownie.", "datasource-invalid-url": "Istnieje źródło danych, które ma nieprawidłowy adres URL. Podaj prawidłowy adres URL i spróbuj ponownie.", @@ -10369,6 +10410,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Ta funkcja jest obecnie aktywnie rozwijana. Aby uzyskać najlepsze wrażenia i najnowsze ulepszenia, zalecamy korzystanie z <2>nocnej kompilacji aplikacji Grafana." @@ -11834,6 +11884,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 30874326182..af641b53d45 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Editar YAML", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Erro ao carregar detalhes da fonte de dados", "edit": "Editar", - "evaluations-to-fire": "Ciclos de avaliação para acionar", "export": "Exportar", "folder": "Pasta", "group-loading-error": "Erro ao carregar o grupo", "interval": "Intervalo", "namespace": "Nomenclatura", - "pending-period": "Período pendente", - "recording": "Registrando", - "rule-name": "Nome da regra" + "new": "" }, "group-edit": { "ds-error": "Erro ao carregar detalhes da fonte de dados", @@ -2237,6 +2237,9 @@ "label-export-all": "Exportar tudo" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Não foi possível carregar o editor de consultas devido a: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Não está encontrando a fonte de dados que você procura? Algumas fontes de dados não são compatíveis com alertas. Clique no ícone para mais informações." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Adicionar novo Alertmanager", "builtin-alertmanager": "Alertmanager integrado", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "(há {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "A pasta de destino não está vazia; algumas regras podem ser substituídas ou removidas. Tem certeza de que deseja importar essas regras de alerta para as regras gerenciadas pela Grafana?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Pública", "tooltip-view-as-scene": "Visualizar como cena" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Ir para o próximo painel de controle", "playlist-previous": "Ir para o painel de controle anterior", "playlist-stop": "Parar lista de reprodução", - "public-dashboard": "Público", "refresh": "Atualizar painel de controle", "save": "Salvar painel de controle", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Tipo" }, "dashboard-preview-banner": { - "not-saved": "O valor ainda não está salvo no banco de dados da Grafana", "not-yet-saved": "O valor não está salvo no banco de dados da Grafana", - "open-pull-request-in-git-hub": "Abrir solicitação de pull no GitHub", - "title-dashboard-loaded-branch-git-hub": "Este painel de controle é carregado a partir de um branch no GitHub.", "title-dashboard-loaded-external-repository": "Este painel de controle é carregado a partir de um repositório externo", - "title-dashboard-loaded-request-git-hub": "Este painel de controle é carregado a partir de uma solicitação de pull no GitHub.", - "title-error-loading-dashboard": "Erro ao carregar o painel de controle", - "value-not-saved": "O valor ainda não está salvo no banco de dados da Grafana", - "view-pull-request-in-git-hub": "Ver solicitação de pull no GitHub" + "title-error-loading-dashboard": "Erro ao carregar o painel de controle" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "Usado em {{count}} painéis", "usage-count_other": "Usado em {{count}} painéis" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Já existe um painel com o mesmo nome na pasta selecionada. Deseja salvar este painel mesmo assim?", "title-name-already-exists": "O nome já existe" @@ -7217,6 +7236,7 @@ "when": "QUANDO" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operação" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Diferenciar maiúsculas/minúsculas", "inspect-drawer-title": "Inspecionar valor", "inspect-menu-label": "Inspecionar valor", + "nested-table": { + "no-data": "" + }, "no-values-label": "Sem valores", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} de {{numRows}} linhas" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "Usado em {{count}} painéis" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Pesquisar por nome ou descrição" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Carregando painel da biblioteca…" @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "O painel de controle já está provisionado e é gerenciado pela Grafana na instância na nuvem. Recomendamos que você passe a usar o painel de controle provisionado. Se você ainda quiser copiar o painel de controle para a instância na nuvem, altere o ID do painel de controle no JSON do painel de controle, salve uma nova captura e envie novamente.", "datasource-already-managed": "A fonte de dados já está provisionada e é gerenciada pela Grafana na instância na nuvem. Caso se trate de outro recurso, defina outro UID e tente novamente.", "datasource-invalid-url": "Há uma fonte de dados com uma URL inválida. Forneça uma URL válida e tente novamente.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Este recurso está em desenvolvimento ativo no momento. Para ter uma melhor experiência e conferir as melhorias mais recentes, recomendamos usar a <2>compilação noturna da Grafana." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 24b4f9ad50f..dee16762e41 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Editar YAML", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Erro ao carregar a detalhes da origem dos dados", "edit": "Editar", - "evaluations-to-fire": "Ciclos de avaliação a disparar", "export": "Exportar", "folder": "Pasta", "group-loading-error": "Erro ao carregar o grupo", "interval": "Intervalo", "namespace": "Espaço de nome", - "pending-period": "Período pendente", - "recording": "Gravação", - "rule-name": "Nome da regra" + "new": "" }, "group-edit": { "ds-error": "Erro ao carregar a detalhes da origem dos dados", @@ -2237,6 +2237,9 @@ "label-export-all": "Exportar tudo" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Não foi possível carregar o editor de consultas devido a: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Não encontra a origem de dados que pretende? Algumas origens de dados não são suportadas para alertas. Clique no ícone para obter mais informações." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Adicionar novo Alertmanager", "builtin-alertmanager": "Alertmanager integrado", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "(há {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "A pasta de destino não está vazia, algumas regras podem ser substituídas ou removidas. Tem a certeza de que pretende importar estas regras de alerta para as regras geridas pela Grafana?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Público", "tooltip-view-as-scene": "Ver como cena" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Ir para o próximo painel de controlo", "playlist-previous": "Ir para o painel de controlo anterior", "playlist-stop": "Parar a lista de reprodução", - "public-dashboard": "Público", "refresh": "Atualizar o painel de controlo", "save": "Guardar o painel de controlo", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Tipo" }, "dashboard-preview-banner": { - "not-saved": "O valor ainda não está guardado na base de dados da Grafana", "not-yet-saved": "O valor não está guardado na base de dados da Grafana", - "open-pull-request-in-git-hub": "Abrir pedido pull no GitHub", - "title-dashboard-loaded-branch-git-hub": "Este painel de controlo é carregado a partir de um ramo no GitHub.", "title-dashboard-loaded-external-repository": "Este painel de controlo é carregado a partir de um repositório externo", - "title-dashboard-loaded-request-git-hub": "Este painel de controlo é carregado a partir de um pedido pull no GitHub.", - "title-error-loading-dashboard": "Erro ao carregar o painel de controlo", - "value-not-saved": "O valor ainda não está guardado na base de dados da Grafana", - "view-pull-request-in-git-hub": "Visualizar pedido pull no GitHub" + "title-error-loading-dashboard": "Erro ao carregar o painel de controlo" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "Utilizado em {{count}} painéis de controlo", "usage-count_other": "Utilizado em {{count}} painéis de controlo" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Já existe um painel de controlo com o mesmo nome na pasta selecionada. Ainda pretende guardar este painel de controlo?", "title-name-already-exists": "Nome já existente" @@ -7217,6 +7236,7 @@ "when": "QUANDO" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Funcionamento" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Diferenciar maiúsculas de minúsculas", "inspect-drawer-title": "Inspecionar valor", "inspect-menu-label": "Inspecionar valor", + "nested-table": { + "no-data": "" + }, "no-values-label": "Nenhum valor", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} de {{numRows}} linhas" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "Utilizado em {{count}} painéis de controlo" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Pesquisar por nome ou descrição" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "A carregar painel de biblioteca..." @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "O painel de controlo já está provisionado e gerido pela Grafana na instância de nuvem. Recomendamos a utilização do painel de controlo provisionado a partir de agora. Se ainda pretender copiar o painel de controlo para a instância de nuvem, altere o ID do painel de controlo no JSON do painel de controlo, guarde um novo instantâneo e carregue novamente.", "datasource-already-managed": "A origem de dados já está provisionada e gerida pela Grafana na instância de nuvem. Se este for um recurso diferente, defina outro UID e tente novamente.", "datasource-invalid-url": "Existe uma origem de dados com um URL inválido. Forneça um URL válido e tente novamente.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Esta funcionalidade está atualmente em desenvolvimento ativo. Para a melhor experiência e as melhorias mais recentes, recomendamos a utilização da <2>compilação noturna da Grafana." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index d5eb61aad8f..f84ae7d9c8b 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -516,6 +516,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Редактировать YAML", @@ -1517,15 +1520,12 @@ "group-details": { "ds-features-error": "Ошибка при загрузке сведений об источнике данных", "edit": "Редактировать", - "evaluations-to-fire": "Циклы оценки для отправки оповещений", "export": "Экспорт", "folder": "Папка", "group-loading-error": "Ошибка при загрузке группы", "interval": "Интервал", "namespace": "Пространство имен", - "pending-period": "Период ожидания", - "recording": "Запись", - "rule-name": "Имя правила" + "new": "" }, "group-edit": { "ds-error": "Ошибка при загрузке сведений об источнике данных", @@ -2255,6 +2255,9 @@ "label-export-all": "Экспортировать всё" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Не удалось загрузить редактор запросов. Причина: {{errorMessage}}" }, @@ -2676,6 +2679,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Не удается найти нужный источник данных? Некоторые источники данных не поддерживаются для функции оповещения. Нажмите значок для получения подробной информации." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Добавить новый обработчик оповещений Alertmanager", "builtin-alertmanager": "Встроенный обработчик оповещений Alertmanager", @@ -2911,6 +2919,11 @@ "timestamp": { "time-ago": "({{time}} назад)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Целевая папка не пуста, некоторые правила могут быть перезаписаны или удалены. Действительно импортировать эти правила оповещения в правила, управляемые Grafana?", @@ -4435,7 +4448,6 @@ } }, "render-left-actions": { - "text-public": "Общедоступный", "tooltip-view-as-scene": "Просмотр в виде сцены" } }, @@ -5211,7 +5223,6 @@ "playlist-next": "Перейти к следующему дашборду", "playlist-previous": "Перейти к предыдущему дашборду", "playlist-stop": "Остановить плейлист", - "public-dashboard": "Общедоступный", "refresh": "Обновить дашборд", "save": "Сохранить дашборд", "save-dashboard": { @@ -5512,15 +5523,9 @@ "type": "Тип" }, "dashboard-preview-banner": { - "not-saved": "Значение еще не сохранено в базе данных Grafana", "not-yet-saved": "Значение не сохранено в базе данных Grafana", - "open-pull-request-in-git-hub": "Открыть запрос на включение изменений в GitHub", - "title-dashboard-loaded-branch-git-hub": "Дашборд загружается из ветви в GitHub.", "title-dashboard-loaded-external-repository": "Дашборд загружается из внешнего репозитория", - "title-dashboard-loaded-request-git-hub": "Дашборд загружается из запроса на включение изменений в GitHub.", - "title-error-loading-dashboard": "Ошибка при загрузке дашборда", - "value-not-saved": "Значение еще не сохранено в базе данных Grafana", - "view-pull-request-in-git-hub": "Просмотр запроса на включение изменений в GitHub" + "title-error-loading-dashboard": "Ошибка при загрузке дашборда" }, "dashboard-scene": { "text": { @@ -5704,6 +5709,20 @@ "usage-count_many": "Используется на {{count}} дашбордах", "usage-count_other": "Используется на {{count}} дашбордах" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Дашборд с таким названием уже существует в выбранной папке. Все равно сохранить дашборд?", "title-name-already-exists": "Название уже существует" @@ -7259,6 +7278,7 @@ "when": "КОГДА" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Операция" }, "math": { @@ -8033,6 +8053,9 @@ "filter-popup-match-case": "С учетом регистра", "inspect-drawer-title": "Проверить значение", "inspect-menu-label": "Проверить значение", + "nested-table": { + "no-data": "" + }, "no-values-label": "Нет значений", "pagination-summary": "Строки: {{itemsRangeStart}} - {{displayedEnd}} из {{numRows}}" }, @@ -8551,7 +8574,7 @@ "usage-count_other": "Используется на {{count}} дашборда" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Поиск по названию или описанию" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Загрузка панели библиотеки..." @@ -8696,12 +8719,18 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_few": "", + "indexed-label_many": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_few": "", + "parsedl-label_many": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_few": "", + "structured-metadata_many": "", + "structured-metadata_other": "" } } }, @@ -8758,6 +8787,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8770,16 +8800,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8882,6 +8916,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9140,6 +9179,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Дашборд уже подготовлен в облачном экземпляре и управляется Grafana. Рекомендуем в будущем использовать подготовленный дашборд. Если вы всё же хотите скопировать дашборд в облачный экземпляр, измените его идентификатор в JSON-файле, сохраните новый снимок и загрузите его снова.", "datasource-already-managed": "Источник данных уже подготовлен в облачном экземпляре и управляется Grafana. Если это другой ресурс, установите другой UID и повторите попытку.", "datasource-invalid-url": "Существует источник данных с недопустимым URL-адресом. Укажите действительный URL-адрес и повторите попытку.", @@ -10369,6 +10410,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "В настоящее время эта функция находится в стадии активной разработки. Чтобы обеспечить максимальное удобство пользования и получить доступ к последним улучшениям, рекомендуем использовать <2>ночную сборку Grafana." @@ -11834,6 +11884,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 4f7bb6d9dec..3d81843fb04 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Redigera YAML", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Fel vid laddning av information för datakälla", "edit": "Redigera", - "evaluations-to-fire": "Utvärderingscykler att utlösa", "export": "Exportera", "folder": "Mapp", "group-loading-error": "Fel vid laddning av gruppen", "interval": "Intervall", "namespace": "Namnområde", - "pending-period": "Väntande period", - "recording": "Registrering", - "rule-name": "Regelns namn" + "new": "" }, "group-edit": { "ds-error": "Fel vid laddning av information för datakälla", @@ -2237,6 +2237,9 @@ "label-export-all": "Exportera alla" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Kunde inte ladda frågeredigerare på grund av: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Hittar du inte den datakälla du söker? Vissa datakällor stöds inte för larm. Klicka på ikonen för mer information." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Lägg till ny Alertmanager", "builtin-alertmanager": "Inbyggd Alertmanager", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} sedan)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Målmappen är inte tom, vissa regler kan skrivas över eller tas bort. Är du säker på att du vill importera dessa larmregler till Grafana-hanterade regler?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Offentlig", "tooltip-view-as-scene": "Visa som scen" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Gå till nästa instrumentpanel", "playlist-previous": "Gå till föregående instrumentpanel", "playlist-stop": "Stoppa spellista", - "public-dashboard": "Offentlig", "refresh": "Uppdatera instrumentpanel", "save": "Spara instrumentpanel", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Typ" }, "dashboard-preview-banner": { - "not-saved": "Värdet har ännu inte sparats i Grafana-databasen", "not-yet-saved": "Värdet sparades inte i Grafana-databasen", - "open-pull-request-in-git-hub": "Öppna pull-begäran i GitHub", - "title-dashboard-loaded-branch-git-hub": "Den här instrumentpanelen laddas från en gren i GitHub.", "title-dashboard-loaded-external-repository": "Den här instrumentpanelen laddas från ett externt arkiv", - "title-dashboard-loaded-request-git-hub": "Den här instrumentpanelen laddas från en pull-begäran i GitHub.", - "title-error-loading-dashboard": "Fel vid laddning av instrumentpanel", - "value-not-saved": "Värdet har ännu inte sparats i Grafana-databasen", - "view-pull-request-in-git-hub": "Visa pull-begäran i GitHub" + "title-error-loading-dashboard": "Fel vid laddning av instrumentpanel" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "Används på {{count}} instrumentpaneler", "usage-count_other": "Används på {{count}} instrumentpaneler" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "En instrumentpanel med samma namn finns redan i den valda mappen. Vill du fortfarande spara denna instrumentpanel?", "title-name-already-exists": "Namnet finns redan" @@ -7217,6 +7236,7 @@ "when": "NÄR" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Drift" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Matcha gemener/versaler", "inspect-drawer-title": "Inspektera värde", "inspect-menu-label": "Inspektera värde", + "nested-table": { + "no-data": "" + }, "no-values-label": "Inga värden", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} av {{numRows}} rader" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "Används på {{count}} instrumentpaneler" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Sök efter namn eller beskrivning" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Laddar bibliotekspanel …" @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Instrumentpanelen tillhandahålls och hanteras redan av Grafana i molninstansen. Vi rekommenderar att du använder den tillhandahållna instrumentpanelen framöver. Om du fortfarande vill kopiera instrumentpanelens molninstans ändrar du instrumentpanelens ID i instrumentpanelens JSON, sparar en ny ögonblicksbild och laddar sedan upp på nytt.", "datasource-already-managed": "Datakällan tillhandahålls och hanteras redan av Grafana i molninstansen. Om detta är en annan resurs, ange ett annat UID och försök igen.", "datasource-invalid-url": "Det finns en datakälla som har en ogiltig webbadress. Ange en giltig webbadress och försök igen.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Den här funktionen är för närvarande under aktiv utveckling. För den bästa upplevelsen och de senaste förbättringarna rekommenderar vi att du använder <2> nightly-versionen av Grafana." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index c8e93d19834..262db9aa789 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -508,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML düzenle", @@ -1505,15 +1508,12 @@ "group-details": { "ds-features-error": "Veri kaynağı ayrıntıları yüklenirken hata oluştu", "edit": "Düzenle", - "evaluations-to-fire": "Tetiklenecek değerlendirme döngüleri", "export": "Dışa aktar", "folder": "Klasör", "group-loading-error": "Grup yüklenirken hata oluştu", "interval": "Aralık", "namespace": "Ad alanı", - "pending-period": "Bekleme süresi", - "recording": "Kayıt", - "rule-name": "Kural adı" + "new": "" }, "group-edit": { "ds-error": "Veri kaynağı ayrıntıları yüklenirken hata oluştu", @@ -2237,6 +2237,9 @@ "label-export-all": "Tümünü dışa aktar" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Sorgu düzenleyici şu nedenle yüklenemedi: {{errorMessage}}" }, @@ -2650,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "İstediğiniz veri kaynağını bulamıyor musunuz? Bazı veri kaynakları uyarılar için desteklenmiyor. Daha fazla bilgi için simgeye tıklayın." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Yeni Alertmanager ekle", "builtin-alertmanager": "Yerleşik Alertmanager", @@ -2885,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} önce)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Hedef klasör boş değil, bazı kuralların üzerine yazılabilir veya silinebilir. Bu uyarı kurallarını Grafana tarafından yönetilen kurallara içe aktarmak istediğinize emin misiniz?", @@ -4399,7 +4412,6 @@ } }, "render-left-actions": { - "text-public": "Herkese açık", "tooltip-view-as-scene": "Sahne olarak görüntüle" } }, @@ -5173,7 +5185,6 @@ "playlist-next": "Sonraki panoya git", "playlist-previous": "Önceki panoya git", "playlist-stop": "Oynatma listesini durdur", - "public-dashboard": "Herkese açık", "refresh": "Panoyu yenile", "save": "Panoyu kaydet", "save-dashboard": { @@ -5474,15 +5485,9 @@ "type": "Tür" }, "dashboard-preview-banner": { - "not-saved": "Değer henüz Grafana veri tabanına kaydedilmedi", "not-yet-saved": "Değer Grafana veri tabanına kaydedilmedi", - "open-pull-request-in-git-hub": "GitHub'da çekme isteği aç", - "title-dashboard-loaded-branch-git-hub": "Bu pano, GitHub'daki bir daldan yüklendi.", "title-dashboard-loaded-external-repository": "Bu pano, haricî bir depodan yüklendi.", - "title-dashboard-loaded-request-git-hub": "Bu pano, GitHub'daki bir çekme isteğinden yüklendi.", - "title-error-loading-dashboard": "Pano yüklenirken hata oluştu", - "value-not-saved": "Değer henüz Grafana veri tabanına kaydedilmedi", - "view-pull-request-in-git-hub": "GitHub'da çekme isteğini görüntüle" + "title-error-loading-dashboard": "Pano yüklenirken hata oluştu" }, "dashboard-scene": { "text": { @@ -5664,6 +5669,20 @@ "usage-count_one": "{{count}} panoda kullanılıyor", "usage-count_other": "{{count}} panoda kullanılıyor" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Seçilen klasörde aynı isimde bir pano zaten mevcut. Yine de bu panoyu kaydetmek istiyor musunuz?", "title-name-already-exists": "Bu ad zaten mevcut" @@ -7217,6 +7236,7 @@ "when": "NE ZAMAN" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Çalıştırma" }, "math": { @@ -7991,6 +8011,9 @@ "filter-popup-match-case": "Büyük/küçük harf duyarlılığı", "inspect-drawer-title": "Değeri incele", "inspect-menu-label": "Değeri incele", + "nested-table": { + "no-data": "" + }, "no-values-label": "Değer yok", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}}/{{numRows}} satır" }, @@ -8503,7 +8526,7 @@ "usage-count_other": "{{count}} panoda kullanılıyor" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Ada veya açıklamaya göre ara" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Kütüphane paneli yükleniyor..." @@ -8644,12 +8667,12 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" } } }, @@ -8706,6 +8729,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8718,16 +8742,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8830,6 +8858,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9088,6 +9121,8 @@ "resource-details": { "dismiss-button": "Tamam", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Pano zaten bulut örneğinde Grafana tarafından sağlanmış ve yönetilmektedir. Bundan sonra sağlanan panoyu kullanmanızı öneririz. Yine de panoyu bulut örneğine kopyalamak istiyorsanız pano JSON'unda pano kimliğini değiştirin, yeni bir anlık görüntü kaydedin ve tekrar yükleyin.", "datasource-already-managed": "Veri kaynağı zaten bulut örneğinde Grafana tarafından sağlanmış ve yönetilmektedir. Bu farklı bir kaynaksa başka bir UID belirleyin ve tekrar deneyin.", "datasource-invalid-url": "Geçersiz bir URL içeren bir veri kaynağı var. Geçerli bir URL girin ve tekrar deneyin.", @@ -10311,6 +10346,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Bu özellik şu anda aktif geliştirme aşamasındadır. En iyi deneyim ve en yeni geliştirmeler için Grafana'nın <2>gecelik derleme sürümünü kullanmanızı öneririz." @@ -11762,6 +11806,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 0d0c16a9ff1..bfaef6559b7 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -504,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "编辑 YAML", @@ -1499,15 +1502,12 @@ "group-details": { "ds-features-error": "加载数据源详情时出错", "edit": "编辑", - "evaluations-to-fire": "待触发的评估周期", "export": "导出", "folder": "文件夹", "group-loading-error": "加载组时出错", "interval": "间隔", "namespace": "命名空间", - "pending-period": "待处理时段", - "recording": "录制", - "rule-name": "规则名称" + "new": "" }, "group-edit": { "ds-error": "加载数据源详情时出错", @@ -2228,6 +2228,9 @@ "label-export-all": "导出全部" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "由于以下原因,无法加载查询编辑器:{{errorMessage}}" }, @@ -2637,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "找不到您想要的数据源?某些数据源不支持警报。点击图标以获取更多信息。" }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "添加新的 Alertmanager", "builtin-alertmanager": "内置 Alertmanager", @@ -2872,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}}前)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "目标文件夹不为空,某些规则可能会被覆盖或移除。您确定要将这些警报规则导入到 Grafana 管理的规则吗?", @@ -4381,7 +4394,6 @@ } }, "render-left-actions": { - "text-public": "公开", "tooltip-view-as-scene": "作为场景查看" } }, @@ -5154,7 +5166,6 @@ "playlist-next": "前往下一个仪表板", "playlist-previous": "前往上一个仪表板", "playlist-stop": "停止播放列表", - "public-dashboard": "公开", "refresh": "刷新仪表板", "save": "保存仪表板", "save-dashboard": { @@ -5455,15 +5466,9 @@ "type": "类型" }, "dashboard-preview-banner": { - "not-saved": "该值尚未保存在 Grafana 数据库中", "not-yet-saved": "该值未保存在 Grafana 数据库中", - "open-pull-request-in-git-hub": "在 GitHub 中打开拉取请求", - "title-dashboard-loaded-branch-git-hub": "此数据面板从 GitHub 的分支加载。", "title-dashboard-loaded-external-repository": "此数据面板从外部存储库加载", - "title-dashboard-loaded-request-git-hub": "此数据面板从 GitHub 的拉取请求加载。", - "title-error-loading-dashboard": "加载数据面板时出错", - "value-not-saved": "该值尚未保存在 Grafana 数据库中", - "view-pull-request-in-git-hub": "在 GitHub 中查看拉取请求" + "title-error-loading-dashboard": "加载数据面板时出错" }, "dashboard-scene": { "text": { @@ -5644,6 +5649,20 @@ "last-edited": "{{timeAgo}}由 ", "usage-count_other": "用于 {{count}} 个数据面板" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "所选文件夹中已存在具有相同名称的数据面板。您仍然要保存此数据面板吗?", "title-name-already-exists": "名称已存在" @@ -7196,6 +7215,7 @@ "when": "满足以下条件时" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "运算" }, "math": { @@ -7970,6 +7990,9 @@ "filter-popup-match-case": "区分大小写", "inspect-drawer-title": "检查值", "inspect-menu-label": "检查值", + "nested-table": { + "no-data": "" + }, "no-values-label": "没有值", "pagination-summary": "第 {{itemsRangeStart}} - {{displayedEnd}} 行,共 {{numRows}} 行" }, @@ -8479,7 +8502,7 @@ "usage-count_other": "用于 {{count}} 个数据面板" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "按名称或描述搜索" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "正在加载库面板..." @@ -8618,12 +8641,9 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" } } }, @@ -8680,6 +8700,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8692,16 +8713,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8804,6 +8829,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9062,6 +9092,8 @@ "resource-details": { "dismiss-button": "好", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "数据面板已由 Grafana 在云实例中配置和管理。我们建议您今后使用已配置的数据面板。如果您仍然希望将数据面板复制到云实例,请更改数据面板 JSON 中的数据面板 ID,保存新的快照并再次上传。", "datasource-already-managed": "数据源已由 Grafana 在云实例中配置和管理。如果这是其他资源,请设置另一个 UID,然后重试。", "datasource-invalid-url": "有一个数据源的 URL 无效。请提供有效的 URL,然后重试。", @@ -10282,6 +10314,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "此功能目前正在积极开发中。为了获得最佳体验和最新改进,我们建议您使用 Grafana 的<2>夜间版本。" @@ -11726,6 +11767,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 52210f14da4..e741a02996c 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -504,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "編輯 YAML", @@ -1499,15 +1502,12 @@ "group-details": { "ds-features-error": "載入資料來源詳細資料時發生錯誤", "edit": "編輯", - "evaluations-to-fire": "待觸發的評估週期", "export": "匯出", "folder": "資料夾", "group-loading-error": "載入群組時發生錯誤", "interval": "間隔", "namespace": "命名空間", - "pending-period": "待處理期間", - "recording": "錄製", - "rule-name": "規則名稱" + "new": "" }, "group-edit": { "ds-error": "載入資料來源詳細資料時發生錯誤", @@ -2228,6 +2228,9 @@ "label-export-all": "匯出全部" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "由於以下原因,無法載入查詢編輯器:{{errorMessage}}" }, @@ -2637,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "找不到您想要的資料來源嗎?某些資料來源不支援警報。按一下該圖示了解更多資訊。" }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "新增 Alertmanager", "builtin-alertmanager": "內建的 Alertmanager", @@ -2872,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}} 前)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "目標資料夾不為空,某些規則可能會被覆寫或移除。確定要將這些警報規則匯入至 Grafana 管理的規則嗎?", @@ -4381,7 +4394,6 @@ } }, "render-left-actions": { - "text-public": "公共", "tooltip-view-as-scene": "以場景形式檢視" } }, @@ -5154,7 +5166,6 @@ "playlist-next": "前往下一個儀表板", "playlist-previous": "前往上一個儀表板", "playlist-stop": "停止播放清單", - "public-dashboard": "公共", "refresh": "重新整理儀表板", "save": "儲存儀表板", "save-dashboard": { @@ -5455,15 +5466,9 @@ "type": "類型" }, "dashboard-preview-banner": { - "not-saved": "該數值尚未儲存在 Grafana 資料庫中", "not-yet-saved": "該數值未儲存在 Grafana 資料庫中", - "open-pull-request-in-git-hub": "在 GitHub 中開啟拉取請求", - "title-dashboard-loaded-branch-git-hub": "此儀表板是從 GitHub 的分支載入。", "title-dashboard-loaded-external-repository": "此儀表板是從外部存放庫載入", - "title-dashboard-loaded-request-git-hub": "此儀表板是從 GitHub 的拉取請求載入。", - "title-error-loading-dashboard": "載入控制面板發生錯誤", - "value-not-saved": "該數值尚未儲存在 Grafana 資料庫中", - "view-pull-request-in-git-hub": "在 GitHub 中檢視拉取請求" + "title-error-loading-dashboard": "載入控制面板發生錯誤" }, "dashboard-scene": { "text": { @@ -5644,6 +5649,20 @@ "last-edited": "在 {{timeAgo}}前", "usage-count_other": "用於 {{count}} 個儀表板" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "所選資料夾中已存在名稱相同的儀表板。仍要儲存此儀表板嗎?", "title-name-already-exists": "名稱已存在" @@ -7196,6 +7215,7 @@ "when": "當" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "操作" }, "math": { @@ -7970,6 +7990,9 @@ "filter-popup-match-case": "區分大小寫", "inspect-drawer-title": "檢查值", "inspect-menu-label": "檢查值", + "nested-table": { + "no-data": "" + }, "no-values-label": "沒有數值", "pagination-summary": "{{itemsRangeStart}} - 第 {{displayedEnd}} 列,共 {{numRows}} 列" }, @@ -8479,7 +8502,7 @@ "usage-count_other": "用於 {{count}} 個儀表板" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "按名稱或描述搜尋" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "正在載入資料庫面板…" @@ -8618,12 +8641,9 @@ "fields": { "type": { "loki": { - "indexed-label": "", - "indexed-label-plural": "", - "parsed-label-plural": "", - "parsedl-label": "", - "structured-metadata": "", - "structured-metadata-plural": "" + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" } } }, @@ -8680,6 +8700,7 @@ "close": "", "copy-shortlink": "", "copy-to-clipboard": "", + "displayed-fields-section": "", "fields": { "adhoc-statistics": "", "copy-value-to-clipboard": "", @@ -8692,16 +8713,20 @@ }, "fields-section": "", "hide-log-line": "", + "inline-mode": "", "links-section": "", + "log-line-field": "", "log-line-section": "", "no-details": "", "pin-line": "", + "remove-displayed-field": "", "search": { "no-results": "" }, "search-placeholder": "", "show-context": "", "show-log-line": "", + "sidebar-mode": "", "unpin-line": "" }, "log-line-menu": { @@ -8804,6 +8829,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -9062,6 +9092,8 @@ "resource-details": { "dismiss-button": "確定", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "儀表板已由 Grafana 在雲端執行個體中佈建和管理。我們建議您將來使用已設定的儀表板。若您仍然希望將儀表板複製至雲端執行個體,請變更儀表板 JSON 中的儀表板 ID,儲存新的快照並重新上傳。", "datasource-already-managed": "資料來源已由 Grafana 在雲端執行個體中佈建和管理。如果這是不同的資源,請設定另一個 UID 並再試一次。", "datasource-invalid-url": "有一個資料來源的網址無效。請提供有效的網址,然後再試一次。", @@ -10282,6 +10314,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "此功能目前正在積極開發中。為了獲得最佳體驗和最新改進版本,建議使用 Grafana 的<2>夜間版本。" @@ -11726,6 +11767,14 @@ "name-show-table-footer": "", "name-show-table-header": "", "name-wrap-header-text": "", + "pill-cell-options-editor": { + "description-color-mode": "", + "description-fixed-color": "", + "description-value-mappings-info": "", + "label-color-mode": "", + "label-fixed-color": "", + "label-value-mappings-info": "" + }, "placeholder-column-width": "", "placeholder-fields": "" }, diff --git a/public/openapi3.json b/public/openapi3.json index f44e48250d6..f6a8589a689 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7360,6 +7360,8 @@ "properties": { "errorCode": { "enum": [ + "ALERT_RULES_QUOTA_REACHED", + "ALERT_RULES_GROUP_QUOTA_REACHED", "DATASOURCE_NAME_CONFLICT", "DATASOURCE_INVALID_URL", "DATASOURCE_ALREADY_MANAGED", diff --git a/public/test/core/redux/reducerTester.ts b/public/test/core/redux/reducerTester.ts index 38c466afa2b..79e2fddbc2d 100644 --- a/public/test/core/redux/reducerTester.ts +++ b/public/test/core/redux/reducerTester.ts @@ -2,7 +2,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import { Action } from 'redux'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; type GrafanaReducer = (state: S, action: A) => S; diff --git a/public/test/core/redux/reduxTester.ts b/public/test/core/redux/reduxTester.ts index 787ce13b59b..caf43717f21 100644 --- a/public/test/core/redux/reduxTester.ts +++ b/public/test/core/redux/reduxTester.ts @@ -3,7 +3,7 @@ import { Middleware, Store, StoreEnhancer, UnknownAction } from 'redux'; import { thunk, ThunkDispatch, ThunkMiddleware } from 'redux-thunk'; import { setStore } from '../../../app/store/store'; -import { StoreState } from '../../../app/types'; +import { StoreState } from '../../../app/types/store'; export interface ReduxTesterGiven { givenRootReducer: (rootReducer: Reducer>>) => ReduxTesterWhen; diff --git a/public/test/helpers/convertToStoreState.ts b/public/test/helpers/convertToStoreState.ts index 9f696b0e45a..92982147231 100644 --- a/public/test/helpers/convertToStoreState.ts +++ b/public/test/helpers/convertToStoreState.ts @@ -2,7 +2,7 @@ import { TypedVariableModel } from '@grafana/data'; import { getPreloadedState } from '../../app/features/variables/state/helpers'; import { VariablesState } from '../../app/features/variables/state/types'; -import { StoreState } from '../../app/types'; +import { StoreState } from '../../app/types/store'; export const convertToStoreState = (key: string, models: TypedVariableModel[]): StoreState => { const variables = models.reduce((byName, variable) => { diff --git a/public/test/helpers/getTemplateSrvDependencies.ts b/public/test/helpers/getTemplateSrvDependencies.ts index c1da0c4d247..d602d5aa03d 100644 --- a/public/test/helpers/getTemplateSrvDependencies.ts +++ b/public/test/helpers/getTemplateSrvDependencies.ts @@ -1,7 +1,7 @@ import { TemplateSrvDependencies } from 'app/features/templating/template_srv'; import { getFilteredVariables, getVariables, getVariableWithName } from '../../app/features/variables/state/selectors'; -import { StoreState } from '../../app/types'; +import { StoreState } from '../../app/types/store'; export const getTemplateSrvDependencies = (state: StoreState): TemplateSrvDependencies => ({ getFilteredVariables: (filter) => getFilteredVariables(filter, state), diff --git a/scripts/check-breaking-changes.sh b/scripts/check-breaking-changes.sh index 7ce7a28ea94..dc1e5741d6b 100755 --- a/scripts/check-breaking-changes.sh +++ b/scripts/check-breaking-changes.sh @@ -68,7 +68,8 @@ mkdir -p ./levitate echo "$GITHUB_LEVITATE_MARKDOWN" >./levitate/levitate.md if [[ "$IS_FORK" == "true" ]]; then - cat ./levitate/levitate.md + cat ./levitate/levitate.md >> "$GITHUB_STEP_SUMMARY" + exit $EXIT_CODE fi # We will exit the workflow accordingly at another step diff --git a/yarn.lock b/yarn.lock index fc1525bbc7e..660da0c9744 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1464,7 +1464,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": version: 7.27.6 resolution: "@babel/runtime@npm:7.27.6" checksum: 10/cc957a12ba3781241b83d528eb69ddeb86ca5ac43179a825e83aa81263a6b3eb88c57bed8a937cdeacfc3192e07ec24c73acdfea4507d0c0428c8e23d6322bfe @@ -2058,177 +2058,184 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/aix-ppc64@npm:0.25.0" +"@esbuild/aix-ppc64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/aix-ppc64@npm:0.25.6" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/android-arm64@npm:0.25.0" +"@esbuild/android-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/android-arm64@npm:0.25.6" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/android-arm@npm:0.25.0" +"@esbuild/android-arm@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/android-arm@npm:0.25.6" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/android-x64@npm:0.25.0" +"@esbuild/android-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/android-x64@npm:0.25.6" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/darwin-arm64@npm:0.25.0" +"@esbuild/darwin-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/darwin-arm64@npm:0.25.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/darwin-x64@npm:0.25.0" +"@esbuild/darwin-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/darwin-x64@npm:0.25.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/freebsd-arm64@npm:0.25.0" +"@esbuild/freebsd-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/freebsd-arm64@npm:0.25.6" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/freebsd-x64@npm:0.25.0" +"@esbuild/freebsd-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/freebsd-x64@npm:0.25.6" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-arm64@npm:0.25.0" +"@esbuild/linux-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-arm64@npm:0.25.6" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-arm@npm:0.25.0" +"@esbuild/linux-arm@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-arm@npm:0.25.6" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-ia32@npm:0.25.0" +"@esbuild/linux-ia32@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-ia32@npm:0.25.6" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-loong64@npm:0.25.0" +"@esbuild/linux-loong64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-loong64@npm:0.25.6" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-mips64el@npm:0.25.0" +"@esbuild/linux-mips64el@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-mips64el@npm:0.25.6" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-ppc64@npm:0.25.0" +"@esbuild/linux-ppc64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-ppc64@npm:0.25.6" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-riscv64@npm:0.25.0" +"@esbuild/linux-riscv64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-riscv64@npm:0.25.6" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-s390x@npm:0.25.0" +"@esbuild/linux-s390x@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-s390x@npm:0.25.6" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-x64@npm:0.25.0" +"@esbuild/linux-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-x64@npm:0.25.6" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/netbsd-arm64@npm:0.25.0" +"@esbuild/netbsd-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/netbsd-arm64@npm:0.25.6" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/netbsd-x64@npm:0.25.0" +"@esbuild/netbsd-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/netbsd-x64@npm:0.25.6" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/openbsd-arm64@npm:0.25.0" +"@esbuild/openbsd-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/openbsd-arm64@npm:0.25.6" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/openbsd-x64@npm:0.25.0" +"@esbuild/openbsd-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/openbsd-x64@npm:0.25.6" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/sunos-x64@npm:0.25.0" +"@esbuild/openharmony-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/openharmony-arm64@npm:0.25.6" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/sunos-x64@npm:0.25.6" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/win32-arm64@npm:0.25.0" +"@esbuild/win32-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/win32-arm64@npm:0.25.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/win32-ia32@npm:0.25.0" +"@esbuild/win32-ia32@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/win32-ia32@npm:0.25.6" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/win32-x64@npm:0.25.0" +"@esbuild/win32-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/win32-x64@npm:0.25.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -2720,7 +2727,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:12.1.0-pre" "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/lezer-logql": "npm:0.2.7" + "@grafana/lezer-logql": "npm:0.2.8" "@grafana/llm": "npm:0.22.1" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/plugin-configs": "npm:12.1.0-pre" @@ -2858,7 +2865,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:12.1.0-pre" "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/google-sdk": "npm:0.3.2" + "@grafana/google-sdk": "npm:0.3.4" "@grafana/plugin-configs": "npm:12.1.0-pre" "@grafana/plugin-ui": "npm:0.10.7" "@grafana/runtime": "npm:12.1.0-pre" @@ -2931,7 +2938,7 @@ __metadata: "@types/uuid": "npm:10.0.0" buffer: "npm:6.0.3" events: "npm:3.3.0" - glob: "npm:11.0.1" + glob: "npm:11.0.3" i18next: "npm:^25.0.0" lodash: "npm:4.17.21" lru-cache: "npm:11.1.0" @@ -3078,7 +3085,7 @@ __metadata: d3-interpolate: "npm:3.0.1" date-fns: "npm:4.1.0" dompurify: "npm:3.2.6" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" eventemitter3: "npm:5.0.1" fast_array_intersect: "npm:1.1.0" history: "npm:4.10.1" @@ -3117,7 +3124,7 @@ __metadata: "@rollup/plugin-node-resolve": "npm:16.0.1" "@types/node": "npm:22.15.0" "@types/semver": "npm:7.7.0" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-esbuild: "npm:6.2.0" @@ -3162,43 +3169,43 @@ __metadata: languageName: unknown linkType: soft -"@grafana/faro-core@npm:^1.13.2, @grafana/faro-core@npm:^1.18.2": - version: 1.18.2 - resolution: "@grafana/faro-core@npm:1.18.2" +"@grafana/faro-core@npm:^1.13.2, @grafana/faro-core@npm:^1.19.0": + version: 1.19.0 + resolution: "@grafana/faro-core@npm:1.19.0" dependencies: "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/otlp-transformer": "npm:^0.201.0" - checksum: 10/b211039b8b7381d2cb6bc8f80dc25d1c7ce484e426c6406bdcdd94c9e694b88387704b0408b1e11a0c5127dd9b1fc29544b24454b0e68f8458efb1a0166de657 + "@opentelemetry/otlp-transformer": "npm:^0.202.0" + checksum: 10/9bb3549074e86dea152469c558a3f126e231c068059a605f5afdf7c1bca1efd6f634a0c1bbbf068de1c1bd841d9c46f92b20490a1dd5a623451a38e73fab16e1 languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:^1.13.2, @grafana/faro-web-sdk@npm:^1.18.2": - version: 1.18.2 - resolution: "@grafana/faro-web-sdk@npm:1.18.2" +"@grafana/faro-web-sdk@npm:^1.13.2, @grafana/faro-web-sdk@npm:^1.19.0": + version: 1.19.0 + resolution: "@grafana/faro-web-sdk@npm:1.19.0" dependencies: - "@grafana/faro-core": "npm:^1.18.2" + "@grafana/faro-core": "npm:^1.19.0" ua-parser-js: "npm:^1.0.32" web-vitals: "npm:^4.0.1" - checksum: 10/f2b5d0f794c5d493c51947380081771436ed49dca02b7e1ea292f966da2e861514dc2d9ed74d1da821e85c398fb66b45c8eda2f6f628c7d6e4bfc8f498074c93 + checksum: 10/a7a3441167e55e4e59d70867ffcb860f177c386a33c22c45b6bbc1aeba9280891becb435699dd06d3d53cf8823365626787a085910b93d27073f3c0264fff7bc languageName: node linkType: hard "@grafana/faro-web-tracing@npm:^1.13.2": - version: 1.18.2 - resolution: "@grafana/faro-web-tracing@npm:1.18.2" + version: 1.19.0 + resolution: "@grafana/faro-web-tracing@npm:1.19.0" dependencies: - "@grafana/faro-web-sdk": "npm:^1.18.2" + "@grafana/faro-web-sdk": "npm:^1.19.0" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/exporter-trace-otlp-http": "npm:^0.201.0" - "@opentelemetry/instrumentation": "npm:^0.201.0" - "@opentelemetry/instrumentation-fetch": "npm:^0.201.0" - "@opentelemetry/instrumentation-xml-http-request": "npm:^0.201.0" - "@opentelemetry/otlp-transformer": "npm:^0.201.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:^0.202.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/instrumentation-fetch": "npm:^0.202.0" + "@opentelemetry/instrumentation-xml-http-request": "npm:^0.202.0" + "@opentelemetry/otlp-transformer": "npm:^0.202.0" "@opentelemetry/resources": "npm:^2.0.0" "@opentelemetry/sdk-trace-web": "npm:^2.0.0" "@opentelemetry/semantic-conventions": "npm:^1.32.0" - checksum: 10/5ca81618b52216da3acac456d149f62d1514c9e11b9d7a091ae9fb56e99e6adf2fb5a8630db13473524fd9c818a392366a7142ba84334e8981e956a110186bcd + checksum: 10/da7e0cd6b3fb5269d26530bdb212b005698ea489748c090fe6867bdb501e124c69ea8157a88f6e6115472500c4358f86001c104ccb24b6c9b8ad24c3b525917f languageName: node linkType: hard @@ -3228,7 +3235,7 @@ __metadata: "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" d3: "npm:^7.8.5" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" jest: "npm:^29.6.4" jest-canvas-mock: "npm:2.5.2" lodash: "npm:4.17.21" @@ -3249,15 +3256,15 @@ __metadata: languageName: unknown linkType: soft -"@grafana/google-sdk@npm:0.3.2": - version: 0.3.2 - resolution: "@grafana/google-sdk@npm:0.3.2" +"@grafana/google-sdk@npm:0.3.4": + version: 0.3.4 + resolution: "@grafana/google-sdk@npm:0.3.4" peerDependencies: "@grafana/data": ">=10.4.0" "@grafana/ui": ">=10.4.0" react: ^18.2.0 react-dom: ^18.2.0 - checksum: 10/da0b1c7eda95a03da5ea919aaf97e3ae942c219ee0854483f84824837192fad06c302c3f7e87a7819882ba272726039a0633891bd3ded10449227aafac16dfc7 + checksum: 10/6156a07ab9a8e31ac20a689448f7d6bc86bd9d3fd0b22de8182cf5e6f96c05054f8c53a38148de58dca807adb5b0563914b03a22308ec7f770bf1cc71e8f21bd languageName: node linkType: hard @@ -3283,12 +3290,12 @@ __metadata: languageName: unknown linkType: soft -"@grafana/lezer-logql@npm:0.2.7": - version: 0.2.7 - resolution: "@grafana/lezer-logql@npm:0.2.7" +"@grafana/lezer-logql@npm:0.2.8": + version: 0.2.8 + resolution: "@grafana/lezer-logql@npm:0.2.8" peerDependencies: "@lezer/lr": ^1.0.0 - checksum: 10/606a9dc77b3b3751e1f325d6b1a8994b1bafef7fe0f6f3980ee7d184244b373f828960f46a748746e765615352ed8928d10f22ff06cede588fcc9d32e70a68d8 + checksum: 10/56b31f9479037201b07d27602c023a2c2373758f232b02a4e5c501dbfabe6f708ddd2a4f8d059b5aad576297137bc2836ed3f520eceee6ffcd3001ad53c73f5c languageName: node linkType: hard @@ -3374,7 +3381,7 @@ __metadata: eslint: "npm:9.19.0" eslint-webpack-plugin: "npm:4.2.0" fork-ts-checker-webpack-plugin: "npm:9.0.2" - glob: "npm:11.0.1" + glob: "npm:11.0.3" imports-loader: "npm:^5.0.0" replace-in-file-webpack-plugin: "npm:1.0.6" swc-loader: "npm:0.2.6" @@ -3469,7 +3476,7 @@ __metadata: "@types/semver": "npm:7.7.0" "@types/uuid": "npm:10.0.0" debounce-promise: "npm:3.1.2" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" i18next-parser: "npm:9.3.0" jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" @@ -3521,7 +3528,7 @@ __metadata: "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/systemjs": "npm:6.15.3" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" history: "npm:4.10.1" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -3543,10 +3550,10 @@ __metadata: linkType: soft "@grafana/scenes-react@npm:^6.27.0": - version: 6.27.1 - resolution: "@grafana/scenes-react@npm:6.27.1" + version: 6.27.2 + resolution: "@grafana/scenes-react@npm:6.27.2" dependencies: - "@grafana/scenes": "npm:6.27.1" + "@grafana/scenes": "npm:6.27.2" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3558,13 +3565,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/1177db57199595f23722b9059ddf21f2679199f423ddde78c41d0a037d0e9c755a47c149541ce89df3694d7c55399e76eabed6a68a02b9b344acb8d3f1d95610 + checksum: 10/415186350304d999a13fa1e66f8bb7675cb28d5174a9449b8cb3d6a25efaecfc74a0e2e69cde9513f6fd9cac9bd2891c12e93a974ec15cfa0d6d04e5b95b5306 languageName: node linkType: hard -"@grafana/scenes@npm:6.27.1, @grafana/scenes@npm:^6.27.0": - version: 6.27.1 - resolution: "@grafana/scenes@npm:6.27.1" +"@grafana/scenes@npm:6.27.2, @grafana/scenes@npm:^6.27.0": + version: 6.27.2 + resolution: "@grafana/scenes@npm:6.27.2" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3584,7 +3591,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/7569db543bdf53483d5960e533cee61e86c25647b30d1290a28038725895dbb7e530bc9fd4520fc8c4e59b9a819f1daeeb87aecdf45c9d8870212c7ef1d65c8d + checksum: 10/8ba3d6c9b29de32a943a4a53511822f27eb51b35270368f0b9d80208eab50ae8767c6b372a7c7779a213618eab24bbb3e441dde42b038fc0a2078989879e0d14 languageName: node linkType: hard @@ -3594,7 +3601,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:16.0.1" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" glob: "npm:^11.0.0" rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" @@ -3714,7 +3721,7 @@ __metadata: "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" - "@types/chance": "npm:1.1.6" + "@types/chance": "npm:^1.1.7" "@types/common-tags": "npm:^1.8.0" "@types/d3": "npm:7.4.3" "@types/hoist-non-react-statics": "npm:3.3.6" @@ -3738,7 +3745,7 @@ __metadata: "@types/tinycolor2": "npm:1.4.6" "@types/uuid": "npm:10.0.0" calculate-size: "npm:1.1.1" - chance: "npm:1.1.12" + chance: "npm:^1.1.13" classnames: "npm:2.5.1" common-tags: "npm:1.8.2" core-js: "npm:3.40.0" @@ -3747,8 +3754,8 @@ __metadata: d3: "npm:7.9.0" date-fns: "npm:4.1.0" downshift: "npm:^9.0.6" - esbuild: "npm:0.25.0" - expose-loader: "npm:5.0.0" + esbuild: "npm:0.25.6" + expose-loader: "npm:5.0.1" fs-extra: "npm:^11.2.0" hoist-non-react-statics: "npm:3.3.2" i18next: "npm:^25.0.0" @@ -3987,6 +3994,22 @@ __metadata: languageName: node linkType: hard +"@isaacs/balanced-match@npm:^4.0.1": + version: 4.0.1 + resolution: "@isaacs/balanced-match@npm:4.0.1" + checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8 + languageName: node + linkType: hard + +"@isaacs/brace-expansion@npm:^5.0.0": + version: 5.0.0 + resolution: "@isaacs/brace-expansion@npm:5.0.0" + dependencies: + "@isaacs/balanced-match": "npm:^4.0.1" + checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -4736,9 +4759,9 @@ __metadata: languageName: node linkType: hard -"@lerna/create@npm:8.2.1": - version: 8.2.1 - resolution: "@lerna/create@npm:8.2.1" +"@lerna/create@npm:8.2.3": + version: 8.2.3 + resolution: "@lerna/create@npm:8.2.3" dependencies: "@npmcli/arborist": "npm:7.5.4" "@npmcli/package-json": "npm:5.2.0" @@ -4763,7 +4786,6 @@ __metadata: get-stream: "npm:6.0.0" git-url-parse: "npm:14.0.0" glob-parent: "npm:6.0.2" - globby: "npm:11.1.0" graceful-fs: "npm:4.2.11" has-unicode: "npm:2.0.1" ini: "npm:^1.3.8" @@ -4798,9 +4820,10 @@ __metadata: slash: "npm:^3.0.0" ssri: "npm:^10.0.6" string-width: "npm:^4.2.3" - strong-log-transformer: "npm:2.1.0" tar: "npm:6.2.1" temp-dir: "npm:1.0.0" + through: "npm:2.3.8" + tinyglobby: "npm:0.2.12" upath: "npm:2.0.1" uuid: "npm:^10.0.0" validate-npm-package-license: "npm:^3.0.4" @@ -4810,7 +4833,7 @@ __metadata: write-pkg: "npm:4.0.0" yargs: "npm:17.7.2" yargs-parser: "npm:21.1.1" - checksum: 10/802db88edad8967afcbf499f68491139965209137ec92f402ac838452079f143701d6f9abd9832b3506a7e1c56e01ea9c09ffd6f8782b1d9202413756fcfd708 + checksum: 10/1264bf324de2c83377dbc0b49c6731b9e27401c552c34601846b012c46aee496c9a3bcdeba87e59cfaf8b54d3c82f7f3ece62ff4c77ca7b3a85acfc021b7cbb2 languageName: node linkType: hard @@ -5634,12 +5657,12 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.201.1": - version: 0.201.1 - resolution: "@opentelemetry/api-logs@npm:0.201.1" +"@opentelemetry/api-logs@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/api-logs@npm:0.202.0" dependencies: "@opentelemetry/api": "npm:^1.3.0" - checksum: 10/baa14906caf848b7ff32fdd2b8cbad5c96b6e5b4bb4e52cb4118b323b77b2e99630b4d58d92f110343d475a21fd5bdcaaa37c29a4a386136ed4ee01528a2b2ed + checksum: 10/22171137ad1d876a79a6f046b4adcc44a13941a07f2172948e4c8dbb6cacfe276b1fa5087f15e4acdd567f748010d531321c93ebf93a6c09586351c9048bf6c7 languageName: node linkType: hard @@ -5697,90 +5720,88 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-http@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.201.1" +"@opentelemetry/exporter-trace-otlp-http@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.202.0" dependencies: "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-exporter-base": "npm:0.201.1" - "@opentelemetry/otlp-transformer": "npm:0.201.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" "@opentelemetry/resources": "npm:2.0.1" "@opentelemetry/sdk-trace-base": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/b1cec9287384900a3dc5326e3d2c089da9cb13b77f42660c201d0da2ae47f34dda81145ee14b1c8fcce12a59dd2511704d1179a0c991fdcf1c645bffdee6072a + checksum: 10/f9e60e51b5dcca3d4d32f2091f04faa2bcaf677b9f8a88990c8cdc7c6345e4244e46c4e8df1c336fc6f741aea092407032ade1236089eb729196e6fcdc055ad0 languageName: node linkType: hard -"@opentelemetry/instrumentation-fetch@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/instrumentation-fetch@npm:0.201.1" +"@opentelemetry/instrumentation-fetch@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation-fetch@npm:0.202.0" dependencies: "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/instrumentation": "npm:0.201.1" + "@opentelemetry/instrumentation": "npm:0.202.0" "@opentelemetry/sdk-trace-web": "npm:2.0.1" "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/9d096774fb93e1c690d40628669a383cb2c972a5aa699bd2fbeb4da7c30dd30e6959dd1b464e6c4d7b3f3d92c71364ca75b3ee5da8e291dc8939f9b9fcd1f1d7 + checksum: 10/c3e87a75878b7b16f5c9ac66c73b6affb8d9d6e5ba95aa19c05a0f73c03d831e7d60e586d9a5bdf76e962acaadbe989b351fefe1908ab439afcd0158bded10e6 languageName: node linkType: hard -"@opentelemetry/instrumentation-xml-http-request@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/instrumentation-xml-http-request@npm:0.201.1" +"@opentelemetry/instrumentation-xml-http-request@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation-xml-http-request@npm:0.202.0" dependencies: "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/instrumentation": "npm:0.201.1" + "@opentelemetry/instrumentation": "npm:0.202.0" "@opentelemetry/sdk-trace-web": "npm:2.0.1" "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/5fec2a79a4b041a22e15a1aa06436b3881d06f25e3f5ee75309bab335ddadde4523045a6352e115b0331c48488cb878d0ebfc33d76cf34df5f73bcf6c07bcbca + checksum: 10/6a2c76374f5af2d37318f5a689447a99ecc65c7c5546f3421171c870d5ddf1536e30f6fd63d8046cbc3e4cbf737529bae304959d971beefaf7f730fefb9e663c languageName: node linkType: hard -"@opentelemetry/instrumentation@npm:0.201.1, @opentelemetry/instrumentation@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/instrumentation@npm:0.201.1" +"@opentelemetry/instrumentation@npm:0.202.0, @opentelemetry/instrumentation@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation@npm:0.202.0" dependencies: - "@opentelemetry/api-logs": "npm:0.201.1" - "@types/shimmer": "npm:^1.2.0" + "@opentelemetry/api-logs": "npm:0.202.0" import-in-the-middle: "npm:^1.8.1" require-in-the-middle: "npm:^7.1.1" - shimmer: "npm:^1.2.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/cef0b05ce1b153f9d7ce770782c67f2422d98a6c1bd4bc5d69db6c2c3d91523c7030bb0ff6ff16ae246586334f4774c8d1b71bef5e395b7e7cba4645a73a14c4 + checksum: 10/da1db1ebc4ca847cc68d894b2e3a6c6552851d93af8ea793d42474e920f710664575c8991dc269e1a83fcf8f5abdda2cc724fa24e9cc4ae19fa6f70eb68ffc0e languageName: node linkType: hard -"@opentelemetry/otlp-exporter-base@npm:0.201.1": - version: 0.201.1 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.201.1" +"@opentelemetry/otlp-exporter-base@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.202.0" dependencies: "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-transformer": "npm:0.201.1" + "@opentelemetry/otlp-transformer": "npm:0.202.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/d9c64ebf531e5a7e3d42537d2058e331165e4764b4a54d453668c1f8bbfa14008255771110bb106aa44c094ad76933a46f37f67d4b362908d511e57e72b7cd09 + checksum: 10/229778895ba1971451a8b1ec8a4787b0cdba337c87f3ca6aaf6d649f5e2b26439ba1befa36d31824fffa3cef62382792680ec70194f57aac6ebe98e95acecc69 languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:0.201.1, @opentelemetry/otlp-transformer@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/otlp-transformer@npm:0.201.1" +"@opentelemetry/otlp-transformer@npm:0.202.0, @opentelemetry/otlp-transformer@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.202.0" dependencies: - "@opentelemetry/api-logs": "npm:0.201.1" + "@opentelemetry/api-logs": "npm:0.202.0" "@opentelemetry/core": "npm:2.0.1" "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/sdk-logs": "npm:0.201.1" + "@opentelemetry/sdk-logs": "npm:0.202.0" "@opentelemetry/sdk-metrics": "npm:2.0.1" "@opentelemetry/sdk-trace-base": "npm:2.0.1" protobufjs: "npm:^7.3.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/bed6f7d12aba212cfc9dd0c482de6d983f31a994faa4cb13f651f1cbe98ae8935ed25a4a25887cdcdc9a53af1ee8cd3406e869d900499c0cbadf87f3218dcdb4 + checksum: 10/67e189af60bf8308a5b93deb85bef9709a5604b8c7915d0d42a7a2bff932b7257dfdfc071812c77913e136955a985ffe838c72b5f10059021087ea0bc52d84cd languageName: node linkType: hard @@ -5808,16 +5829,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.201.1": - version: 0.201.1 - resolution: "@opentelemetry/sdk-logs@npm:0.201.1" +"@opentelemetry/sdk-logs@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/sdk-logs@npm:0.202.0" dependencies: - "@opentelemetry/api-logs": "npm:0.201.1" + "@opentelemetry/api-logs": "npm:0.202.0" "@opentelemetry/core": "npm:2.0.1" "@opentelemetry/resources": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10/c2d8aad418268c5ab4ad18f8eea5bb11fff1659b9bbbcd30546a622c2a6e04e3361de7809e702bff7c151cf7c21408ab8fd798b43ffbc8f549bfb91d0c40d4bb + checksum: 10/e1b76647282a41ad7004c86c0058b0e9be70fdcf34aa2761929401e274af79bc5e3c6610a63e14fde4d51d96d8c96adaf3247c10b9a64cb70086d2ede2e421ca languageName: node linkType: hard @@ -8902,10 +8923,10 @@ __metadata: languageName: node linkType: hard -"@types/chance@npm:1.1.6, @types/chance@npm:^1.1.3": - version: 1.1.6 - resolution: "@types/chance@npm:1.1.6" - checksum: 10/f4366f1b3144d143af3e6f0fad2ed1db7b9bdfa7d82d40944e9619d57fe7e6b60e8c1452f47a8ededa6b2188932879518628ecd9aac81c40384ded39c26338ba +"@types/chance@npm:^1.1.7": + version: 1.1.7 + resolution: "@types/chance@npm:1.1.7" + checksum: 10/5b3bf4ef0b7a2f6554f7767d7f081b4f613b45e74e74450d4da9c10cd12162420fc6ff5bb0abea143d6f7c43b62776b7a0e75d242cbe25c729fe020a1735e16f languageName: node linkType: hard @@ -10052,13 +10073,6 @@ __metadata: languageName: node linkType: hard -"@types/shimmer@npm:^1.2.0": - version: 1.2.0 - resolution: "@types/shimmer@npm:1.2.0" - checksum: 10/f081a31d826ce7bfe8cc7ba8129d2b1dffae44fd580eba4fcf741237646c4c2494ae6de2cada4b7713d138f35f4bc512dbf01311d813dee82020f97d7d8c491c - languageName: node - linkType: hard - "@types/sinonjs__fake-timers@npm:8.1.1": version: 8.1.1 resolution: "@types/sinonjs__fake-timers@npm:8.1.1" @@ -11726,21 +11740,21 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:10.4.20": - version: 10.4.20 - resolution: "autoprefixer@npm:10.4.20" +"autoprefixer@npm:10.4.21": + version: 10.4.21 + resolution: "autoprefixer@npm:10.4.21" dependencies: - browserslist: "npm:^4.23.3" - caniuse-lite: "npm:^1.0.30001646" + browserslist: "npm:^4.24.4" + caniuse-lite: "npm:^1.0.30001702" fraction.js: "npm:^4.3.7" normalize-range: "npm:^0.1.2" - picocolors: "npm:^1.0.1" + picocolors: "npm:^1.1.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.1.0 bin: autoprefixer: bin/autoprefixer - checksum: 10/d3c4b562fc4af2393623a0207cc336f5b9f94c4264ae1c316376904c279702ce2b12dc3f27205f491195d1e29bb52ffc269970ceb0f271f035fadee128a273f7 + checksum: 10/5d7aeee78ef362a6838e12312908516a8ac5364414175273e5cff83bbff67612755b93d567f3aa01ce318342df48aeab4b291847b5800c780e58c458f61a98a6 languageName: node linkType: hard @@ -12287,17 +12301,17 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3": - version: 4.25.0 - resolution: "browserslist@npm:4.25.0" +"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3, browserslist@npm:^4.24.4, browserslist@npm:^4.24.5": + version: 4.25.1 + resolution: "browserslist@npm:4.25.1" dependencies: - caniuse-lite: "npm:^1.0.30001718" - electron-to-chromium: "npm:^1.5.160" + caniuse-lite: "npm:^1.0.30001726" + electron-to-chromium: "npm:^1.5.173" node-releases: "npm:^2.0.19" update-browserslist-db: "npm:^1.1.3" bin: browserslist: cli.js - checksum: 10/4a5442b1a0d09c4c64454f184b8fed17d8c3e202034bf39de28f74497d7bd28dddee121b2bab4e34825fe0ed4c166d84e32a39f576c76fce73c1f8f05e4b6ee6 + checksum: 10/bfb5511b425886279bbe2ea44d10e340c8aea85866c9d45083c13491d049b6362e254018c0afbf56d41ceeb64f994957ea8ae98dbba74ef1e54ef901c8732987 languageName: node linkType: hard @@ -12467,13 +12481,13 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1": - version: 1.0.1 - resolution: "call-bind-apply-helpers@npm:1.0.1" +"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" dependencies: es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" - checksum: 10/6e30c621170e45f1fd6735e84d02ee8e02a3ab95cb109499d5308cbe5d1e84d0cd0e10b48cc43c76aa61450ae1b03a7f89c37c10fc0de8d4998b42aab0f268cc + checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 languageName: node linkType: hard @@ -12489,13 +12503,13 @@ __metadata: languageName: node linkType: hard -"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3": - version: 1.0.3 - resolution: "call-bound@npm:1.0.3" +"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3, call-bound@npm:^1.0.4": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - get-intrinsic: "npm:^1.2.6" - checksum: 10/c39a8245f68cdb7c1f5eea7b3b1e3a7a90084ea6efebb78ebc454d698ade2c2bb42ec033abc35f1e596d62496b6100e9f4cdfad1956476c510130e2cda03266d + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10/ef2b96e126ec0e58a7ff694db43f4d0d44f80e641370c21549ed911fecbdbc2df3ebc9bddad918d6bbdefeafb60bb3337902006d5176d72bcd2da74820991af7 languageName: node linkType: hard @@ -12567,10 +12581,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001718": - version: 1.0.30001723 - resolution: "caniuse-lite@npm:1.0.30001723" - checksum: 10/edab89e84a2b257cf640f0bac1f25f92c699ade86143b2affc73403468f894023416a9f4a99e5345c933956990b005a2facfb87ac4517c8ccb588819bb62453b +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001726": + version: 1.0.30001727 + resolution: "caniuse-lite@npm:1.0.30001727" + checksum: 10/6155a4141332c337d6317325bea58a09036a65f45bd9bd834ec38978b40c27d214baa04d25b21a5661664f3fbd00cb830e2bdb7eee8df09970bdd98a71f4dabf languageName: node linkType: hard @@ -12677,10 +12691,10 @@ __metadata: languageName: node linkType: hard -"chance@npm:1.1.12, chance@npm:^1.0.10": - version: 1.1.12 - resolution: "chance@npm:1.1.12" - checksum: 10/8700d5a66e27b47f4bdcf68f48489e1a490f39cd8bc8e39cac67c089b3f8d04b4bbc6710db0e85e802ad698cf3440d92784861208d2c248820355404b85b3f30 +"chance@npm:^1.1.13": + version: 1.1.13 + resolution: "chance@npm:1.1.13" + checksum: 10/968e31ce9b8ce554c8a84fb66d85f6d06a33517bf21355ab760e347a0aaaece0652a23c791406826d4efe619b1aa4e7b66fb42acbd07859ba36ec2498a167b8c languageName: node linkType: hard @@ -12849,15 +12863,15 @@ __metadata: languageName: node linkType: hard -"chrome-remote-interface@npm:0.33.2": - version: 0.33.2 - resolution: "chrome-remote-interface@npm:0.33.2" +"chrome-remote-interface@npm:0.33.3": + version: 0.33.3 + resolution: "chrome-remote-interface@npm:0.33.3" dependencies: commander: "npm:2.11.x" ws: "npm:^7.2.0" bin: chrome-remote-interface: bin/client.js - checksum: 10/fa82c76c5af629f5fbccb22c383604650f8679571385b8610783fde6f006b899c48e1522be551fd9670de59c1a2d93beb152a55651f24ba9c91ca0f8df3f581c + checksum: 10/65d07afc8f97fad6326bd94f0c4ef004d76b16841015c72d09aefbfa4df62b47407067a01c886535a706c36b23070b40fc3edb96444665438efa1d4f65cc3db8 languageName: node linkType: hard @@ -13933,14 +13947,14 @@ __metadata: languageName: node linkType: hard -"css-minimizer-webpack-plugin@npm:7.0.0": - version: 7.0.0 - resolution: "css-minimizer-webpack-plugin@npm:7.0.0" +"css-minimizer-webpack-plugin@npm:7.0.2": + version: 7.0.2 + resolution: "css-minimizer-webpack-plugin@npm:7.0.2" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.25" - cssnano: "npm:^7.0.1" + cssnano: "npm:^7.0.4" jest-worker: "npm:^29.7.0" - postcss: "npm:^8.4.38" + postcss: "npm:^8.4.40" schema-utils: "npm:^4.2.0" serialize-javascript: "npm:^6.0.2" peerDependencies: @@ -13958,7 +13972,7 @@ __metadata: optional: true lightningcss: optional: true - checksum: 10/47d8f8a38c97496759f1676b5344231d48bfb205cc272e163a113d4cd40daddd17f8eb719d1f1071cf3ec4d62fd83537801cba598d7c653657aa97f1461e4a8b + checksum: 10/80ada5f059900a3b474e33ff0f10c0e5f5a37b7525fa7ef30812b6049bcbc28fbda1d703d8f76724a97449114306de5cd7a7af15003a16784e6c9cc374dfedc8 languageName: node linkType: hard @@ -14098,64 +14112,64 @@ __metadata: languageName: node linkType: hard -"cssnano-preset-default@npm:^7.0.6": - version: 7.0.6 - resolution: "cssnano-preset-default@npm:7.0.6" +"cssnano-preset-default@npm:^7.0.7": + version: 7.0.7 + resolution: "cssnano-preset-default@npm:7.0.7" dependencies: - browserslist: "npm:^4.23.3" + browserslist: "npm:^4.24.5" css-declaration-sorter: "npm:^7.2.0" - cssnano-utils: "npm:^5.0.0" - postcss-calc: "npm:^10.0.2" - postcss-colormin: "npm:^7.0.2" - postcss-convert-values: "npm:^7.0.4" - postcss-discard-comments: "npm:^7.0.3" - postcss-discard-duplicates: "npm:^7.0.1" - postcss-discard-empty: "npm:^7.0.0" - postcss-discard-overridden: "npm:^7.0.0" - postcss-merge-longhand: "npm:^7.0.4" - postcss-merge-rules: "npm:^7.0.4" - postcss-minify-font-values: "npm:^7.0.0" - postcss-minify-gradients: "npm:^7.0.0" - postcss-minify-params: "npm:^7.0.2" - postcss-minify-selectors: "npm:^7.0.4" - postcss-normalize-charset: "npm:^7.0.0" - postcss-normalize-display-values: "npm:^7.0.0" - postcss-normalize-positions: "npm:^7.0.0" - postcss-normalize-repeat-style: "npm:^7.0.0" - postcss-normalize-string: "npm:^7.0.0" - postcss-normalize-timing-functions: "npm:^7.0.0" - postcss-normalize-unicode: "npm:^7.0.2" - postcss-normalize-url: "npm:^7.0.0" - postcss-normalize-whitespace: "npm:^7.0.0" - postcss-ordered-values: "npm:^7.0.1" - postcss-reduce-initial: "npm:^7.0.2" - postcss-reduce-transforms: "npm:^7.0.0" - postcss-svgo: "npm:^7.0.1" - postcss-unique-selectors: "npm:^7.0.3" + cssnano-utils: "npm:^5.0.1" + postcss-calc: "npm:^10.1.1" + postcss-colormin: "npm:^7.0.3" + postcss-convert-values: "npm:^7.0.5" + postcss-discard-comments: "npm:^7.0.4" + postcss-discard-duplicates: "npm:^7.0.2" + postcss-discard-empty: "npm:^7.0.1" + postcss-discard-overridden: "npm:^7.0.1" + postcss-merge-longhand: "npm:^7.0.5" + postcss-merge-rules: "npm:^7.0.5" + postcss-minify-font-values: "npm:^7.0.1" + postcss-minify-gradients: "npm:^7.0.1" + postcss-minify-params: "npm:^7.0.3" + postcss-minify-selectors: "npm:^7.0.5" + postcss-normalize-charset: "npm:^7.0.1" + postcss-normalize-display-values: "npm:^7.0.1" + postcss-normalize-positions: "npm:^7.0.1" + postcss-normalize-repeat-style: "npm:^7.0.1" + postcss-normalize-string: "npm:^7.0.1" + postcss-normalize-timing-functions: "npm:^7.0.1" + postcss-normalize-unicode: "npm:^7.0.3" + postcss-normalize-url: "npm:^7.0.1" + postcss-normalize-whitespace: "npm:^7.0.1" + postcss-ordered-values: "npm:^7.0.2" + postcss-reduce-initial: "npm:^7.0.3" + postcss-reduce-transforms: "npm:^7.0.1" + postcss-svgo: "npm:^7.0.2" + postcss-unique-selectors: "npm:^7.0.4" peerDependencies: - postcss: ^8.4.31 - checksum: 10/686e7652d01ad4337dbad17b22fdb9cf132cf4664fddd05194da13a1f44f1177697745bbc6da73083941356280e89fe2cceacb6f422cc4522d70ff51db83cd63 + postcss: ^8.4.32 + checksum: 10/1ca9b739531acc2dff66347cc1b6195da0549058b5b00b9d36c3f241535ad67476218f61201cfb15e8b460357ec42414aa53bf78a7f01ee26ac26b7852e6c244 languageName: node linkType: hard -"cssnano-utils@npm:^5.0.0": - version: 5.0.0 - resolution: "cssnano-utils@npm:5.0.0" +"cssnano-utils@npm:^5.0.1": + version: 5.0.1 + resolution: "cssnano-utils@npm:5.0.1" peerDependencies: - postcss: ^8.4.31 - checksum: 10/89ed5b8ca554697b4ae285e0d3e134fccc9a0471adda57c8fba17a2bace2f062b9fcf7aeaf66fbd7fabddca8a15a6b1e5ccb70a2783421ae1ac164f779d9f24e + postcss: ^8.4.32 + checksum: 10/cdf37315d3cf9726e10ce842b18e148e4df1d1d18d292540e724d5a96994901abc631c8894328c39ab70c864449a8a83f8fc117114fdcbade204e5e65898af90 languageName: node linkType: hard -"cssnano@npm:^7.0.1": - version: 7.0.6 - resolution: "cssnano@npm:7.0.6" +"cssnano@npm:^7.0.4": + version: 7.0.7 + resolution: "cssnano@npm:7.0.7" dependencies: - cssnano-preset-default: "npm:^7.0.6" - lilconfig: "npm:^3.1.2" + cssnano-preset-default: "npm:^7.0.7" + lilconfig: "npm:^3.1.3" peerDependencies: - postcss: ^8.4.31 - checksum: 10/12b1e1f2b52ff2ba0ecb470e51f8fb3298d976bf91a51c7d2854793ea1e2af5d3c40385a85ad82d2117c84b9528d08f4bfecbb14949c6014d953dae34260952b + postcss: ^8.4.32 + checksum: 10/c5b3123757834537f818e0f3eb6b20da51a194fefed599632f7ddd600c9e25d38abe38a22582a579660a49368a146c294e2096b2837cbeeda51ddfc85b108601 languageName: node linkType: hard @@ -15335,7 +15349,7 @@ __metadata: languageName: node linkType: hard -"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2": +"duplexer@npm:^0.1.2": version: 0.1.2 resolution: "duplexer@npm:0.1.2" checksum: 10/62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 @@ -15384,10 +15398,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.160": - version: 1.5.167 - resolution: "electron-to-chromium@npm:1.5.167" - checksum: 10/078093a38e7295e575f381943f62914f49b53dd73506af2ce3e59332835c42b487ad02ff1207dfdcb33a5886d74a98e352c04431c0537366d9999a79c7d15c94 +"electron-to-chromium@npm:^1.5.173": + version: 1.5.180 + resolution: "electron-to-chromium@npm:1.5.180" + checksum: 10/8d7f68650427f6bcb107ee1dcbe18f68b5c582601653095bec653fa898bd8427be4b1836581ce74405dca7cb36ebfc265a85c186e750b4de6e6f3ac4cfeef71b languageName: node linkType: hard @@ -15761,12 +15775,12 @@ __metadata: languageName: node linkType: hard -"es-object-atoms@npm:^1.0.0": - version: 1.0.0 - resolution: "es-object-atoms@npm:1.0.0" +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" dependencies: es-errors: "npm:^1.3.0" - checksum: 10/f8910cf477e53c0615f685c5c96210591841850871b81924fcf256bfbaa68c254457d994a4308c60d15b20805e7f61ce6abc669375e01a5349391a8c1767584f + checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 languageName: node linkType: hard @@ -15866,35 +15880,36 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.25.0, esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0, esbuild@npm:^0.25.0": - version: 0.25.0 - resolution: "esbuild@npm:0.25.0" +"esbuild@npm:0.25.6, esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0, esbuild@npm:^0.25.0": + version: 0.25.6 + resolution: "esbuild@npm:0.25.6" dependencies: - "@esbuild/aix-ppc64": "npm:0.25.0" - "@esbuild/android-arm": "npm:0.25.0" - "@esbuild/android-arm64": "npm:0.25.0" - "@esbuild/android-x64": "npm:0.25.0" - "@esbuild/darwin-arm64": "npm:0.25.0" - "@esbuild/darwin-x64": "npm:0.25.0" - "@esbuild/freebsd-arm64": "npm:0.25.0" - "@esbuild/freebsd-x64": "npm:0.25.0" - "@esbuild/linux-arm": "npm:0.25.0" - "@esbuild/linux-arm64": "npm:0.25.0" - "@esbuild/linux-ia32": "npm:0.25.0" - "@esbuild/linux-loong64": "npm:0.25.0" - "@esbuild/linux-mips64el": "npm:0.25.0" - "@esbuild/linux-ppc64": "npm:0.25.0" - "@esbuild/linux-riscv64": "npm:0.25.0" - "@esbuild/linux-s390x": "npm:0.25.0" - "@esbuild/linux-x64": "npm:0.25.0" - "@esbuild/netbsd-arm64": "npm:0.25.0" - "@esbuild/netbsd-x64": "npm:0.25.0" - "@esbuild/openbsd-arm64": "npm:0.25.0" - "@esbuild/openbsd-x64": "npm:0.25.0" - "@esbuild/sunos-x64": "npm:0.25.0" - "@esbuild/win32-arm64": "npm:0.25.0" - "@esbuild/win32-ia32": "npm:0.25.0" - "@esbuild/win32-x64": "npm:0.25.0" + "@esbuild/aix-ppc64": "npm:0.25.6" + "@esbuild/android-arm": "npm:0.25.6" + "@esbuild/android-arm64": "npm:0.25.6" + "@esbuild/android-x64": "npm:0.25.6" + "@esbuild/darwin-arm64": "npm:0.25.6" + "@esbuild/darwin-x64": "npm:0.25.6" + "@esbuild/freebsd-arm64": "npm:0.25.6" + "@esbuild/freebsd-x64": "npm:0.25.6" + "@esbuild/linux-arm": "npm:0.25.6" + "@esbuild/linux-arm64": "npm:0.25.6" + "@esbuild/linux-ia32": "npm:0.25.6" + "@esbuild/linux-loong64": "npm:0.25.6" + "@esbuild/linux-mips64el": "npm:0.25.6" + "@esbuild/linux-ppc64": "npm:0.25.6" + "@esbuild/linux-riscv64": "npm:0.25.6" + "@esbuild/linux-s390x": "npm:0.25.6" + "@esbuild/linux-x64": "npm:0.25.6" + "@esbuild/netbsd-arm64": "npm:0.25.6" + "@esbuild/netbsd-x64": "npm:0.25.6" + "@esbuild/openbsd-arm64": "npm:0.25.6" + "@esbuild/openbsd-x64": "npm:0.25.6" + "@esbuild/openharmony-arm64": "npm:0.25.6" + "@esbuild/sunos-x64": "npm:0.25.6" + "@esbuild/win32-arm64": "npm:0.25.6" + "@esbuild/win32-ia32": "npm:0.25.6" + "@esbuild/win32-x64": "npm:0.25.6" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -15938,6 +15953,8 @@ __metadata: optional: true "@esbuild/openbsd-x64": optional: true + "@esbuild/openharmony-arm64": + optional: true "@esbuild/sunos-x64": optional: true "@esbuild/win32-arm64": @@ -15948,7 +15965,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/451daf6a442df29ec5d528587caa4ce783d41ff4acb93252da5a852b8d36c22e9f84d17f6721d4fbef9a1ba9855bc9fe1f167dd732c11665fe53032f2b89f114 + checksum: 10/b1c94893d53e39f3be02493c3a082bc9d090ca1702bf6017967ace1391d31e596da4dfd87e02efef3b7b4f0426918dbc5aa6909420a6ba42d51a872266ab6f2e languageName: node linkType: hard @@ -16167,11 +16184,11 @@ __metadata: linkType: hard "eslint-plugin-no-barrel-files@npm:^1.1.1": - version: 1.2.0 - resolution: "eslint-plugin-no-barrel-files@npm:1.2.0" + version: 1.2.1 + resolution: "eslint-plugin-no-barrel-files@npm:1.2.1" dependencies: "@typescript-eslint/utils": "npm:^8.13.0" - checksum: 10/da54a4930ddfd7d14bd82766e857af9eea5315125a2599d5ba03326530214e0d416f78915335d6c239b19df21b989196ffc4146919eff05477cef7e34261b946 + checksum: 10/22a888dd3b49527bed0f92758f4c7f34e78397c9197688db1936d2bdad8369f9ded2a1231a6b85e54791f059784f1d7bcdbeeb21cf0b453399044a1957a153c4 languageName: node linkType: hard @@ -16184,9 +16201,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react@npm:7.37.4": - version: 7.37.4 - resolution: "eslint-plugin-react@npm:7.37.4" +"eslint-plugin-react@npm:7.37.5": + version: 7.37.5 + resolution: "eslint-plugin-react@npm:7.37.5" dependencies: array-includes: "npm:^3.1.8" array.prototype.findlast: "npm:^1.2.5" @@ -16198,7 +16215,7 @@ __metadata: hasown: "npm:^2.0.2" jsx-ast-utils: "npm:^2.4.1 || ^3.0.0" minimatch: "npm:^3.1.2" - object.entries: "npm:^1.1.8" + object.entries: "npm:^1.1.9" object.fromentries: "npm:^2.0.8" object.values: "npm:^1.2.1" prop-types: "npm:^15.8.1" @@ -16208,7 +16225,7 @@ __metadata: string.prototype.repeat: "npm:^1.0.0" peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - checksum: 10/c538c10665c87cb90a0bcc4efe53a758570db10997d079d31474a9760116ef5584648fa22403d889ca672df8071bda10b40434ea0499e5ee8360bc5c8aba1679 + checksum: 10/ee1bd4e0ec64f29109d5a625bb703d179c82e0159c86c3f1b52fc1209d2994625a137dae303c333fb308a2e38315e44066d5204998177e31974382f9fda25d5c languageName: node linkType: hard @@ -16595,12 +16612,12 @@ __metadata: languageName: node linkType: hard -"expose-loader@npm:5.0.0": - version: 5.0.0 - resolution: "expose-loader@npm:5.0.0" +"expose-loader@npm:5.0.1": + version: 5.0.1 + resolution: "expose-loader@npm:5.0.1" peerDependencies: webpack: ^5.0.0 - checksum: 10/6a77c79744e41f81fd4da1eb07c319f13a097be85c4078380361fd6c69ac69576539bdd81a07d50b36b44ff1c2c5c97c1f4d2e75f0a14db391153bfb7035709b + checksum: 10/8e7c5eab2ed78f182845425e99b1ab270d71b9dfbf23b032839757d7aa2e8d8ddfd8aaeda996d26f4f6374c34e3293914db1d4b396f53be95241b85226cd40ff languageName: node linkType: hard @@ -16901,6 +16918,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.3": + version: 6.4.6 + resolution: "fdir@npm:6.4.6" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/c186ba387e7b75ccf874a098d9bc5fe0af0e9c52fc56f8eac8e80aa4edb65532684bf2bf769894ff90f53bf221d6136692052d31f07a9952807acae6cbe7ee50 + languageName: node + linkType: hard + "fflate@npm:^0.8.2": version: 0.8.2 resolution: "fflate@npm:0.8.2" @@ -17229,13 +17258,13 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0": - version: 3.1.1 - resolution: "foreground-child@npm:3.1.1" +"foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1": + version: 3.3.1 + resolution: "foreground-child@npm:3.3.1" dependencies: - cross-spawn: "npm:^7.0.0" + cross-spawn: "npm:^7.0.6" signal-exit: "npm:^4.0.1" - checksum: 10/087edd44857d258c4f73ad84cb8df980826569656f2550c341b27adf5335354393eec24ea2fabd43a253233fb27cee177ebe46bd0b7ea129c77e87cb1e9936fb + checksum: 10/427b33f997a98073c0424e5c07169264a62cda806d8d2ded159b5b903fdfc8f0a1457e06b5fc35506497acb3f1e353f025edee796300209ac6231e80edece835 languageName: node linkType: hard @@ -17598,21 +17627,21 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7": - version: 1.2.7 - resolution: "get-intrinsic@npm:1.2.7" +"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" dependencies: - call-bind-apply-helpers: "npm:^1.0.1" + call-bind-apply-helpers: "npm:^1.0.2" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" + es-object-atoms: "npm:^1.1.1" function-bind: "npm:^1.1.2" - get-proto: "npm:^1.0.0" + get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-symbols: "npm:^1.1.0" hasown: "npm:^2.0.2" math-intrinsics: "npm:^1.1.0" - checksum: 10/4f7149c9a826723f94c6d49f70bcb3df1d3f9213994fab3668f12f09fa72074681460fb29ebb6f135556ec6372992d63802386098791a8f09cfa6f27090fa67b + checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 languageName: node linkType: hard @@ -17872,19 +17901,19 @@ __metadata: languageName: node linkType: hard -"glob@npm:11.0.1, glob@npm:^11.0.0": - version: 11.0.1 - resolution: "glob@npm:11.0.1" +"glob@npm:11.0.3, glob@npm:^11.0.0": + version: 11.0.3 + resolution: "glob@npm:11.0.3" dependencies: - foreground-child: "npm:^3.1.0" - jackspeak: "npm:^4.0.1" - minimatch: "npm:^10.0.0" + foreground-child: "npm:^3.3.1" + jackspeak: "npm:^4.1.1" + minimatch: "npm:^10.0.3" minipass: "npm:^7.1.2" package-json-from-dist: "npm:^1.0.0" path-scurry: "npm:^2.0.0" bin: glob: dist/esm/bin.mjs - checksum: 10/57b12a05cc25f1c38f3b24cf6ea7a8bacef11e782c4b9a8c5b0bef3e6c5bcb8c4548cb31eb4115592e0490a024c1bde7359c470565608dd061d3b21179740457 + checksum: 10/2ae536c1360c0266b523b2bfa6aadc10144a8b7e08869b088e37ac3c27cd30774f82e4bfb291cde796776e878f9e13200c7ff44010eb7054e00f46f649397893 languageName: node linkType: hard @@ -18014,7 +18043,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:11.1.0, globby@npm:^11.0.0, globby@npm:^11.1.0": +"globby@npm:^11.0.0, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -18120,9 +18149,9 @@ __metadata: "@grafana/faro-web-sdk": "npm:^1.13.2" "@grafana/faro-web-tracing": "npm:^1.13.2" "@grafana/flamegraph": "workspace:*" - "@grafana/google-sdk": "npm:0.3.2" + "@grafana/google-sdk": "npm:0.3.4" "@grafana/i18n": "workspace:*" - "@grafana/lezer-logql": "npm:0.2.7" + "@grafana/lezer-logql": "npm:0.2.8" "@grafana/llm": "npm:0.22.1" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/o11y-ds-frontend": "workspace:*" @@ -18178,7 +18207,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/babel__core": "npm:^7" "@types/babel__preset-env": "npm:^7" - "@types/chance": "npm:^1.1.3" + "@types/chance": "npm:^1.1.7" "@types/common-tags": "npm:^1.8.0" "@types/confusing-browser-globals": "npm:^1" "@types/d3": "npm:7.4.3" @@ -18238,15 +18267,15 @@ __metadata: "@visx/tooltip": "npm:3.12.0" "@welldone-software/why-did-you-render": "npm:8.0.3" ansicolor: "npm:2.0.3" - autoprefixer: "npm:10.4.20" + autoprefixer: "npm:10.4.21" babel-loader: "npm:9.2.1" baron: "npm:3.0.3" blob-polyfill: "npm:9.0.20240710" brace: "npm:0.11.1" browserslist: "npm:^4.21.4" centrifuge: "npm:5.3.5" - chance: "npm:^1.0.10" - chrome-remote-interface: "npm:0.33.2" + chance: "npm:^1.1.13" + chrome-remote-interface: "npm:0.33.3" classnames: "npm:2.5.1" codeowners: "npm:^5.1.1" combokeys: "npm:^3.0.0" @@ -18258,7 +18287,7 @@ __metadata: crashme: "npm:0.0.15" croner: "npm:^9.0.0" css-loader: "npm:7.1.2" - css-minimizer-webpack-plugin: "npm:7.0.0" + css-minimizer-webpack-plugin: "npm:7.0.2" cypress: "npm:14.3.2" cypress-file-upload: "npm:5.0.8" cypress-recurse: "npm:^1.35.3" @@ -18269,7 +18298,7 @@ __metadata: date-fns: "npm:4.1.0" debounce-promise: "npm:3.1.2" diff: "npm:^8.0.0" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" esbuild-loader: "npm:4.3.0" esbuild-plugin-browserslist: "npm:^1.0.0" eslint: "npm:9.19.0" @@ -18281,19 +18310,19 @@ __metadata: eslint-plugin-jsx-a11y: "npm:6.10.2" eslint-plugin-lodash: "npm:8.0.0" eslint-plugin-no-barrel-files: "npm:^1.1.1" - eslint-plugin-react: "npm:7.37.4" + eslint-plugin-react: "npm:7.37.5" eslint-plugin-react-hooks: "npm:5.1.0" eslint-plugin-testing-library: "npm:^7.0.0" eslint-plugin-unicorn: "npm:^56.0.0" eslint-scope: "npm:^8.1.0" eslint-webpack-plugin: "npm:4.2.0" - expose-loader: "npm:5.0.0" + expose-loader: "npm:5.0.1" fast-deep-equal: "npm:^3.1.3" fast-json-patch: "npm:3.1.1" file-saver: "npm:2.0.5" fishery: "npm:^2.2.2" fork-ts-checker-webpack-plugin: "npm:9.0.2" - glob: "npm:11.0.1" + glob: "npm:11.0.3" history: "npm:4.10.1" html-loader: "npm:5.1.0" html-webpack-plugin: "npm:5.6.3" @@ -18323,10 +18352,11 @@ __metadata: json-source-map: "npm:0.6.1" jsurl: "npm:^0.1.5" kbar: "npm:0.1.0-beta.45" - lerna: "npm:8.2.1" + lerna: "npm:8.2.3" leven: "npm:^4.0.0" lodash: "npm:4.17.21" logfmt: "npm:^1.3.2" + lossless-json: "npm:^4.1.1" lru-cache: "npm:11.1.0" lru-memoize: "npm:^1.1.0" lucene: "npm:^2.1.1" @@ -18353,7 +18383,7 @@ __metadata: pdf-parse: "npm:^1.1.1" plop: "npm:^4.0.1" pluralize: "npm:^8.0.0" - postcss: "npm:8.5.1" + postcss: "npm:8.5.6" postcss-loader: "npm:8.1.1" postcss-reporter: "npm:7.1.0" postcss-scss: "npm:4.0.9" @@ -19219,22 +19249,22 @@ __metadata: linkType: hard "i18next@npm:^23.5.1 || ^24.2.0": - version: 24.2.2 - resolution: "i18next@npm:24.2.2" + version: 24.2.3 + resolution: "i18next@npm:24.2.3" dependencies: - "@babel/runtime": "npm:^7.23.2" + "@babel/runtime": "npm:^7.26.10" peerDependencies: typescript: ^5 peerDependenciesMeta: typescript: optional: true - checksum: 10/f66ed9e56d9412e59502f5df39163631daf9f1264774732fb21edbd66a528ca7a6b67dc2e2aec95683c6c7956e42c651587a54bd8ee082bd12008880ce6cd326 + checksum: 10/6c73d964f2a98b1aa2c2717fe6da66fc265bcbbc5fcd52b2bfff51ff013d30d4f7d7449c4eb7f464d27af43e2e73f2e7f1d46a144d731bd3bdb1385d4c199e4c languageName: node linkType: hard "i18next@npm:^25.0.0": - version: 25.3.1 - resolution: "i18next@npm:25.3.1" + version: 25.3.2 + resolution: "i18next@npm:25.3.2" dependencies: "@babel/runtime": "npm:^7.27.6" peerDependencies: @@ -19242,7 +19272,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/f5f38af0f7b7c0055e607d4017dade5aedd1c75c44e091473b4d01537f0849e21a7ae7f66397719a2557fc53767cf9ea96a4da34127b906d3fe5ce21db895224 + checksum: 10/fb6b2035cc8f3bcc89f56e164d22cefbefd54e5a569315b20ddcfa6e1b68c48962307181b271fea7e5ee37ddfaa90721a4a7f7814b739f24bf00a3a670b8eb93 languageName: node linkType: hard @@ -20469,16 +20499,12 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^4.0.1": - version: 4.0.1 - resolution: "jackspeak@npm:4.0.1" +"jackspeak@npm:^4.1.1": + version: 4.1.1 + resolution: "jackspeak@npm:4.1.1" dependencies: "@isaacs/cliui": "npm:^8.0.2" - "@pkgjs/parseargs": "npm:^0.11.0" - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 10/b20dc0df0dbb2903e4d540ae68308ec7d1dd60944b130e867e218c98b5d77481d65ea734b6c81c812d481500076e8b3fdfccfb38fc81cb1acf165e853da3e26c + checksum: 10/ffceb270ec286841f48413bfb4a50b188662dfd599378ce142b6540f3f0a66821dc9dcb1e9ebc55c6c3b24dc2226c96e5819ba9bd7a241bd29031b61911718c7 languageName: node linkType: hard @@ -21593,11 +21619,11 @@ __metadata: languageName: node linkType: hard -"lerna@npm:8.2.1": - version: 8.2.1 - resolution: "lerna@npm:8.2.1" +"lerna@npm:8.2.3": + version: 8.2.3 + resolution: "lerna@npm:8.2.3" dependencies: - "@lerna/create": "npm:8.2.1" + "@lerna/create": "npm:8.2.3" "@npmcli/arborist": "npm:7.5.4" "@npmcli/package-json": "npm:5.2.0" "@npmcli/run-script": "npm:8.1.0" @@ -21624,7 +21650,6 @@ __metadata: get-stream: "npm:6.0.0" git-url-parse: "npm:14.0.0" glob-parent: "npm:6.0.2" - globby: "npm:11.1.0" graceful-fs: "npm:4.2.11" has-unicode: "npm:2.0.1" import-local: "npm:3.1.0" @@ -21664,9 +21689,10 @@ __metadata: slash: "npm:3.0.0" ssri: "npm:^10.0.6" string-width: "npm:^4.2.3" - strong-log-transformer: "npm:2.1.0" tar: "npm:6.2.1" temp-dir: "npm:1.0.0" + through: "npm:2.3.8" + tinyglobby: "npm:0.2.12" typescript: "npm:>=3 < 6" upath: "npm:2.0.1" uuid: "npm:^10.0.0" @@ -21679,7 +21705,7 @@ __metadata: yargs-parser: "npm:21.1.1" bin: lerna: dist/cli.js - checksum: 10/ebf9fd1af102a8b7e89dcf05e32f92dfa2ce13e77c9788a86eb4828e6a5269e7bf85edf1bcdb4e4ea383f42d872880ad61fc26d304276715b3757fb54cd60d94 + checksum: 10/3ef9e5c6e2ee20cad0c750817cf628dffa0056f9b87ee4956f641833ac3b06a8fdf50d4cd6ba63a818427c7e6c1482568c9e184f0535fd23239ed55e5eae57a7 languageName: node linkType: hard @@ -21801,7 +21827,7 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^3.1.2, lilconfig@npm:^3.1.3": +"lilconfig@npm:^3.1.3": version: 3.1.3 resolution: "lilconfig@npm:3.1.3" checksum: 10/b932ce1af94985f0efbe8896e57b1f814a48c8dbd7fc0ef8469785c6303ed29d0090af3ccad7e36b626bfca3a4dc56cc262697e9a8dd867623cf09a39d54e4c3 @@ -22149,6 +22175,13 @@ __metadata: languageName: node linkType: hard +"lossless-json@npm:^4.1.1": + version: 4.1.1 + resolution: "lossless-json@npm:4.1.1" + checksum: 10/1e22e9e4ad8ebf169567a8c24553403bfd7abc33c141f4918f1b9626417f80477b849e6175a315a224e0c68072759c69a0e332a925de80269a7f9720af3a7fbd + languageName: node + linkType: hard + "loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2": version: 3.1.3 resolution: "loupe@npm:3.1.3" @@ -22629,9 +22662,9 @@ __metadata: linkType: hard "micro-memoize@npm:^4.1.2": - version: 4.1.2 - resolution: "micro-memoize@npm:4.1.2" - checksum: 10/027e90c3147c97c07224440ea50ede27eb7d888149e4925820397b466d16efc525f5ec3981e4cadec3258a8d36dfd5e7e7c8e660879fbe2e47106785be9bc570 + version: 4.1.3 + resolution: "micro-memoize@npm:4.1.3" + checksum: 10/4e9c7767911cc76ae9c9779584ec87844437af9446b295a01774640a732c2c7f91944794027f44625031f7330ab7f9147740d0a9fb612680d1d2d858dad43402 languageName: node linkType: hard @@ -22771,12 +22804,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.0.0": - version: 10.0.1 - resolution: "minimatch@npm:10.0.1" +"minimatch@npm:^10.0.3": + version: 10.0.3 + resolution: "minimatch@npm:10.0.3" dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/082e7ccbc090d5f8c4e4e029255d5a1d1e3af37bda837da2b8b0085b1503a1210c91ac90d9ebfe741d8a5f286ece820a1abb4f61dc1f82ce602a055d461d93f3 + "@isaacs/brace-expansion": "npm:^5.0.0" + checksum: 10/d5b8b2538b367f2cfd4aeef27539fddeee58d1efb692102b848e4a968a09780a302c530eb5aacfa8c57f7299155fb4b4e85219ad82664dcef5c66f657111d9b8 languageName: node linkType: hard @@ -23309,12 +23342,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.8": - version: 3.3.8 - resolution: "nanoid@npm:3.3.8" +"nanoid@npm:^3.3.11": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" bin: nanoid: bin/nanoid.cjs - checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40 + checksum: 10/73b5afe5975a307aaa3c95dfe3334c52cdf9ae71518176895229b8d65ab0d1c0417dd081426134eb7571c055720428ea5d57c645138161e7d10df80815527c48 languageName: node linkType: hard @@ -24046,14 +24079,15 @@ __metadata: languageName: node linkType: hard -"object.entries@npm:^1.1.8": - version: 1.1.8 - resolution: "object.entries@npm:1.1.8" +"object.entries@npm:^1.1.9": + version: 1.1.9 + resolution: "object.entries@npm:1.1.9" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/2301918fbd1ee697cf6ff7cd94f060c738c0a7d92b22fd24c7c250e9b593642c9707ad2c44d339303c1439c5967d8964251cdfc855f7f6ec55db2dd79e8dc2a7 + es-object-atoms: "npm:^1.1.1" + checksum: 10/24163ab1e1e013796693fc5f5d349e8b3ac0b6a34a7edb6c17d3dd45c6a8854145780c57d302a82512c1582f63720f4b4779d6c1cfba12cbb1420b978802d8a3 languageName: node linkType: hard @@ -25119,7 +25153,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.1": +"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 @@ -25317,79 +25351,79 @@ __metadata: languageName: node linkType: hard -"postcss-calc@npm:^10.0.2": - version: 10.0.2 - resolution: "postcss-calc@npm:10.0.2" +"postcss-calc@npm:^10.1.1": + version: 10.1.1 + resolution: "postcss-calc@npm:10.1.1" dependencies: - postcss-selector-parser: "npm:^6.1.2" + postcss-selector-parser: "npm:^7.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4.38 - checksum: 10/12d497e632b4a12f7d33507ed6f74db2dd01f9b9cc1f9986271af16b118d25f959dc255777a91d742e0431f400a90b8540d00533fc0513f34c1840a491cf2bee + checksum: 10/16a25ec594cfbbda439fd2939820f78ed4e7b8b5ab458aed7283b05fffabe68e1d4e1f4821fac798095f10539371676cd690bd27927adefab1911ff69b33d62c languageName: node linkType: hard -"postcss-colormin@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-colormin@npm:7.0.2" +"postcss-colormin@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-colormin@npm:7.0.3" dependencies: - browserslist: "npm:^4.23.3" + browserslist: "npm:^4.24.5" caniuse-api: "npm:^3.0.0" colord: "npm:^2.9.3" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/cb83d95d21668c770e5268f50ec6f8cd5d991d65123bafd3aa4a697580609c62d0078e704c4b7820db57638bf386084b253885b1e86263f580e8a393a687e973 + postcss: ^8.4.32 + checksum: 10/b9016d205eaf61a25efb187264a2ce35cb59aa1734b946268abcd747b5796e0d855c081b460ead4042a17c6806e011b57ee543b9e1f6312620f8daf661a7e40c languageName: node linkType: hard -"postcss-convert-values@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-convert-values@npm:7.0.4" +"postcss-convert-values@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-convert-values@npm:7.0.5" dependencies: - browserslist: "npm:^4.23.3" + browserslist: "npm:^4.24.5" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/077481cc98514965acf335cdacae4f604be86f4153ed3bcfdd2c4c54058182d0b472f859931d55d9aeb01600f08fff6a88a21539adbb6169019fda8b22f064ef + postcss: ^8.4.32 + checksum: 10/67920f9ba823a6f6aa3b46c3a098c2d4a7a2a32349971cfa6ce986e08e7cbae6badeb23de680d36d1439e7d3f2cdbf26f5ee080a66f2823931c1d3f8146bc2a6 languageName: node linkType: hard -"postcss-discard-comments@npm:^7.0.3": - version: 7.0.3 - resolution: "postcss-discard-comments@npm:7.0.3" +"postcss-discard-comments@npm:^7.0.4": + version: 7.0.4 + resolution: "postcss-discard-comments@npm:7.0.4" dependencies: - postcss-selector-parser: "npm:^6.1.2" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/f7c994df0d2de75d876f0db7ebd5b63718ef7ee5336a35f5f753f8ea115ecf8be26d5d2ad8800e833f18b33da6e018af82de7b5f0aa69e6338d3e0aff46348d4 + postcss: ^8.4.32 + checksum: 10/a09ac248bfbd6f2baa72b84873a876f4113df0fb5e9dd10808f6bbb310473fcd7905cc4639dbfd3ad8a5444053d42f7bb644a6934e95305820bdedc731d3c80a languageName: node linkType: hard -"postcss-discard-duplicates@npm:^7.0.1": +"postcss-discard-duplicates@npm:^7.0.2": + version: 7.0.2 + resolution: "postcss-discard-duplicates@npm:7.0.2" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/2da841b5c0117528e56e1ccda28924339c03fdb93dab61b767cebb9a9e4a2a077498d00e0c97c9ec36a534f98d6f358e6236f30913c184f90d51f6d302f4f0f6 + languageName: node + linkType: hard + +"postcss-discard-empty@npm:^7.0.1": version: 7.0.1 - resolution: "postcss-discard-duplicates@npm:7.0.1" + resolution: "postcss-discard-empty@npm:7.0.1" peerDependencies: - postcss: ^8.4.31 - checksum: 10/0c757bb542caf017740157a2e29186ae83085bb42cd8e5ea3649fa039cc3d505ccaca739b1aed6c89e1f0a7f18440f77c3f49e4b99f45efd767c863d6647af94 + postcss: ^8.4.32 + checksum: 10/39977000657e78202da891ae6300593e40e1c8a756f1d9707087390e47a410739c394c35e902130556efb5808e6701b3b34b89facf7a9e56533d617dd9597049 languageName: node linkType: hard -"postcss-discard-empty@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-discard-empty@npm:7.0.0" +"postcss-discard-overridden@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-discard-overridden@npm:7.0.1" peerDependencies: - postcss: ^8.4.31 - checksum: 10/0c5cea198057727765855dbb43b5f16bd4d7da8c783fea8d18ad445ad3457681a7bc1696fda6bf16313e6fadaf86d519470aff68f02378b8b413e60023b70d57 - languageName: node - linkType: hard - -"postcss-discard-overridden@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-discard-overridden@npm:7.0.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/e41c448305f96a93ec97a4a8ce2932a123283898041ff38ed2f7a35fcb76d937f448c2c8efb7d74d53d38b4ebf9163ae12935297bb99baec2f6751776b0ea29b + postcss: ^8.4.32 + checksum: 10/a0e67314b696591396e6bb371cdd57537e06f63e9fa0d742fe678decf600bed0cdcfa481487bce91b3732bdd7c46338f9102ccc8180c41032811e99962883715 languageName: node linkType: hard @@ -25420,78 +25454,78 @@ __metadata: languageName: node linkType: hard -"postcss-merge-longhand@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-merge-longhand@npm:7.0.4" +"postcss-merge-longhand@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-merge-longhand@npm:7.0.5" dependencies: postcss-value-parser: "npm:^4.2.0" - stylehacks: "npm:^7.0.4" + stylehacks: "npm:^7.0.5" peerDependencies: - postcss: ^8.4.31 - checksum: 10/b94b98a9b21bc8671aa0fba96491e8e2deea57c9bbfe9a74305400a36035b63764d8bfbdc6bda047887b665b92b91361a8bbc1cb9c14f80b3792feef19881005 + postcss: ^8.4.32 + checksum: 10/3378fc3a196082dfdb9acff94efbfa0de95ed86bf87f485285e775fd3c21218e5a243e363ad80b96237edb454776f7c1deea28c37afb8b96ddfaf5cfe8bd606b languageName: node linkType: hard -"postcss-merge-rules@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-merge-rules@npm:7.0.4" +"postcss-merge-rules@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-merge-rules@npm:7.0.5" dependencies: - browserslist: "npm:^4.23.3" + browserslist: "npm:^4.24.5" caniuse-api: "npm:^3.0.0" - cssnano-utils: "npm:^5.0.0" - postcss-selector-parser: "npm:^6.1.2" + cssnano-utils: "npm:^5.0.1" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/f67a4f6e814c5e7ce990a3c3d699e1c1dba7e79c5cb3a11795534d47b0fa257d27465e248546b45104d8278dfcbd07d9fbceb8046fcdfac86fe6340ca3c85f9a + postcss: ^8.4.32 + checksum: 10/fa490791ea5e907e4498701593252ce33df468a821e5f3acf5f126f73c8262189c13ca7a0c1645ae3d66a46a03cf930048e10d808182a3e9bec78af30a02893a languageName: node linkType: hard -"postcss-minify-font-values@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-minify-font-values@npm:7.0.0" +"postcss-minify-font-values@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-minify-font-values@npm:7.0.1" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/8578c1d1d4d65ca34db5ac0cccc7b73500040e52a3abb8abc7e5b6e47e5f72c88bfe5f3b19847556a2a68082245009d693a7c098b8bc58e7f9640abba4e80194 + postcss: ^8.4.32 + checksum: 10/6578a1fd293e202e738ce38d91d71c08ba970f4a998edff48022cb21ec23ef26bf7d284ddb41d6e51bf20b5b5676fe142de1bd092a76d2ef982d5ee1d6b00190 languageName: node linkType: hard -"postcss-minify-gradients@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-minify-gradients@npm:7.0.0" +"postcss-minify-gradients@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-minify-gradients@npm:7.0.1" dependencies: colord: "npm:^2.9.3" - cssnano-utils: "npm:^5.0.0" + cssnano-utils: "npm:^5.0.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/9649e255ad954e67e0d7c2111b0f1681a93e8cba7179a547491eacf135d64596dfee9774b589d7a46ee3ace673a026113e56e734d6ab19297367f11dd3104c0e + postcss: ^8.4.32 + checksum: 10/4aa782331c5d1826e549b3940eefb54e2d51f5c5a2c5f5537384bfe6eac45bfe7ba4535c03cd1642d8a27ab088f56c3682b55f5dd2c3f7969b715692e0c1102b languageName: node linkType: hard -"postcss-minify-params@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-minify-params@npm:7.0.2" +"postcss-minify-params@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-minify-params@npm:7.0.3" dependencies: - browserslist: "npm:^4.23.3" - cssnano-utils: "npm:^5.0.0" + browserslist: "npm:^4.24.5" + cssnano-utils: "npm:^5.0.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/26b6ce4db3cdefcceb7a00b64dfbd27dee4194b55708937dddd5c4000c1f02013dc0659e62e799dc1ce1f1a697961cec55a2a746a4f59d54ccae4b68adf41768 + postcss: ^8.4.32 + checksum: 10/97de22d6ba0310685d33b530dbfeefa930f7ac48effe623fc8a4a59d2b98bed221d0d2edad4f2e1f4590322240d0e1e94bdb162069c40b5d7ae00c58637c90c9 languageName: node linkType: hard -"postcss-minify-selectors@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-minify-selectors@npm:7.0.4" +"postcss-minify-selectors@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-minify-selectors@npm:7.0.5" dependencies: cssesc: "npm:^3.0.0" - postcss-selector-parser: "npm:^6.1.2" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/54c74dcb098819417e95ec2b5ecdd33a2c6fdccea2346e110037c762d37644e11f83d67e6b0c93405f2b7cc28880ca0a07ad4d6618330436f7b8b84d719b85fb + postcss: ^8.4.32 + checksum: 10/12580d9a17c146c9e9bb604b4887085d897554317590cee91e0f28e2a4757c18e09299365a44eae25e848e65d53b845928dfa56a9d0199d0e159d525732fbf89 languageName: node linkType: hard @@ -25539,136 +25573,136 @@ __metadata: languageName: node linkType: hard -"postcss-normalize-charset@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-charset@npm:7.0.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/a41043fb81a1d5b3b05e8b317de7fe123854a4535f9ce2904a16196a32b3565d2fd6ac59a9842e337cf1bb298dcc108cbdbc6a5d4a500aec3520d759e951a8de - languageName: node - linkType: hard - -"postcss-normalize-display-values@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-display-values@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/55bbfb4dac3bf9bcc2aed30057c0bc968927b5337b372ee2dd825d6ec626c18d1481b0e8dd928d4cab70c3e8a2e6708d6115b14bebd34fe4462eb15aacff35f4 - languageName: node - linkType: hard - -"postcss-normalize-positions@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-positions@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/a6b982e567ddf1ad4120aaf898056f2fdbe5f6cae1d475fef22cb1f025c9bfe37df5511a4353b9f13d01feae8b1d9638c1deb70537058312262647052d004f64 - languageName: node - linkType: hard - -"postcss-normalize-repeat-style@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-repeat-style@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/f8ef8cf5ac6232f1d0615a97f21ea464a6930484b58421c87e0f9e626b1bb52916592f25e4f9874f424b1529807b170d8805d45878aa8293ea0608dd753230c8 - languageName: node - linkType: hard - -"postcss-normalize-string@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-string@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/23ea7dd7b28880dfafd0880ab782d65186ab94a4cf789b8723f9666020c7f7c8b97546e0dc46d08da3f71a873bb6db41cd69a4cafb4fde4a85f97ef83ee38bae - languageName: node - linkType: hard - -"postcss-normalize-timing-functions@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-timing-functions@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/f85870b3c8132b530fb8e5c8474f1eea1d0ef69a374d5867d0300f7501803bffa55f7fad34f662d88a747ce73d552ec0f818722d2d5157cf8e5dc45a98fa552b - languageName: node - linkType: hard - -"postcss-normalize-unicode@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-normalize-unicode@npm:7.0.2" - dependencies: - browserslist: "npm:^4.23.3" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/cb342f7507f28c8e9c500a2d6369c6b04a85f6c6f93aaa1ab6768d0e097453480834d3f7c5fad503f9fb9e178d9011df50ceaeebe2ac68d5daaa7c8a63ad3b3f - languageName: node - linkType: hard - -"postcss-normalize-url@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-url@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/c5edca0646a13d76c5347fffaaa828184e035486d7eeb2a8b31781d30de6a90f7ad3f0cffe59e8fd4c31f1525fdb85b45777745685603ac533a151c42691f601 - languageName: node - linkType: hard - -"postcss-normalize-whitespace@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-whitespace@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/c409362e3256ed66629fc48c63e834c9bfb598ca20587adb620bbc04fdccef4cd0d08b1f485eb8290d6a30e8dd836fecb0def38c3a49fe8503e2579e60f5bccf - languageName: node - linkType: hard - -"postcss-ordered-values@npm:^7.0.1": +"postcss-normalize-charset@npm:^7.0.1": version: 7.0.1 - resolution: "postcss-ordered-values@npm:7.0.1" - dependencies: - cssnano-utils: "npm:^5.0.0" - postcss-value-parser: "npm:^4.2.0" + resolution: "postcss-normalize-charset@npm:7.0.1" peerDependencies: - postcss: ^8.4.31 - checksum: 10/048082c09eee021d97def02eb8fc03fb0414402b1f6925af29a862f537b66b43d7a8e8d94c552ca67cd6172230873260f4ad44f1d5bac81c553afb054d80e6a8 + postcss: ^8.4.32 + checksum: 10/bcec822491e3421b009c688473433164b5c80bbef48af4e47f704bee68f0b7ba2009aaf46788e698dd233d5f4e1cf444a4f59a901623c73f8458c2227b15db57 languageName: node linkType: hard -"postcss-reduce-initial@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-reduce-initial@npm:7.0.2" +"postcss-normalize-display-values@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-display-values@npm:7.0.1" dependencies: - browserslist: "npm:^4.23.3" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/53f341c17a5487639e6f7c917ad695e059bf4aff66b3c971e008163f774337444753310def9f38dd26066ea96b136422592fc74077c38c40b3bfdfaa338d5b58 + languageName: node + linkType: hard + +"postcss-normalize-positions@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-positions@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/72b23ab87c97c155d2ec475fba8a8b968f7c7b42d055a79b267449d570c328d5ea4cb0002428cf26e9daa70c58655e0b931d2a5801cc407554d3f03a21ac041b + languageName: node + linkType: hard + +"postcss-normalize-repeat-style@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-repeat-style@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/db677bceec8c00a1860b64932b99af937e7674b3e5c5ac333c95efb090e9abd747eca4ad51855f0fe73fbe544c3d21e58d06b39e03fd525945309743e31ec235 + languageName: node + linkType: hard + +"postcss-normalize-string@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-string@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/48df2eaca6f5365af31ad46fd60a32dc7b714cc5ec8ba80980e65855ddc47c03ac82077ce7ca04c90898f73d173410d1d6a104754ff487e7e5a59e3eae8325b3 + languageName: node + linkType: hard + +"postcss-normalize-timing-functions@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-timing-functions@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/31fb88489244334295918fa7d6af2d76c310a83abd20be0a7f1c408c54ac0c0f81b0ae7877698bf66de1f76495766e159c8871387407dfcafa0cb1a53f5f0460 + languageName: node + linkType: hard + +"postcss-normalize-unicode@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-normalize-unicode@npm:7.0.3" + dependencies: + browserslist: "npm:^4.24.5" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/fc10205655f77d6467da811fbd26aa607c519cbf162ae2ba40821cf64227233445490881119c820c6988c0943cb2f4dc755abe94cb30637001ca35cce5d07b61 + languageName: node + linkType: hard + +"postcss-normalize-url@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-url@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/975dd0d1b55b637d45756ec57e554b2134f77368dd3ae09be9fa6636f2f41e72422505409d7fca75c635b9b1b8ec8ec2607d84c6c85497bbfd4e7748a2992882 + languageName: node + linkType: hard + +"postcss-normalize-whitespace@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-whitespace@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/05a0fa74f4c8e93243053b9cc865cbddddb309b2ccb08271ca9c38ea7ece2ff43d5faa12cce87f06e40cbcf22c94443c9fa2b74ed0c6b94d72a9e67ea0381626 + languageName: node + linkType: hard + +"postcss-ordered-values@npm:^7.0.2": + version: 7.0.2 + resolution: "postcss-ordered-values@npm:7.0.2" + dependencies: + cssnano-utils: "npm:^5.0.1" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/be8fb13639fb0e1ffd7d4e9bb4824d3a283c8a63a8b0dd1a654435fff1e019007c79be877940bb101bb9ebd8ba3ac18bcffd144e939890bedeb40044dcc2b9cc + languageName: node + linkType: hard + +"postcss-reduce-initial@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-reduce-initial@npm:7.0.3" + dependencies: + browserslist: "npm:^4.24.5" caniuse-api: "npm:^3.0.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/5a8260cbf7fa6ea12908debe23e191bb45109b29048d15e63c60df42c4ed62c860273ce9b37172d5f31c4bdb965e984962e4e6f506939a1fc49202dd7bf520c5 + postcss: ^8.4.32 + checksum: 10/8fd9ff4b49a2f7e1b7c51b7da637578e32a178363e3e932c80565241454dca306658dacd390ad3d73647d55dace8be8fe29278668afa32fd9d872ee7026bdbf7 languageName: node linkType: hard -"postcss-reduce-transforms@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-reduce-transforms@npm:7.0.0" +"postcss-reduce-transforms@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-reduce-transforms@npm:7.0.1" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/1c369a1be820a80e8bf06376476190fe2ae5a0b5a7459257d7d9b5bc0c9aed79f46026e8558fca088f7a814e632c678f67749b246901a3839f2d50b7b9ec2d41 + postcss: ^8.4.32 + checksum: 10/a22d07559859b9d4313d579104a25aa254695bc37dec5134de1064d1bd52b9d1f33f050fbf330170ef1105ede9aad7741bbcf9cad2221a6a5c8d529fd3cf0259 languageName: node linkType: hard @@ -25719,36 +25753,36 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-selector-parser@npm:7.0.0" +"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.0": + version: 7.1.0 + resolution: "postcss-selector-parser@npm:7.1.0" dependencies: cssesc: "npm:^3.0.0" util-deprecate: "npm:^1.0.2" - checksum: 10/0e92be7281e2b440a8be8cf207de40a24ca7bc765577916499614d5a47827a3e658206728cc559db96803e554270516104aad919a04f91bfa8914ccef1ba14ca + checksum: 10/2caf09e66e2be81d45538f8afdc5439298c89bea71e9943b364e69dce9443d9c5ab33f4dd8b237f1ed7d2f38530338dcc189c1219d888159e6afb5b0afe58b19 languageName: node linkType: hard -"postcss-svgo@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-svgo@npm:7.0.1" +"postcss-svgo@npm:^7.0.2": + version: 7.0.2 + resolution: "postcss-svgo@npm:7.0.2" dependencies: postcss-value-parser: "npm:^4.2.0" svgo: "npm:^3.3.2" peerDependencies: - postcss: ^8.4.31 - checksum: 10/4196d9b7ec37ea7c427b6d3d40fa75bdae6d1fdf5a814481202138fb9b074ecc1e442b8e0202aa8c76eaaff747e2f6bfec968cfe7bc774d8a58faf8bd945ff4e + postcss: ^8.4.32 + checksum: 10/8615877dffbac2bb2b971fb0e8c882ebff479c2529a0fc20937d09623fcaf35a2d934c4046188bae2534729aba1de5a1ba227630aaf96a800b6f2acdbfbf1d32 languageName: node linkType: hard -"postcss-unique-selectors@npm:^7.0.3": - version: 7.0.3 - resolution: "postcss-unique-selectors@npm:7.0.3" +"postcss-unique-selectors@npm:^7.0.4": + version: 7.0.4 + resolution: "postcss-unique-selectors@npm:7.0.4" dependencies: - postcss-selector-parser: "npm:^6.1.2" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/c38ca6b5f539cae1e0e8ef0efa338f91e4e054dbd9c619e26708d787e94ce788739bbe782103f2cf35c38819233897901038292255a1726905bd04433ac9e5f2 + postcss: ^8.4.32 + checksum: 10/b880f96fdb20037b16ae21b48f5240a4cf8585bf3133c7894dd869711b14f3a1a82bbdecd36adc78f8c34553a46fc2199ed3e92d5031b0267ff6f43894fc00f7 languageName: node linkType: hard @@ -25759,14 +25793,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.5.1, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.5.1": - version: 8.5.1 - resolution: "postcss@npm:8.5.1" +"postcss@npm:8.5.6, postcss@npm:^8.4.33, postcss@npm:^8.4.40, postcss@npm:^8.5.1": + version: 8.5.6 + resolution: "postcss@npm:8.5.6" dependencies: - nanoid: "npm:^3.3.8" + nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10/1fbd28753143f7f03e4604813639918182b15343c7ad0f4e72f3875fc2cc0b8494c887f55dc05008fad5fbf1e1e908ce2edbbce364a91f84dcefb71edf7cd31d + checksum: 10/9e4fbe97574091e9736d0e82a591e29aa100a0bf60276a926308f8c57249698935f35c5d2f4e80de778d0cbb8dcffab4f383d85fd50c5649aca421c3df729b86 languageName: node linkType: hard @@ -28814,13 +28848,6 @@ __metadata: languageName: node linkType: hard -"shimmer@npm:^1.2.1": - version: 1.2.1 - resolution: "shimmer@npm:1.2.1" - checksum: 10/aa0d6252ad1c682a4fdfda69e541be987f7a265ac7b00b1208e5e48cc68dc55f293955346ea4c71a169b7324b82c70f8400b3d3d2d60b2a7519f0a3522423250 - languageName: node - linkType: hard - "short-unique-id@npm:^5.3.2": version: 5.3.2 resolution: "short-unique-id@npm:5.3.2" @@ -30004,19 +30031,6 @@ __metadata: languageName: node linkType: hard -"strong-log-transformer@npm:2.1.0": - version: 2.1.0 - resolution: "strong-log-transformer@npm:2.1.0" - dependencies: - duplexer: "npm:^0.1.1" - minimist: "npm:^1.2.0" - through: "npm:^2.3.4" - bin: - sl-log-transformer: bin/sl-log-transformer.js - checksum: 10/2fd14eb0a68893fdadefd89f964df404e3d637729c48aca015eb12d1c47455dee28b2522ad7150de23f7a57cce503656585e7644c9cd8532023ea572f8cc5a80 - languageName: node - linkType: hard - "strtok3@npm:^6.2.4": version: 6.3.0 resolution: "strtok3@npm:6.3.0" @@ -30052,15 +30066,15 @@ __metadata: languageName: node linkType: hard -"stylehacks@npm:^7.0.4": - version: 7.0.4 - resolution: "stylehacks@npm:7.0.4" +"stylehacks@npm:^7.0.5": + version: 7.0.5 + resolution: "stylehacks@npm:7.0.5" dependencies: - browserslist: "npm:^4.23.3" - postcss-selector-parser: "npm:^6.1.2" + browserslist: "npm:^4.24.5" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/fc9d6b1e0b996d139a77f391df6db49ee1ab7e8fdeb32a8fa6b4c11512e72eb072470c32080171e46ebe123c9c96d763b9e4421b09c9c428985077940b6ba085 + postcss: ^8.4.32 + checksum: 10/798ac0f92ff4489c251550d64b903f1aa8b5946e5b09b33ebf68290b5a345257cecf98c989526a5d462b560081194fead38c4f804ec016ceb8b1b3f17ec74fc5 languageName: node linkType: hard @@ -30560,7 +30574,7 @@ __metadata: languageName: node linkType: hard -"through@npm:2, through@npm:2.3.x, through@npm:>=2.2.7 <3, through@npm:^2.3.4, through@npm:^2.3.6, through@npm:^2.3.8": +"through@npm:2, through@npm:2.3.8, through@npm:2.3.x, through@npm:>=2.2.7 <3, through@npm:^2.3.6, through@npm:^2.3.8": version: 2.3.8 resolution: "through@npm:2.3.8" checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 @@ -30616,6 +30630,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:0.2.12": + version: 0.2.12 + resolution: "tinyglobby@npm:0.2.12" + dependencies: + fdir: "npm:^6.4.3" + picomatch: "npm:^4.0.2" + checksum: 10/4ad28701fa9118b32ef0e27f409e0a6c5741e8b02286d50425c1f6f71e6d6c6ded9dd5bbbbb714784b08623c4ec4d150151f1d3d996cfabe0495f908ab4f7002 + languageName: node + linkType: hard + "tinyrainbow@npm:^1.2.0": version: 1.2.0 resolution: "tinyrainbow@npm:1.2.0"