Merge branch 'main' into 86870-canvas-pan-and-zoom

This commit is contained in:
Ihor Yeromin
2025-07-10 20:38:03 +02:00
1210 changed files with 13687 additions and 22123 deletions
+3 -51
View File
@@ -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"],
+3 -2
View File
@@ -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:
@@ -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
@@ -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
+46 -36
View File
@@ -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)
+21 -2
View File
@@ -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
+6 -6
View File
@@ -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
+7
View File
@@ -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=
@@ -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{},
}
}
@@ -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)
}
@@ -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{}
}
@@ -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{},
}
}
@@ -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)
}
@@ -23,5 +23,7 @@ type CheckTypeSpec struct {
// NewCheckTypeSpec creates a new CheckTypeSpec object.
func NewCheckTypeSpec() *CheckTypeSpec {
return &CheckTypeSpec{}
return &CheckTypeSpec{
Steps: []CheckTypeStep{},
}
}
@@ -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,
}
)
@@ -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{
+18 -8
View File
@@ -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
}
+22
View File
@@ -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)
}
+13 -26
View File
@@ -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) {
+9 -9
View File
@@ -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
}
@@ -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: {
@@ -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: {
@@ -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"`
}
@@ -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,
+1
View File
@@ -10,6 +10,7 @@ RoleSpec: {
// Display name of the role
title: string
description: string
version: int
group: string
@@ -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?
@@ -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?
@@ -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?
@@ -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{
@@ -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
}
}
+1 -1
View File
@@ -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
@@ -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
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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/<GRAFANA_VERSION>/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.
@@ -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 ."
},
@@ -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<string, unknown>): Promise<Configuration> => {
const config = async (env: Env): Promise<Configuration> => {
const baseConfig = await grafanaConfig(env);
const customConfig = {
plugins: [
@@ -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 ."
},
@@ -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<string, unknown>): Promise<Configuration> => {
const config = async (env: Env): Promise<Configuration> => {
const baseConfig = await grafanaConfig(env);
const customConfig = {
plugins: [
+3 -14
View File
@@ -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
+6 -34
View File
@@ -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=
+14 -13
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -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 <Context.Provider value={value}>{children}</Context.Provider>;
return <PluginContext.Provider value={value}>{children}</PluginContext.Provider>;
}
@@ -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<T extends KeyValue = KeyValue> {
meta: PluginMeta<T>;
}
export interface DataSourcePluginContextType extends PluginContextType {
export interface DataSourcePluginContextType<T extends KeyValue = KeyValue> extends PluginContextType<T> {
instanceSettings: DataSourceInstanceSettings;
}
export const Context = createContext<PluginContextType | undefined>(undefined);
export const PluginContext = createContext<PluginContextType | undefined>(undefined);
@@ -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<PluginContextProviderProps>): ReactElement {
const { children, ...rest } = props;
return <Context.Provider value={rest}>{children}</Context.Provider>;
return <PluginContext.Provider value={rest}>{children}</PluginContext.Provider>;
}
@@ -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<T extends KeyValue = KeyValue>(
context: PluginContextType<T>
): context is DataSourcePluginContextType<T> {
return 'instanceSettings' in context && 'meta' in context;
}
@@ -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<T extends KeyValue = KeyValue>(): PluginContextType<T> | 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<T>;
}
+1 -1
View File
@@ -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 {
@@ -34,6 +34,11 @@ export interface TransformerRegistryItem<TOptions = any> extends RegistryItem {
* Set of categories associated with the transformer
*/
categories?: Set<TransformerCategory>;
/**
* Set of tags associated with the transformer for improved transformation search
*/
tags?: Set<string>;
}
export enum TransformerCategory {
@@ -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: {},
},
@@ -37,6 +37,12 @@ export const limitTransformer: DataTransformerInfo<LimitTransformerOptions> = {
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),
};
@@ -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;
/**
@@ -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 = '南';
@@ -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);
}
}
+1 -1
View File
@@ -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",
@@ -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',
+1 -1
View File
@@ -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';
<FlameGraph
getTheme={() => createTheme({ colors: { mode: 'dark' } })}
+1 -1
View File
@@ -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",
+2 -1
View File
@@ -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",
+3 -11
View File
@@ -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", "."]
}
@@ -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;
}
+9 -2
View File
@@ -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<Record<string, string>> {
@@ -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<Configuration> => {
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<Configuration> => {
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<Configuration> => {
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<Configuration> => {
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<Configuration> => {
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'
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -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<TableFieldOptions> = {
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<TableFieldOptions> = {
align: 'auto',
inspect: false,
};
/**
* A specific timezone from https://en.wikipedia.org/wiki/Tz_database
*/
+8 -2
View File
@@ -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")
@@ -15,6 +15,7 @@ export const pluginVersion = "12.1.0-pre";
export interface Options {
controlsStorageKey?: string;
dedupStrategy: common.LogsDedupStrategy;
detailsMode?: ('inline' | 'sidebar');
displayedFields?: Array<string>;
enableInfiniteScrolling?: boolean;
enableLogDetails: boolean;
@@ -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<string, any>;
}
+4 -4
View File
@@ -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",
@@ -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),
},
},
},
},
},
},
}),
});
}),
};
};
@@ -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<React.SVGProps<SVGElement>, 'onLoad' | 'onError' | 'ref'> {
name: IconName;
@@ -53,12 +53,10 @@ export const Icon = React.forwardRef<SVGElement, IconProps>(
// 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,
@@ -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`;
}
@@ -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 && (
<Portal>
<div
ref={refCallback}
{...getReferenceProps()}
{...getFloatingProps()}
style={floatingStyles}
className={styles.tooltipWrapper}
data-testid={selectors.components.DataLinksActionsTooltip.tooltipWrapper}
>
<VizTooltipWrapper>
<VizTooltipFooter dataLinks={links} actions={actions} />
</VizTooltipWrapper>
</div>
</Portal>
)}
<Portal>
<div
ref={refCallback}
{...getReferenceProps()}
{...getFloatingProps()}
style={floatingStyles}
className={styles.tooltipWrapper}
data-testid={selectors.components.DataLinksActionsTooltip.tooltipWrapper}
>
<VizTooltipWrapper>
<VizTooltipFooter dataLinks={links} actions={actions} />
</VizTooltipWrapper>
</div>
</Portal>
</>
);
};
@@ -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 (
<div className={styles.buttonsGap}>
{actions && actions.map((action, i) => <ActionButton key={i} action={action} variant="secondary" />)}
{actions.map((action, i) => (
<ActionButton key={i} action={action} variant="secondary" />
))}
</div>
);
};
@@ -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(
<AutoCell
value="test"
field={field}
justifyContent="normal"
rowIdx={0}
cellOptions={{ type: TableCellDisplayMode.Auto }}
/>
);
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(
<AutoCell
value="test"
field={field}
justifyContent="normal"
rowIdx={0}
cellOptions={{ type: TableCellDisplayMode.Auto }}
/>
);
const cell = screen.getByTestId(selectors.components.TablePanel.autoCell);
await userEvent.click(cell);
expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument();
});
});
});
@@ -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<DataLinksActionsTooltipCoords>();
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
<div
className={styles.cell}
onClick={tooltipOnClickHandler(setTooltipCoords)}
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
data-testid={selectors.components.TablePanel.autoCell}
>
{shouldShowLink ? (
renderSingleLink(links[0], formattedValue, getLinkStyle(styles, cellOptions))
) : shouldShowTooltip ? (
<DataLinksActionsTooltip
links={links}
actions={actions}
value={formattedValue}
coords={tooltipCoords}
onTooltipClose={() => setTooltipCoords(undefined)}
/>
) : (
formattedValue
)}
<div className={styles.cell}>
{link == null ? formattedValue : renderSingleLink(link, formattedValue, getLinkStyle(styles, cellOptions))}
</div>
);
}
@@ -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<DataLinksActionsTooltipCoords>();
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
const renderComponent = () => {
return (
<BarGauge
width={width}
height={height - heightOffset}
field={config}
display={field.display}
text={{ valueSize: 14 }}
value={displayValue}
orientation={VizOrientation.Horizontal}
theme={theme}
alignmentFactors={alignmentFactors}
itemSpacing={1}
lcdCellWidth={8}
displayMode={barGaugeMode}
valueDisplayMode={valueDisplayMode}
/>
);
};
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
onClick={tooltipOnClickHandler(setTooltipCoords)}
>
{shouldShowLink ? (
renderSingleLink(links[0], renderComponent())
) : shouldShowTooltip ? (
<DataLinksActionsTooltip
links={links}
actions={actions}
value={renderComponent()}
coords={tooltipCoords}
onTooltipClose={() => setTooltipCoords(undefined)}
/>
) : (
renderComponent()
)}
</div>
const barGaugeComponent = (
<BarGauge
width={width}
height={height - heightOffset}
field={config}
display={field.display}
text={{ valueSize: 14 }}
value={displayValue}
orientation={VizOrientation.Horizontal}
theme={theme}
alignmentFactors={alignmentFactors}
itemSpacing={1}
lcdCellWidth={8}
displayMode={barGaugeMode}
valueDisplayMode={valueDisplayMode}
/>
);
const link = useSingleLink(field, rowIdx);
return link == null ? barGaugeComponent : renderSingleLink(link, barGaugeComponent);
};
@@ -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<DataLinksActionsTooltipCoords>();
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 = <img alt={alt} src={text} className={styles.image} title={title} />;
const link = useSingleLink(field, rowIdx);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div
className={styles.imageContainer}
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
onClick={tooltipOnClickHandler(setTooltipCoords)}
>
{shouldShowLink ? (
renderSingleLink(links[0], img)
) : shouldShowTooltip ? (
<DataLinksActionsTooltip
links={links}
actions={actions}
value={img}
coords={tooltipCoords}
onTooltipClose={() => setTooltipCoords(undefined)}
/>
) : (
img
)}
</div>
);
return <div className={styles.imageContainer}>{link == null ? img : renderSingleLink(link, img)}</div>;
};
const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({
@@ -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<DataLinksActionsTooltipCoords>();
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
<div
className={styles.jsonText}
onClick={tooltipOnClickHandler(setTooltipCoords)}
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
>
{shouldShowLink ? (
renderSingleLink(links[0], displayValue)
) : shouldShowTooltip ? (
<DataLinksActionsTooltip
links={links}
actions={actions}
value={displayValue}
coords={tooltipCoords}
onTooltipClose={() => setTooltipCoords(undefined)}
/>
) : (
displayValue
)}
</div>
);
return <div className={styles.jsonText}>{link == null ? displayValue : renderSingleLink(link, displayValue)}</div>;
};
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({
@@ -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(<PillCell {...defaultProps} />);
expect(screen.getByText('test-value')).toBeInTheDocument();
});
it('should render pills for CSV values', () => {
render(<PillCell {...defaultProps} value="value1,value2,value3" />);
expect(screen.getByText('value1')).toBeInTheDocument();
expect(screen.getByText('value2')).toBeInTheDocument();
expect(screen.getByText('value3')).toBeInTheDocument();
});
it('should render pills for JSON array values', () => {
render(<PillCell {...defaultProps} value='["item1","item2","item3"]' />);
expect(screen.getByText('item1')).toBeInTheDocument();
expect(screen.getByText('item2')).toBeInTheDocument();
expect(screen.getByText('item3')).toBeInTheDocument();
});
it('should show dash for empty values', () => {
render(<PillCell {...defaultProps} value="" />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should show dash for null values', () => {
render(<PillCell {...defaultProps} value={null as unknown as string} />);
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(<PillCell {...defaultProps} value="success,error,warning,unknown" cellOptions={mappedOptions} />);
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(
<PillCell
{...defaultProps}
value="success,error,warning,unknown"
cellOptions={mappedOptions}
field={fieldWithMappings}
/>
);
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(<PillCell {...defaultProps} cellOptions={fixedOptions} />);
expect(screen.getByText('test-value')).toBeInTheDocument();
});
it('should use auto color when colorMode is auto', () => {
const autoOptions: TablePillCellOptions = {
type: TableCellDisplayMode.Pill,
colorMode: 'auto',
};
render(<PillCell {...defaultProps} cellOptions={autoOptions} />);
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']);
});
});
@@ -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 <div className={styles.cell}>-</div>;
}
return (
<div className={styles.cell}>
<div className={styles.pillsContainer}>
{pills.map((pill) => (
<span
key={pill.key}
className={styles.pill}
style={{
backgroundColor: pill.bgColor,
color: pill.color,
}}
>
{pill.value}
</span>
))}
</div>
</div>
);
}
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',
}),
});
@@ -24,7 +24,10 @@ export function TableCellActions(props: TableCellActionsProps) {
} = props;
return (
<div className={className}>
// 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
<div className={className} onClick={(ev) => ev.stopPropagation()}>
{cellInspect && (
<IconButton
name="eye"
@@ -39,7 +42,8 @@ export function TableCellActions(props: TableCellActionsProps) {
dataProjection: 'EPSG:4326',
});
mode = TableCellInspectorMode.code;
} else if ('cellType' in cellOptions && cellOptions.cellType === TableCellDisplayMode.JSONView) {
}
if (cellOptions.type === TableCellDisplayMode.JSONView) {
mode = TableCellInspectorMode.code;
}
@@ -141,6 +141,14 @@ describe('TableNG Cells renderers', () => {
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);
});
});
});
@@ -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) => (
<JSONCell
justifyContent={props.justifyContent}
value={props.value}
field={props.field}
rowIdx={props.rowIdx}
actions={props.actions}
/>
<JSONCell justifyContent={props.justifyContent} value={props.value} field={props.field} rowIdx={props.rowIdx} />
);
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) => <DataLinksCell field={props.field} rowIdx={props.rowIdx} />;
const ACTIONS_RENDERER: TableCellRenderer = (props) => <ActionsCell actions={props.actions} />;
const ACTIONS_RENDERER: TableCellRenderer = ({ field, rowIdx, getActions = () => [] }) => (
<ActionsCell field={field} rowIdx={rowIdx} getActions={getActions} />
);
const PILL_RENDERER: TableCellRenderer = (props) => <PillCell {...props} />;
function isCustomCellOptions(options: TableCellOptions): options is TableCustomCellOptions {
return options.type === TableCellDisplayMode.Custom;
@@ -104,6 +100,7 @@ const CELL_RENDERERS: Record<TableCellOptions['type'], TableCellRenderer> = {
[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 */
@@ -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(<TableNG enableVirtualization={false} data={dataFrame} width={800} height={600} />);
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(<TableNG enableVirtualization={false} data={dataFrame} width={800} height={600} />);
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();
});
});
});
@@ -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<TableRow, TableSummaryRow>) => 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<DataGridProps<TableRow, TableSummaryRow>>,
[
enableVirtualization,
resizeHandler,
sortColumns,
headerHeight,
styles.headerRow,
rowHeight,
hasFooter,
setSortColumns,
onSortByChange,
]
[enableVirtualization, resizeHandler, sortColumns, rowHeight, hasFooter, setSortColumns, onSortByChange]
);
interface Schema {
columns: TableColumn[];
cellRootRenderers: Record<string, CellRootRenderer>;
colsWithTooltip: Record<string, boolean>;
}
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<TableRow, TableSummaryRow>): 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 (
<Cell
@@ -335,14 +336,11 @@ export function TableNG(props: TableNGProps) {
const renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): 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 && (
<TableCellActions
@@ -427,6 +426,7 @@ export function TableNG(props: TableNGProps) {
return result;
}
const hasNestedHeaders = firstNestedData.meta?.custom?.noHeader !== true;
const renderRow = renderRowFactory(firstNestedData.fields, panelContext, expandedRows, enableSharedCrosshair);
const { columns: nestedColumns, cellRootRenderers: nestedCellRootRenderers } = fromFields(
firstNestedData.fields,
@@ -476,11 +476,20 @@ export function TableNG(props: TableNGProps) {
}
const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns);
if (!expandedRecords.length) {
return (
<div className={styles.noDataNested}>
<Trans i18nKey="grafana-ui.table.nested-table.no-data">No data</Trans>
</div>
);
}
return (
<DataGrid<TableRow, TableSummaryRow>
{...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<DataLinksActionsTooltipState>();
return (
<>
<DataGrid<TableRow, TableSummaryRow>
@@ -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) {
</div>
)}
{tooltipState && (
<DataLinksActionsTooltip
links={tooltipState.links ?? []}
actions={tooltipState.actions}
coords={tooltipState.coords}
onTooltipClose={() => setTooltipState(undefined)}
/>
)}
{isContextMenuOpen && (
<ContextMenu
x={contextMenuProps?.left || 0}
@@ -655,17 +695,20 @@ const renderRowFactory =
const getGridStyles = (
theme: GrafanaTheme2,
{ enablePagination, noHeader }: { enablePagination?: boolean; noHeader?: boolean }
{ enablePagination, transparent }: { enablePagination?: boolean; transparent?: boolean }
) => ({
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,
}),
},
}),
};
};
@@ -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,
};
@@ -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]);
}
@@ -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<KeyValue<string>> | 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<TableFieldOptions, 'cellOptions'> & {
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;
@@ -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
);
}
@@ -57,7 +57,7 @@ export interface TableCellProps extends CellProps<any> {
onCellFilterAdded?: TableFilterActionCallback;
innerWidth: number;
frame: DataFrame;
actions?: ActionModel[];
actions?: ActionModel[]; // unused in NG
setInspectCell?: TableInspectCellCallback;
}
@@ -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);
+1 -1
View File
@@ -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)

Some files were not shown because too many files have changed in this diff Show More