Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs

This commit is contained in:
Ryan McKinley
2025-06-30 13:47:39 -07:00
330 changed files with 8259 additions and 4001 deletions
+3 -9
View File
@@ -702,9 +702,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/components/Table/TableNG/Filter/FilterPopup.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -720,15 +718,11 @@ exports[`better eslint`] = {
],
"packages/grafana-ui/src/components/Table/TableNG/utils.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"packages/grafana-ui/src/components/Table/TableNG/utils.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"],
[0, 0, 0, "Do not use any type assertions.", "3"]
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"packages/grafana-ui/src/components/Table/TableRT/Filter.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
+1 -1
View File
@@ -65,7 +65,7 @@ require (
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
+2 -2
View File
@@ -142,8 +142,8 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+1 -1
View File
@@ -18,7 +18,7 @@ require (
github.com/evilmartians/lefthook v1.4.8 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+2 -2
View File
@@ -29,8 +29,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+1 -1
View File
@@ -24,7 +24,7 @@ require (
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
+2 -2
View File
@@ -41,8 +41,8 @@ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3Bum
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 h1:KFcZmKdZmapAog2+eL1buervAYrYolBZk7fMecPPDmo=
github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37/go.mod h1:i1/E+d8iPNReSE7y04FaVu5OPKB3il5cn+T1Egogg3I=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+1
View File
@@ -773,6 +773,7 @@ embed.go @grafana/grafana-as-code
/.github/actions/setup-enterprise/action.yml @grafana/grafana-backend-group
/.github/actions/setup-grafana-bench/ @Proximyst
/.github/actions/build-package @grafana/grafana-developer-enablement-squad
/.github/actions/change-detection @grafana/grafana-developer-enablement-squad
/.github/workflows/actionlint-format.txt @grafana/grafana-developer-enablement-squad
/.github/workflows/actionlint.yml @grafana/grafana-developer-enablement-squad
/.github/workflows/add-to-whats-new.yml @grafana/docs-tooling
+141
View File
@@ -0,0 +1,141 @@
name: Detect changed files
description: Detects whether any matching files have changed in the current PR
inputs:
self:
description: The path to the calling workflow (e.g. .github/workflows/backend-unit-tests.yml). It is regarded as any category.
required: true
outputs:
self:
description: Whether the calling workflow has changed in any way
value: ${{ steps.changed-files.outputs.self_any_changed || 'true' }}
backend:
description: Whether the backend or self have changed in any way
value: ${{ steps.changed-files.outputs.backend_any_changed || 'true' }}
frontend:
description: Whether the frontend or self has changed in any way
value: ${{ steps.changed-files.outputs.frontend_any_changed || 'true' }}
e2e:
description: Whether the e2e tests or self have changed in any way
value: ${{ steps.changed-files.outputs.e2e_any_changed == 'true' ||
steps.changed-files.outputs.backend_any_changed == 'true' ||
steps.changed-files.outputs.frontend_any_changed == 'true' || 'true' }}
dev-tooling:
description: Whether the dev tooling or self have changed in any way
value: ${{ steps.changed-files.outputs.dev_tooling_any_changed || 'true' }}
docs:
description: Whether the docs or self have changed in any way
value: ${{ steps.changed-files.outputs.docs_any_changed || 'true' }}
runs:
using: composite
steps:
# Assumption: We've done a checkout with the actions/checkout action.
# It must persist credentials to allow the changed-files action to get more history.
- name: Detect changes
id: changed-files
if: github.event_name == 'pull_request'
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46
with:
files_yaml: |
self:
- '.github/actions/change-detection/**'
- '${{ inputs.self }}'
backend:
- '!*.md'
- '!docs/**'
- '!.github/**'
- '.github/actions/setup-enterprise/**'
- '.github/actions/checkout/**'
- '**/go.mod'
- '**/go.sum'
- '**.go'
- 'pkg/**'
- '!pkg/**.md'
- 'apps/**'
- '!apps/**.md'
- 'build.sh'
- '.github/actions/change-detection/**'
- '**.cue'
- 'devenv/docker/blocks/*_tests/**'
- 'kindsv2/**'
- '${{ inputs.self }}'
frontend:
- '.github/actions/setup-enterprise/**'
- '.github/actions/checkout/**'
- 'public/**'
- '**.js'
- '**.jsx'
- '**.ts'
- '**.tsx'
- '**.css'
- '**.mjs'
- 'yarn.lock'
- 'package.json'
- '!**.md'
- '.github/actions/change-detection/**'
- '**.cue'
- '.prettier*'
- '.betterer*'
- '.yarnrc.yml'
- 'eslint.config.js'
- 'jest.config.js'
- 'nx.json'
- 'tsconfig.json'
- '.yarn/**'
- '${{ inputs.self }}'
e2e:
- 'e2e/**'
- '.github/actions/setup-enterprise/**'
- '.github/actions/checkout/**'
- 'emails/**'
- 'pkg/**'
- 'proto/**'
- '**/Makefile'
- 'scripts/**'
- '!scripts/drone/**'
- '!**.md'
- '.github/actions/change-detection/**'
- '**.cue'
- 'conf/**'
- 'cypress.config.js'
- '${{ inputs.self }}'
dev_tooling:
- '.github/actions/setup-enterprise/**'
- '.github/actions/checkout/**'
- '**.sh'
- '.trivyignore'
- '.prettierrc.js'
- '**/Makefile'
- 'proto/**.yaml'
- 'pkg/build/**'
- 'pkg/wire/**'
- 'scripts/**'
- '!**.md'
- '.citools/**'
- '.bingo/**'
- '.github/actions/change-detection/**'
- '${{ inputs.self }}'
docs:
- 'contribute/**'
- 'docs/**'
- '**.md'
- 'LICENSE'
- '.vale.ini'
- '.github/actions/change-detection/**'
- '${{ inputs.self }}'
- name: Print all change groups
shell: bash
run: |
echo "Self: ${{ steps.changed-files.outputs.self_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.self_all_changed_files }}"
echo "Backend: ${{ steps.changed-files.outputs.backend_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.backend_all_changed_files }}"
echo "Frontend: ${{ steps.changed-files.outputs.frontend_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.frontend_all_changed_files }}"
echo "E2E: ${{ steps.changed-files.outputs.e2e_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.e2e_all_changed_files }}"
echo " --> ${{ steps.changed-files.outputs.backend_all_changed_files }}"
echo " --> ${{ steps.changed-files.outputs.frontend_all_changed_files }}"
echo "Dev Tooling: ${{ steps.changed-files.outputs.dev_tooling_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.dev_tooling_all_changed_files }}"
echo "Docs: ${{ steps.changed-files.outputs.docs_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.docs_all_changed_files }}"
+4 -4
View File
@@ -34,9 +34,9 @@ runs:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
git clone https://x-access-token:${GH_TOKEN}@github.com/grafana/grafana-enterprise.git ../grafana-enterprise;
cd ../grafana-enterprise
if git checkout ${GITHUB_HEAD_REF}; then
echo "checked out ${GITHUB_HEAD_REF}"
elif git checkout ${GITHUB_BASE_REF}; then
@@ -44,5 +44,5 @@ runs:
else
git checkout main
fi
./build.sh
QUIET=1 ./build.sh
+22 -3
View File
@@ -14,10 +14,29 @@ concurrency:
permissions: {}
jobs:
detect-changes:
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
changed: ${{ steps.detect-changes.outputs.backend }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: true # required to get more history in the changed-files action
fetch-depth: 2
- name: Detect changes
id: detect-changes
uses: ./.github/actions/change-detection
with:
self: .github/workflows/backend-unit-tests.yml
grafana:
# Run this workflow only for PRs from forks
# the `pr-backend-unit-tests-enterprise` workflow will run instead
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
needs: detect-changes
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.changed == 'true'
strategy:
matrix:
shard: [
@@ -53,8 +72,8 @@ jobs:
grafana-enterprise:
# Run this workflow for non-PR events (like pushes to `main` or `release-*`) OR for internal PRs (PRs not from forks)
# If it gets merged into `main` or `release-*`, then a junit test result is uploaded to GCS.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
needs: detect-changes
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.changed == 'true'
strategy:
matrix:
shard: [
+28 -4
View File
@@ -9,13 +9,32 @@ on:
permissions: {}
jobs:
detect-changes:
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
changed: ${{ steps.detect-changes.outputs.frontend }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: true # required to get more history in the changed-files action
fetch-depth: 2
- name: Detect changes
id: detect-changes
uses: ./.github/actions/change-detection
with:
self: .github/workflows/frontend-lint.yml
lint-frontend-prettier:
needs: detect-changes
permissions:
contents: read
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
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.changed == 'true'
name: Lint
runs-on: ubuntu-latest
steps:
@@ -31,11 +50,12 @@ jobs:
- run: yarn run prettier:check
- run: yarn run lint
lint-frontend-prettier-enterprise:
needs: detect-changes
permissions:
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
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.changed == 'true'
name: Lint
runs-on: ubuntu-latest
steps:
@@ -55,12 +75,13 @@ jobs:
- run: yarn run prettier:check
- run: yarn run lint
lint-frontend-typecheck:
needs: detect-changes
permissions:
contents: read
id-token: write
# Run this workflow only for PRs from forks; if it gets merged into `main` or `release-*`,
# the `lint-frontend-typecheck-enterprise` workflow will run instead
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.changed == 'true'
name: Typecheck
runs-on: ubuntu-latest
steps:
@@ -75,11 +96,12 @@ jobs:
- run: yarn install --immutable --check-cache
- run: yarn run typecheck
lint-frontend-typecheck-enterprise:
needs: detect-changes
permissions:
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
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.changed == 'true'
name: Typecheck
runs-on: ubuntu-latest
steps:
@@ -98,9 +120,11 @@ jobs:
- run: yarn install --immutable --check-cache
- run: yarn run typecheck
lint-frontend-betterer:
needs: detect-changes
permissions:
contents: read
id-token: write
if: needs.detect-changes.outputs.changed == 'true'
name: Betterer
runs-on: ubuntu-latest
steps:
+22
View File
@@ -14,7 +14,27 @@ concurrency:
permissions: {}
jobs:
detect-changes:
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
changed: ${{ steps.detect-changes.outputs.e2e }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: true # required to get more history in the changed-files action
fetch-depth: 2
- name: Detect changes
id: detect-changes
uses: ./.github/actions/change-detection
with:
self: .github/workflows/pr-e2e-tests.yml
build-grafana:
needs: detect-changes
if: needs.detect-changes.outputs.changed == 'true'
name: Build & Package Grafana
runs-on: ubuntu-latest-16-cores
permissions:
@@ -41,6 +61,8 @@ jobs:
path: grafana.tar.gz
build-e2e-runner:
needs: detect-changes
if: needs.detect-changes.outputs.changed == 'true'
name: Build E2E test runner
runs-on: ubuntu-latest
permissions:
+22 -2
View File
@@ -9,13 +9,32 @@ on:
permissions: {}
jobs:
detect-changes:
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
changed: ${{ steps.detect-changes.outputs.frontend }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: true # required to get more history in the changed-files action
fetch-depth: 2
- name: Detect changes
id: detect-changes
uses: ./.github/actions/change-detection
with:
self: .github/workflows/pr-frontend-unit-tests.yml
frontend-unit-tests:
permissions:
contents: read
id-token: write
# Run this workflow only for PRs from forks; if it gets merged into `main` or `release-*`,
# the `frontend-unit-tests-enterprise` workflow will run instead
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
needs: detect-changes
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.changed == 'true'
runs-on: ubuntu-latest-8-cores
name: "Unit tests (${{ matrix.chunk }} / 8)"
strategy:
@@ -43,7 +62,8 @@ 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
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.changed == 'true'
runs-on: ubuntu-latest-8-cores
name: "Unit tests (${{ matrix.chunk }} / 8)"
strategy:
+13 -4
View File
@@ -90,6 +90,7 @@ swagger-enterprise-gen: ## Generate API Swagger specification
-x "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" \
-x "github.com/prometheus/alertmanager" \
-i pkg/api/swagger_tags.json \
-t enterprise \
--exclude-tag=alpha \
--include-tag=enterprise
endif
@@ -189,10 +190,18 @@ gen-feature-toggles:
go test -v ./pkg/services/featuremgmt/...; \
fi
.PHONY: gen-go
gen-go:
@echo "generate go files"
$(GO) run $(GO_RACE_FLAG) ./pkg/build/wire/cmd/wire/main.go gen -tags $(WIRE_TAGS) ./pkg/server
.PHONY: gen-go gen-enterprise-go
ifeq ("$(wildcard $(ENTERPRISE_EXT_FILE))","") ## if enterprise is not enabled
gen-enterprise-go:
@echo "skipping re-generating Wire graph for enterprise: not enabled"
else
gen-enterprise-go: ## Generate Wire graph (Enterprise)
@echo "re-generating Wire graph for enterprise"
$(GO) run ./pkg/build/wire/cmd/wire/main.go gen -tags "enterprise" -gen_tags "(enterprise || pro)" -output_file_prefix="enterprise_" ./pkg/server
endif
gen-go: gen-enterprise-go ## Generate Wire graph
@echo "generatng Wire graph"
$(GO) run ./pkg/build/wire/cmd/wire/main.go gen -tags "oss" -gen_tags "(!enterprise && !pro)" ./pkg/server
.PHONY: fix-cue
fix-cue:
+18 -17
View File
@@ -24,6 +24,7 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
@@ -32,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.35.0 // indirect
github.com/grafana/grafana-app-sdk/logging v0.38.2 // 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
@@ -49,32 +50,32 @@ require (
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.9.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+24
View File
@@ -36,6 +36,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -60,10 +62,12 @@ github.com/grafana/grafana-app-sdk v0.30.0 h1:Hqn2pETu2mQ4RpWkZYEQfu01P7xd1Z1Gj+
github.com/grafana/grafana-app-sdk v0.30.0/go.mod h1:jhfqNIovb+Mes2vdMf9iMCWQkp1GTNtyNuExONtiNuk=
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/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/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=
@@ -125,6 +129,7 @@ github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQy
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
@@ -152,32 +157,39 @@ go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
@@ -188,6 +200,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -202,9 +215,11 @@ golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -213,6 +228,7 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -233,6 +249,7 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -253,16 +270,19 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
@@ -281,18 +301,22 @@ k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE=
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/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/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/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/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=
@@ -82,9 +82,12 @@ type LibraryPanelSpec struct {
// The panel type
PluginVersion string `json:"pluginVersion,omitempty"`
// The panel title
// The title of the library panel
Title string `json:"title,omitempty"`
// The title of the panel when displayed in the dashboard
PanelTitle string `json:"panelTitle,omitempty"`
// Library panel description
Description string `json:"description,omitempty"`
@@ -97,11 +100,28 @@ type LibraryPanelSpec struct {
// The default datasource type
Datasource *data.DataSourceRef `json:"datasource,omitempty"`
// The grid position
GridPos GridPos `json:"gridPos,omitempty"`
// Whether the panel is transparent
Transparent bool `json:"transparent,omitempty"`
// The links for the panel
Links []common.Unstructured `json:"links,omitempty"`
// The datasource queries
// +listType=set
Targets []data.DataQuery `json:"targets,omitempty"`
}
// +k8s:deepcopy-gen=true
type GridPos struct {
W int `json:"w"`
H int `json:"h"`
X int `json:"x"`
Y int `json:"y"`
}
// +k8s:deepcopy-gen=true
type LibraryPanelStatus struct {
// Translation warnings (mostly things that were in SQL columns but not found in the saved body)
@@ -9,6 +9,7 @@ package v0alpha1
import (
datav0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
commonv0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@@ -190,6 +191,22 @@ func (in *FacetResult) DeepCopy() *FacetResult {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GridPos) DeepCopyInto(out *GridPos) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GridPos.
func (in *GridPos) DeepCopy() *GridPos {
if in == nil {
return nil
}
out := new(GridPos)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LibraryPanel) DeepCopyInto(out *LibraryPanel) {
*out = *in
@@ -265,6 +282,14 @@ func (in *LibraryPanelSpec) DeepCopyInto(out *LibraryPanelSpec) {
*out = new(datav0alpha1.DataSourceRef)
**out = **in
}
out.GridPos = in.GridPos
if in.Links != nil {
in, out := &in.Links, &out.Links
*out = make([]commonv0alpha1.Unstructured, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Targets != nil {
in, out := &in.Targets, &out.Targets
*out = make([]datav0alpha1.DataQuery, len(*in))
@@ -29,6 +29,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionList(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardWithAccessInfo(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.FacetResult": schema_pkg_apis_dashboard_v0alpha1_FacetResult(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.GridPos": schema_pkg_apis_dashboard_v0alpha1_GridPos(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanel": schema_pkg_apis_dashboard_v0alpha1_LibraryPanel(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelList(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref),
@@ -713,6 +714,47 @@ func schema_pkg_apis_dashboard_v0alpha1_FacetResult(ref common.ReferenceCallback
}
}
func schema_pkg_apis_dashboard_v0alpha1_GridPos(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"w": {
SchemaProps: spec.SchemaProps{
Default: 0,
Type: []string{"integer"},
Format: "int32",
},
},
"h": {
SchemaProps: spec.SchemaProps{
Default: 0,
Type: []string{"integer"},
Format: "int32",
},
},
"x": {
SchemaProps: spec.SchemaProps{
Default: 0,
Type: []string{"integer"},
Format: "int32",
},
},
"y": {
SchemaProps: spec.SchemaProps{
Default: 0,
Type: []string{"integer"},
Format: "int32",
},
},
},
Required: []string{"w", "h", "x", "y"},
},
},
}
}
func schema_pkg_apis_dashboard_v0alpha1_LibraryPanel(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -833,7 +875,14 @@ func schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref common.ReferenceCal
},
"title": {
SchemaProps: spec.SchemaProps{
Description: "The panel title",
Description: "The title of the library panel",
Type: []string{"string"},
Format: "",
},
},
"panelTitle": {
SchemaProps: spec.SchemaProps{
Description: "The title of the panel when displayed in the dashboard",
Type: []string{"string"},
Format: "",
},
@@ -863,6 +912,33 @@ func schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref common.ReferenceCal
Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef"),
},
},
"gridPos": {
SchemaProps: spec.SchemaProps{
Description: "The grid position",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.GridPos"),
},
},
"transparent": {
SchemaProps: spec.SchemaProps{
Description: "Whether the panel is transparent",
Type: []string{"boolean"},
Format: "",
},
},
"links": {
SchemaProps: spec.SchemaProps{
Description: "The links for the panel",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
},
},
},
"targets": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
@@ -886,7 +962,7 @@ func schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref common.ReferenceCal
},
},
Dependencies: []string{
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery", "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef", "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"},
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery", "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.GridPos", "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"},
}
}
@@ -1,6 +1,7 @@
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,DashboardHit,Tags
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,DashboardMetadata,Finalizers
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,FacetResult,Terms
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,LibraryPanelSpec,Links
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,LibraryPanelStatus,Warnings
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SearchResults,Hits
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SortableFields,Fields
+17 -16
View File
@@ -24,14 +24,15 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/grafana-app-sdk/logging v0.38.2 // indirect
github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb // indirect
github.com/grafana/grafana-app-sdk/logging v0.38.2 // 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
@@ -58,27 +59,27 @@ require (
github.com/spf13/pflag v1.0.6 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/tools v0.34.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+23
View File
@@ -35,6 +35,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -55,8 +57,10 @@ github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb h1:oTl2j6/4miQUYmX
github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
github.com/grafana/grafana-app-sdk v0.35.1 h1:zEXubzsQrxGBOzXJJMBwhEClC/tvPi0sfK7NGmlX3RI=
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/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmwA9wvdwdzJTZsv9buM=
github.com/grafana/grafana-app-sdk/logging v0.35.1/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -139,20 +143,27 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
@@ -163,6 +174,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -173,6 +185,7 @@ golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -181,6 +194,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -195,6 +209,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -204,6 +219,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -213,13 +229,16 @@ gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -233,12 +252,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY=
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/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
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/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
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/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
@@ -12,6 +12,7 @@
--web.console.libraries=/usr/share/prometheus/console_libraries
--web.console.templates=/usr/share/prometheus/consoles
--web.config.file=/etc/prometheus/web.yml
--web.enable-remote-write-receiver
node_exporter:
image: prom/node-exporter
@@ -26,7 +27,7 @@
FD_DATASOURCE: prom
alertmanager:
image: prom/alertmanager
image: prom/alertmanager
volumes:
- ${PWD}/docker/blocks/prometheus/alertmanager.yml:/etc/alertmanager/alertmanager.yml
command: >
@@ -59,18 +59,22 @@ For more information, refer to [this GitHub issue](https://github.com/grafana/gr
## High load on database caused by a high number of alert instances
If you have a high number of alert instances, it can happen that the load on the database gets very high, as each state
transition of an alert instance is saved in the database.
transition of an alert instance is saved in the database after every evaluation.
### Compressed alert state
When the `alertingSaveStateCompressed` feature toggle is enabled, Grafana saves the alert rule state in a compressed form, reducing database overhead for alerts with many instances.
When the `alertingSaveStateCompressed` feature toggle is enabled, Grafana saves the alert rule state in a compressed form. Instead of performing an individual SQL update for each alert instance, Grafana performs a single SQL update per alert rule, updating all alert instances belonging to that rule.
This can significantly reduce database overhead for alert rules with many alert instances.
### Save state periodically
High load can be also prevented by writing to the database periodically. For this the feature flag `alertingSaveStatePeriodic` needs
to be enabled. By default, it saves the states every 5 minutes to the database and on each shutdown. The periodic interval
can also be configured using the `state_periodic_save_interval` configuration flag. During this process, Grafana deletes all existing alert instances from the
database and then writes the entire current set of instances back in batches in a single transacton.
High load can be also prevented by writing to the database periodically, instead of after every evaluation.
To save state periodically, enable the `alertingSaveStatePeriodic` feature toggle.
By default, it saves the states every 5 minutes to the database and on each shutdown. The periodic interval
can also be configured using the `state_periodic_save_interval` configuration flag. During this process, Grafana deletes all existing alert instances from the database and then writes the entire current set of instances back in batches in a single transaction.
Configure the size of each batch using the `state_periodic_save_batch_size` configuration option.
The time it takes to write to the database periodically can be monitored using the `state_full_sync_duration_seconds` metric
@@ -325,6 +325,24 @@ To edit dashboards, follow these steps:
1. Click **Save**.
1. Toggle off the edit mode switch.
### Undo and redo
When a dashboard is in edit mode, you can undo and redo changes you've made using the buttons on the toolbar:
{{< figure src="/media/docs/grafana/dashboards/screenshot-undo-redo-icons-v12.0.png" max-width="500px" alt="Undo and redo buttons" >}}
When you've made a change and hover the cursor over the buttons, the tooltip displays the change you're about to undo or redo.
Also, you can continue undoing or redoing as many changes as you need:
{{< video-embed src="/media/docs/grafana/dashboards/screenrecord-undo-redo-v12.0.mp4" >}}
The undo and redo buttons are only available at the dashboard level and only apply to changes made there, such as dashboard layout and grouping and high-level dashboard or panel updates.
They aren't visible and don't apply when you're configuring a panel or making changes in the dashboard settings.
{{< admonition type="note" >}}
Not all dashboard edit actions can be undone or redone yet.
{{< /admonition >}}
## Move or resize a panel
<!-- previous headings Move a panel & Resize a panel -->
@@ -17,11 +17,6 @@ labels:
title: Bar chart
weight: 100
refs:
standard-calculations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/calculation-types/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types/
standard-options-definitions:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#max
@@ -32,11 +27,6 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/#add-a-field-override
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/#add-a-field-override
configure-data-links:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-data-links/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-data-links/
time-series:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/time-series/
@@ -23,6 +23,21 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/time-series/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/time-series/
color-scheme:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#color-scheme
configure-field-overrides:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/
add-a-field-override:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/#add-a-field-override
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/#add-a-field-override
---
# Candlestick
@@ -162,6 +177,7 @@ When you set the **Tooltip mode** to **All**, the **Values sort order** option i
#### Hover proximity
Set the hover proximity (in pixels) to control how close the cursor must be to a data point to trigger the tooltip to display.
The following screen recording shows this option in a time series visualization:
![Adding a hover proximity limit for tooltips](/media/docs/grafana/gif-grafana-10-4-hover-proximity.gif)
@@ -173,7 +189,122 @@ Set the hover proximity (in pixels) to control how close the cursor must be to a
The options under the **Graph styles** section let you control the general appearance of [additional fields](#additional-fields) in the visualization, excluding [color](#standard-options).
{{< docs/shared lookup="visualizations/graph-styles-options.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+1" >}}
<!-- prettier-ignore-start -->
| Option | Description |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [Style](#style) | Choose whether to display your time-series data as **Lines**, **Bars**, or **Points**. |
| [Line interpolation](#line-interpolation) | Choose how the graph interpolates the series line. |
| Line width | Set the thickness of the series lines or the outline for bars using the **Line width** slider. |
| Fill opacity | Set the series area fill color using the **Fill opacity** slider. |
| [Gradient mode](#gradient-mode) | Choose a gradient mode to control the gradient fill, which is based on the series color. |
| [Line style](#line-style) | Choose a solid, dashed, or dotted line style. |
| [Connect null values](#connect-null-values) | Choose how null values, which are gaps in the data, appear on the graph. |
| [Disconnect values](#disconnect-values) | Choose whether to set a threshold above which values in the data should be disconnected. |
| [Show points](#show-points) | Set whether to show data points to lines or bars. |
| Point size | Set the size of the points, from 1 to 40 pixels in diameter. |
| [Stack series](#stack-series) | Set whether Grafana displays series on top of each other. |
| [Bar alignment](#bar-alignment) | Set the position of the bar relative to a data point. |
| Bar width factor | Set the width of the bar relative to minimum space between data points. A factor of 0.5 means that the bars take up half of the available space between data points. A factor of 1.0 means that the bars take up all available space. |
<!-- prettier-ignore-end -->
#### Style
Choose whether to display your time-series data as **Lines**, **Bars**, or **Points**.
You can use overrides to combine multiple styles in the same graph. Choose from the following:
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-candle-style-v12.0.png" max-width="750px" alt="Graph styles" >}}
#### Line interpolation
Choose how the graph interpolates the series line:
- **Linear** - Points are joined by straight lines.
- **Smooth** - Points are joined by curved lines that smooths transitions between points.
- **Step before** - The line is displayed as steps between points. Points are rendered at the end of the step.
- **Step after** - The line is displayed as steps between points. Points are rendered at the beginning of the step.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-candle-interpolation-v12.0.png" max-width="750px" alt="Line interpolation styles" >}}
#### Gradient mode
Choose a gradient mode to control the gradient fill, which is based on the series color. To change the color, use the standard color scheme field option. For more information, refer to [Color scheme](ref:color-scheme).
- **None** - No gradient fill. This is the default setting.
- **Opacity** - An opacity gradient where the opacity of the fill increases as y-axis values increase.
- **Hue** - A subtle gradient that's based on the hue of the series color.
- **Scheme** - A color gradient defined by your [Color scheme](ref:color-scheme). This setting is used for the fill area and line. For more information about scheme, refer to [Scheme gradient mode](#scheme-gradient-mode).
Gradient appearance is influenced by the **Fill opacity** setting. The following image shows the **Fill opacity** set to 50.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-candle-gradient-v12.0.png" max-width="750px" alt="Gradient modes" >}}
##### Scheme gradient mode
In **Scheme** gradient mode, the line or bar receives a gradient color defined from the selected **Color scheme** option in the visualization's **Standard** options.
The following image shows a line chart with the **Green-Yellow-Red (by value)** color scheme option selected:
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-candle-scheme-grad-1-v12.0.png" max-width="600px" alt="Gradient color scheme" >}}
If the **Color scheme** is set to **From thresholds (by value)** and **Gradient mode** is set to **Scheme**, then the line or bar color changes as it crosses the defined thresholds:
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-candle-scheme-grad-2-v12.0.png" max-width="600px" alt="Gradient color scheme with thresholds" >}}
#### Line style
Choose a solid, dashed, or dotted line style:
- **Solid** - Display a solid line. This is the default setting.
- **Dash** - Display a dashed line. When you choose this option, a list appears for you to select the length and gap (length, gap) for the line dashes. Dash spacing is 10, 10 by default.
- **Dots** - Display dotted lines. When you choose this option, a list appears for you to select the gap (length = 0, gap) for the dot spacing. Dot spacing is 0, 10 by default.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-candle-line-style-v12.0.png" max-width="750px" alt="Line styles" >}}
{{< docs/shared lookup="visualizations/connect-null-values.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+1" >}}
{{< docs/shared lookup="visualizations/disconnect-values.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+1" >}}
To change the color, use the standard [color scheme](ref:color-scheme) field option.
#### Show points
Set whether to show data points as lines or bars. Choose from the following:
- **Auto** - Grafana determines a point's visibility based on the density of the data. If the density is low, then points appear.
- **Always** - Show the points regardless of how dense the dataset is.
- **Never** - Don't show points.
#### Stack series
Set whether Grafana stacks or displays series on top of each other. Be cautious when using stacking because it can create misleading graphs. To read more about why stacking might not be the best approach, refer to [The issue with stacking](https://www.data-to-viz.com/caveat/stacking.html). Choose from the following:
- **Off** - Turns off series stacking. When **Off**, all series share the same space in the visualization.
- **Normal** - Stacks series on top of each other.
- **100%** - Stack by percentage where all series add up to 100%.
##### Stack series in groups
The stacking group option is only available as an override. For more information about creating an override, refer to [Configure field overrides](ref:configure-field-overrides).
1. Edit the panel and click **Overrides**.
1. Create a field override for the **Stack series** option.
1. In stacking mode, click **Normal**.
1. Name the stacking group in which you want the series to appear.
The stacking group name option is only available when you create an override.
#### Bar alignment
Set the position of the bar relative to a data point. In the examples below, **Show points** is set to **Always** which makes it easier to see the difference this setting makes. The points don't change, but the bars change in relationship to the points. Choose from the following:
- **Before** ![Bar alignment before icon](/static/img/docs/time-series-panel/bar-alignment-before.png)
The bar is drawn before the point. The point is placed on the trailing corner of the bar.
- **Center** ![Bar alignment center icon](/static/img/docs/time-series-panel/bar-alignment-center.png)
The bar is drawn around the point. The point is placed in the center of the bar. This is the default.
- **After** ![Bar alignment after icon](/static/img/docs/time-series-panel/bar-alignment-after.png)
The bar is drawn after the point. The point is placed on the leading corner of the bar.
### Axis options
@@ -21,11 +21,6 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-data-links/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-data-links/
add-field-from-calculation-transform:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/transform-data/#add-field-from-calculation
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/transform-data/#add-field-from-calculation
---
# Canvas
@@ -18,11 +18,6 @@ labels:
title: Histogram
weight: 100
refs:
standard-calculations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/calculation-types/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types/
color-scheme:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme
@@ -15,17 +15,6 @@ labels:
- oss
title: State timeline
weight: 100
refs:
color-scheme:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#color-scheme
value-mappings:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-value-mappings/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-value-mappings/
---
# State timeline
@@ -15,17 +15,6 @@ labels:
- oss
title: Status history
weight: 100
refs:
color-scheme:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#color-scheme
value-mappings:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-value-mappings/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-value-mappings/
---
# Status history
@@ -44,11 +44,6 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme
- pattern: /docs/grafana-cloud
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#color-scheme
configuration-file:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#configuration-file-location
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#configuration-file-location
field-override:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/
@@ -18,7 +18,7 @@ description: Configure options for Grafana's text visualization
title: Text
weight: 100
refs:
disable_sanitize_html:
disable-sanitize-html:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#disable_sanitize_html
- pattern: /docs/grafana-cloud/
@@ -63,7 +63,7 @@ Use the following options to refine your text visualization.
| Option | Description |
| ------ | ----------- |
| Mode | Determines how embedded content appears. Choose from:<ul><li>**Markdown** - Formats the content as [markdown](https://en.wikipedia.org/wiki/Markdown).</li><li>**HTML** - Renders the content as [sanitized](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/text/sanitize.ts) HTML. If you require more direct control over the output, you can set the [disable_sanitize_html](ref:disable_sanitize_html) flag which enables you to directly enter HTML.</li><li>**Code** - Renders content inside a read-only code editor. [Variables](ref:variables) in the content are expanded for display.</li></ul><p>To allow embedding of iframes and other websites, you need set `allow_embedding = true` in your Grafana `config.ini` or environment variables (depending on your employment).</p> |
| Mode | Determines how embedded content appears. Choose from:<ul><li>**Markdown** - Formats the content as [markdown](https://en.wikipedia.org/wiki/Markdown).</li><li>**HTML** - Renders the content as [sanitized](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/text/sanitize.ts) HTML. If you require more direct control over the output, you can set the [disable_sanitize_html](ref:disable-sanitize-html) flag which enables you to directly enter HTML.</li><li>**Code** - Renders content inside a read-only code editor. [Variables](ref:variables) in the content are expanded for display.</li></ul><p>To allow embedding of iframes and other websites, you need set `allow_embedding = true` in your Grafana `config.ini` or environment variables (depending on your employment).</p> |
| Language | When you choose **Code** as your text mode, select an appropriate language to apply syntax highlighting to the embedded text. |
| Show line numbers | Displays line numbers in the panel preview when you choose **Code** as your text mode. |
| Show mini map | Displays a small outline of the embedded text in the panel preview when you choose **Code** as your text mode. |
@@ -34,37 +34,12 @@ title: Time series
menuTitle: Time series
weight: 10
refs:
configure-standard-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#max
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#max
color-scheme:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#color-scheme
add-a-field-override:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/#add-a-field-override
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/#add-a-field-override
configure-field-overrides:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/
link-alert:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-grafana-managed-rule/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule/
panel-editor-alerts:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/panel-editor-overview/#data-section
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-editor-overview/#data-section
data-transformation:
panel-data-section:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/panel-editor-overview/#data-section
- pattern: /docs/grafana-cloud/
@@ -75,7 +50,7 @@ refs:
Time series visualizations are the default way to show the variations of a set of data values over time. Each data point is matched to a timestamp and this _time series_ is displayed as a graph. The visualization can render series as lines, points, or bars and it's versatile enough to display almost any type of [time-series data](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/fundamentals/timeseries/).
{{< figure src="/static/img/docs/time-series-panel/time_series_small_example.png" max-width="1200px" alt="Time series" >}}
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-time-series-v12.0.png" max-width="750px" alt="Time series visualization" >}}
{{< admonition type="note" >}}
You can migrate from the legacy Graph visualization to the time series visualization. To migrate, open the panel and click the **Migrate** button in the side pane.
@@ -116,7 +91,7 @@ In the following example, there are three numeric fields represented by three li
![Time series line chart with multiple numeric fields](/media/docs/grafana/panels-visualizations/screenshot-grafana-11.1-timeseries-example1v2.png 'Time series line chart with multiple numeric fields')
If the time field isn't automatically detected, you might need to convert the data to a time format using a [data transformation](ref:data-transformation).
If the time field isn't automatically detected, you might need to convert the data to a time format using a [data transformation](ref:panel-data-section).
### Example 2
@@ -168,7 +143,7 @@ When you add the offset, the resulting visualization makes the datasets appear t
## Alert rules
You can [link alert rules](ref:link-alert) to time series visualizations in the form of annotations to observe when alerts fire and are resolved. In addition, you can create alert rules from the **Alert** tab within the [panel editor](ref:panel-editor-alerts).
You can [link alert rules](ref:link-alert) to time series visualizations in the form of annotations to observe when alerts fire and are resolved. In addition, you can create alert rules from the **Alert** tab within the [panel editor](ref:panel-data-section).
## Special overrides
@@ -187,7 +162,7 @@ The **Graph styles > Fill below to** [override property](#field-overrides) fills
The following example shows three series: Min, Max, and Value. The Min and Max series have **Line width** set to 0. Max has a **Fill below to** override set to Min, which fills the area between Max and Min with the Max line color.
{{< figure src="/static/img/docs/time-series-panel/fill-below-to-7-4.png" max-width="600px" alt="Fill below to example" >}}
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-fill-below-to-v12.0.png" max-width="600px" alt="Fill below to example" >}}
{{< docs/shared lookup="visualizations/multiple-y-axes.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+2" >}}
@@ -16,40 +16,16 @@ description: Configure options for Grafana's xy chart
title: XY chart
weight: 100
refs:
panel-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-panel-options/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-panel-options/
data-links:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-data-links/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-data-links/
configure-standard-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#max
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#max
standard-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/
color-scheme:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#color-scheme
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#color-scheme
add-a-field-override:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/#add-a-field-override
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/#add-a-field-override
configure-field-overrides:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/
@@ -45,7 +45,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes |
| `formatString` | Enable format string transformer | Yes |
| `kubernetesClientDashboardsFolders` | Route the folder and dashboard service requests to k8s | Yes |
| `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes |
| `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes |
| `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes |
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes |
@@ -163,7 +163,7 @@ Complete the following steps to install Grafana using the standalone binaries:
1. Use the binary to manually start the Grafana server:
```shell
/usr/local/grafana/bin/grafana-server --homepath /usr/local/grafana
/usr/local/grafana/bin/grafana server --homepath /usr/local/grafana
```
{{< admonition type="note" >}}
@@ -33,7 +33,7 @@ Select the placement of the y-axis. Choose from the following:
- **Auto** - Automatically assigns the y-axis to the series. When there are two or more series with different units, Grafana assigns the left axis to the first unit and the right axis to the units that follow.
- **Left** - Display all y-axes on the left side.
- **Right** - Display all y-axes on the right side.
- **Hidden** - Hide all axes. To selectively hide axes, [Add a field override](ref:add-a-field-override) that targets specific fields.
- **Hidden** - Hide all axes. To selectively hide axes, [Add a field override](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/#add-a-field-override) that targets specific fields.
#### Scale
@@ -49,6 +49,6 @@ Set a **Soft min** or **soft max** option for better control of y-axis limits. B
**Soft min** and **soft max** settings can prevent small variations in the data from being magnified when it's mostly flat. In contrast, hard min and max values help prevent obscuring useful detail in the data by clipping intermittent spikes past a specific point.
To define hard limits of the y-axis, set standard min/max options. For more information, refer to [Configure standard options](ref:configure-standard-options).
To define hard limits of the y-axis, set standard min/max options. For more information, refer to [Configure standard options](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#max).
![Label example](/static/img/docs/time-series-panel/axis-soft-min-max-7-4.png)
![Label example](/media/docs/grafana/panels-visualizations/screenshot-soft-min-max-v12.0.png)
@@ -32,7 +32,7 @@ Select the placement of the y-axis. Choose from the following:
- **Auto** - Automatically assigns the y-axis to the series. When there are two or more series with different units, Grafana assigns the left axis to the first unit and the right axis to the units that follow.
- **Left** - Display all y-axes on the left side.
- **Right** - Display all y-axes on the right side.
- **Hidden** - Hide all axes. To selectively hide axes, [Add a field override](ref:add-a-field-override) that targets specific fields.
- **Hidden** - Hide all axes. To selectively hide axes, [Add a field override](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/#add-a-field-override) that targets specific fields.
#### Scale
@@ -50,8 +50,7 @@ Set a **Soft min** or **soft max** option for better control of y-axis limits. B
**Soft min** and **soft max** settings can prevent small variations in the data from being magnified when it's mostly flat. In contrast, hard min and max values help prevent obscuring useful detail in the data by clipping intermittent spikes past a specific point.
To define hard limits of the y-axis, set standard min/max options. For more information, refer to [Configure standard options](ref:configure-standard-options).
To define hard limits of the y-axis, set standard min/max options. For more information, refer to [Configure standard options](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#max).
The following examples shows how this option works in a time series visualization:
<!--
![Label example](/static/img/docs/time-series-panel/axis-soft-min-max-7-4.png) -->
![Label example](/media/docs/grafana/panels-visualizations/screenshot-soft-min-max-v12.0.png)
@@ -4,4 +4,4 @@ title: Stack series link
## Axis
For full instructions, refer to [Change axis display](/docs/grafana/<GRAFANA_VERSION>/visualizations/time-series/change-axis-display/).
For full instructions, refer to [Change axis display](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/visualizations/time-series/change-axis-display/).
@@ -1,7 +1,7 @@
---
title: Configuration options intro text
comments: |
This file is used in the following in all visualizations except: alert list, annotiations list, logs, news, text
This file is used in the following in all visualizations except: alert list, annotations list, logs, news, text
---
The following section describes the configuration options available in the panel editor pane for this visualization. These options are, as much as possible, ordered as they appear in Grafana.
@@ -8,7 +8,7 @@ comments: |
Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel.
You can create links that include the series name or even the value under the cursor.
To learn more, refer to [Configure data links and actions](../../configure-data-links/).
To learn more, refer to [Configure data links and actions](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-data-links/).
{{< admonition type="note" >}}
Actions are not supported for this visualization.
@@ -33,4 +33,4 @@ For each action, define the following API call settings:
<!-- prettier-ignore-end -->
To learn more, refer to [Configure data links and actions](../../configure-data-links/).
To learn more, refer to [Configure data links and actions](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-data-links/).
@@ -35,4 +35,4 @@ For each action, define the following API call settings:
<!-- prettier-ignore-end -->
To learn more, refer to [Configure data links and actions](../../configure-data-links/).
To learn more, refer to [Configure data links and actions](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-data-links/).
@@ -19,4 +19,4 @@ For each data link, set the following options:
- **URL**
- **Open in new tab**
To learn more, refer to [Configure data links and actions](../../configure-data-links/).
To learn more, refer to [Configure data links and actions](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-data-links/).
@@ -1,34 +1,34 @@
---
title: Graph styles options
comments: |
This file is used in the following visualizations: candlestick, time series.
This file is used in the following visualizations: time series.
---
<!-- prettier-start-ignore -->
<!-- prettier-ignore-start -->
| Option | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Style](#style) | Choose whether to display your time-series data as **Lines**, **Bars**, or **Points**. |
| [Line interpolation](#line-interpolation) | Choose how the graph interpolates the series line. |
| Line width | Set the thickness of the series lines or the outline for bars using the **Line width** slider. |
| [Fill opacity](#fill-opacity) | Set the series area fill color using the **Fill opacity** slider. |
| [Gradient mode](#gradient-mode) | Choose a gradient mode to control the gradient fill, which is based on the series color. |
| [Line style](#line-style) | Choose a solid, dashed, or dotted line style. |
| [Connect null values](#connect-null-values) | Choose how null values, which are gaps in the data, appear on the graph. |
| [Disconnect values](#disconnect-values) | Choose whether to set a threshold above which values in the data should be disconnected. |
| [Show points](#show-points) | Set whether to show data points to lines or bars. |
| Point size | Set the size of the points, from 1 to 40 pixels in diameter. |
| [Stack series](#stack-series) | Set whether Grafana displays series on top of each other. |
| [Bar alignment](#bar-alignment) | Set the position of the bar relative to a data point. |
| Option | Description |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [Style](#style) | Choose whether to display your time-series data as **Lines**, **Bars**, or **Points**. |
| [Line interpolation](#line-interpolation) | Choose how the graph interpolates the series line. |
| Line width | Set the thickness of the series lines or the outline for bars using the **Line width** slider. |
| Fill opacity | Set the series area fill color using the **Fill opacity** slider. |
| [Gradient mode](#gradient-mode) | Choose a gradient mode to control the gradient fill, which is based on the series color. |
| [Line style](#line-style) | Choose a solid, dashed, or dotted line style. |
| [Connect null values](#connect-null-values) | Choose how null values, which are gaps in the data, appear on the graph. |
| [Disconnect values](#disconnect-values) | Choose whether to set a threshold above which values in the data should be disconnected. |
| [Show points](#show-points) | Set whether to show data points to lines or bars. |
| Point size | Set the size of the points, from 1 to 40 pixels in diameter. |
| [Stack series](#stack-series) | Set whether Grafana displays series on top of each other. |
| [Bar alignment](#bar-alignment) | Set the position of the bar relative to a data point. |
| Bar width factor | Set the width of the bar relative to minimum space between data points. A factor of 0.5 means that the bars take up half of the available space between data points. A factor of 1.0 means that the bars take up all available space. |
<!-- prettier-end-ignore -->
<!-- prettier-ignore-end -->
#### Style
Choose whether to display your time-series data as **Lines**, **Bars**, or **Points**. You can use overrides to combine multiple styles in the same graph. Choose from the following:
![Style modes](/static/img/docs/time-series-panel/style-modes-v9.png)
![Graph style examples](/media/docs/grafana/panels-visualizations/screenshot-time-style-v12.0.png)
#### Line interpolation
@@ -39,44 +39,32 @@ Choose how the graph interpolates the series line:
- **Step before** - The line is displayed as steps between points. Points are rendered at the end of the step.
- **Step after** - The line is displayed as steps between points. Points are rendered at the beginning of the step.
#### Line width
Set the thickness of the series lines or the outline for bars using the **Line width** slider.
#### Fill opacity
Set the series area fill color using the **Fill opacity** slider.
![Fill opacity examples](/static/img/docs/time-series-panel/fill-opacity.png)
![Line interpolation examples](/media/docs/grafana/panels-visualizations/screenshot-time-interpolation-v12.0.png)
#### Gradient mode
Choose a gradient mode to control the gradient fill, which is based on the series color. To change the color, use the standard color scheme field option. For more information, refer to [Color scheme](ref:color-scheme).
Choose a gradient mode to control the gradient fill, which is based on the series color. To change the color, use the standard color scheme field option. For more information, refer to [Color scheme](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme).
- **None** - No gradient fill. This is the default setting.
- **Opacity** - An opacity gradient where the opacity of the fill increases as y-axis values increase.
- **Hue** - A subtle gradient that's based on the hue of the series color.
- **Scheme** - A color gradient defined by your [Color scheme](ref:color-scheme). This setting is used for the fill area and line. For more information about scheme, refer to [Scheme gradient mode](#scheme-gradient-mode).
- **Scheme** - A color gradient defined by your [Color scheme](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme). This setting is used for the fill area and line. For more information about scheme, refer to [Scheme gradient mode](#scheme-gradient-mode).
Gradient appearance is influenced by the **Fill opacity** setting. The following image shows the **Fill opacity** set to 50.
![Gradient mode examples](/static/img/docs/time-series-panel/gradient-modes-v9.png)
![Gradient mode examples](/media/docs/grafana/panels-visualizations/screenshot-time-gradient-v12.0.png)
##### Scheme gradient mode
The **Gradient mode** option located under the **Graph styles** section has a mode called **Scheme**. When you enable **Scheme**, the line or bar receives a gradient color defined from the selected **Color scheme**.
In **Scheme** gradient mode, the line or bar receives a gradient color defined from the selected **Color scheme** option in the visualization's **Standard** options.
###### From thresholds
The following image shows a line chart with the **Green-Yellow-Red (by value)** color scheme option selected:
If the **Color scheme** is set to **From thresholds (by value)** and **Gradient mode** is set to **Scheme**, then the line or bar color changes as it crosses the defined thresholds.
{{< figure src="/static/img/docs/time-series-panel/gradient_mode_scheme_line.png" max-width="600px" alt="Color scheme: Green-Yellow-Red" >}}
{{< figure src="/static/img/docs/time-series-panel/gradient_mode_scheme_thresholds_line.png" max-width="1200px" alt="Colors scheme: From thresholds" >}}
If the **Color scheme** is set to **From thresholds (by value)** and **Gradient mode** is set to **Scheme**, then the line or bar color changes as it crosses the defined thresholds:
###### Gradient color schemes
The following image shows a line chart with the **Green-Yellow-Red (by value)** color scheme option selected.
{{< figure src="/static/img/docs/time-series-panel/gradient_mode_scheme_line.png" max-width="1200px" alt="Color scheme: Green-Yellow-Red" >}}
{{< figure src="/static/img/docs/time-series-panel/gradient_mode_scheme_thresholds_line.png" max-width="600px" alt="Colors scheme: From thresholds" >}}
#### Line style
@@ -86,13 +74,13 @@ Choose a solid, dashed, or dotted line style:
- **Dash** - Display a dashed line. When you choose this option, a list appears for you to select the length and gap (length, gap) for the line dashes. Dash spacing is 10, 10 by default.
- **Dots** - Display dotted lines. When you choose this option, a list appears for you to select the gap (length = 0, gap) for the dot spacing. Dot spacing is 0, 10 by default.
![Line styles examples](/static/img/docs/time-series-panel/line-styles-examples-v9.png)
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-time-line-style-v12.0.png" max-width="550px" alt="Line style examples" >}}
{{< docs/shared lookup="visualizations/connect-null-values.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+1" >}}
{{< docs/shared lookup="visualizations/disconnect-values.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+1" >}}
To change the color, use the standard [color scheme](ref:color-scheme) field option.
To change the color, use the standard [color scheme](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme) field option.
#### Show points
@@ -112,7 +100,7 @@ Set whether Grafana stacks or displays series on top of each other. Be cautious
##### Stack series in groups
The stacking group option is only available as an override. For more information about creating an override, refer to [Configure field overrides](ref:configure-field-overrides).
The stacking group option is only available as an override. For more information about creating an override, refer to [Configure field overrides](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/).
1. Edit the panel and click **Overrides**.
1. Create a field override for the **Stack series** option.
@@ -6,12 +6,12 @@ comments: |
This file is used in the following visualizations: bar chart, candlestick, histogram, time series, trend, xy chart
---
Legend options control the series names and statistics that appear under or to the right of the graph. For more information about the legend, refer to [Configure a legend](../configure-legend/).
Legend options control the series names and statistics that appear under or to the right of the graph. For more information about the legend, refer to [Configure a legend](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-legend/).
| Option | Description |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Visibility | Toggle the switch to turn the legend on or off. |
| Mode | Use these settings to define how the legend appears in your visualization. **List** displays the legend as a list. This is a default display mode of the legend. **Table** displays the legend as a table. |
| Placement | Choose where to display the legend. **Bottom** places the legend below the graph. **Right** places the legend to the right of the graph. |
| Width | Control how wide the legend is when placed on the right side of the visualization. This option is only displayed if you set the legend placement to **Right**. |
| Values | Choose which of the [standard calculations](../../query-transform-data/calculation-types/) to show in the legend. You can have more than one. |
| Option | Description |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Visibility | Toggle the switch to turn the legend on or off. |
| Mode | Use these settings to define how the legend appears in your visualization. **List** displays the legend as a list. This is a default display mode of the legend. **Table** displays the legend as a table. |
| Placement | Choose where to display the legend. **Bottom** places the legend below the graph. **Right** places the legend to the right of the graph. |
| Width | Control how wide the legend is when placed on the right side of the visualization. This option is only displayed if you set the legend placement to **Right**. |
| Values | Choose which of the [standard calculations](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/calculation-types/) to show in the legend. You can have more than one. |
@@ -6,9 +6,9 @@ comments: |
This file is used in the following visualizations: state timeline, status history
---
When the legend option is enabled it can show either the value mappings or the threshold brackets. To show the value mappings in the legend, it's important that the **Color scheme** as referenced in [Color scheme](../../configure-standard-options#color-scheme) is set to **Single color** or **Classic palette**. To see the threshold brackets in the legend set the **Color scheme** to **From thresholds**.
When the legend option is enabled it can show either the value mappings or the threshold brackets. To show the value mappings in the legend, it's important that the **Color scheme** as referenced in [Color scheme](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/#color-scheme) is set to **Single color** or **Classic palette**. To see the threshold brackets in the legend set the **Color scheme** to **From thresholds**.
For more information about the legend, refer to [Configure a legend](../configure-legend/).
For more information about the legend, refer to [Configure a legend](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-legend/).
### Visibility
@@ -6,4 +6,4 @@ title: Multiple y-axes
In some cases, you might want to display multiple y-axes. For example, if you have a dataset showing both temperature and humidity over time, you might want to show two y-axes with different units for the two series.
You can configure multiple y-axes and control where they're displayed in the visualization by adding field overrides. [This example of a dataset that includes temperature and humidity](../../configure-overrides/#example-2-format-temperature-and-humidity) describes how you can configure that. Repeat the steps for every y-axis you wish to display.
You can configure multiple y-axes and control where they're displayed in the visualization by adding field overrides. [This example of a dataset that includes temperature and humidity](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/#example-2-format-temperature-and-humidity) describes how you can configure that. Repeat the steps for every y-axis you wish to display.
@@ -16,4 +16,4 @@ Choose from the following override options:
| Fields returned by query | Select all fields returned by a specific query, such as A, B, or C. |
| Fields with values | Select all fields returned by your defined reducer condition, such as **Min**, **Max**, **Count**, **Total**. |
To learn more, refer to [Configure field overrides](../../configure-overrides/).
To learn more, refer to [Configure field overrides](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/).
@@ -4,4 +4,4 @@ comments: |
This file is used in all visualizations pages
---
In the **Panel options** section of the panel editor pane, set basic options like panel title and description, as well as panel links. To learn more, refer to [Configure panel options](../../configure-panel-options/).
In the **Panel options** section of the panel editor pane, set basic options like panel title and description, as well as panel links. To learn more, refer to [Configure panel options](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-panel-options/).
@@ -4,4 +4,4 @@ title: Stack series link
### Stack series
For full instructions, refer to [Graph stacked time series](/docs/grafana/<GRAFANA_VERSION>/visualizations/time-series/graph-time-series-stacking/).
For full instructions, refer to [Graph stacked time series](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/visualizations/time-series/graph-time-series-stacking/).
@@ -4,7 +4,7 @@ comments: |
This file is used in the following visualizations: bar chart, bar gauge, candlestick, canvas, gauge, geomap, histogram, pie chart, stat, state timeline, status history, table, time series, trend
---
**Standard options** in the panel editor pane let you change how field data is displayed in your visualizations. When you set a standard option, the change is applied to all fields or series. For more granular control over the display of fields, refer to [Configure overrides](../../configure-overrides/).
**Standard options** in the panel editor pane let you change how field data is displayed in your visualizations. When you set a standard option, the change is applied to all fields or series. For more granular control over the display of fields, refer to [Configure overrides](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/).
| Option | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -16,4 +16,4 @@ comments: |
| Color scheme | Set single or multiple colors for your entire visualization. |
| No value | Enter what Grafana should display if the field value is empty or null. The default value is a hyphen (-). |
To learn more, refer to [Configure standard options](../../configure-standard-options/).
To learn more, refer to [Configure standard options](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/).
@@ -16,4 +16,4 @@ For each threshold, set the following options:
| Thresholds mode | Choose from **Absolute** and **Percentage**. |
| Show thresholds | Choose from a variety of display options including not displaying thresholds at all. |
To learn more, refer to [Configure thresholds](../../configure-thresholds/).
To learn more, refer to [Configure thresholds](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-thresholds/).
@@ -15,4 +15,4 @@ For each threshold, set the following options:
| Value | Set the value for each threshold. |
| Thresholds mode | Choose from **Absolute** and **Percentage**. |
To learn more, refer to [Configure thresholds](../../configure-thresholds/).
To learn more, refer to [Configure thresholds](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-thresholds/).
@@ -38,5 +38,6 @@ When you set the **Tooltip mode** to **All**, the **Values sort order** option i
### Hover proximity
Set the hover proximity (in pixels) to control how close the cursor must be to a data point to trigger the tooltip to display.
The following screen recording shows this option in a time series visualization:
![Adding a hover proximity limit for tooltips](/media/docs/grafana/gif-grafana-10-4-hover-proximity.gif)
@@ -17,4 +17,4 @@ For each value mapping, set the following options:
- **Color** (Optional)
- **Icon** (Canvas only)
To learn more, refer to [Configure value mappings](../../configure-value-mappings/).
To learn more, refer to [Configure value mappings](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-value-mappings/).
+1 -1
View File
@@ -91,7 +91,7 @@ const config = {
},
{
url: '${HOST}/org/teams',
threshold: 0,
threshold: 1,
},
{
url: '${HOST}/plugins',
+1 -1
View File
@@ -382,7 +382,7 @@ require (
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/googleapis v1.4.1 // indirect
+2 -2
View File
@@ -1358,8 +1358,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
+8
View File
@@ -754,6 +754,7 @@ github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U=
github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc=
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ=
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI=
@@ -885,6 +886,7 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpV
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
@@ -1335,6 +1337,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeU
github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q=
github.com/grafana/grafana-plugin-sdk-go v0.269.1/go.mod h1:yv2KbO4mlr9WuDK2f+2gHAMTwwLmLuqaEnrPXTRU+OI=
github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c=
github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagIr8sWr1RT9h7ufJl5NMcWJAU=
github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA=
github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c=
github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:q+h3HbmqU/PposW6lq8cMle1v8vuyX1LCMrGzbabHxc=
@@ -1366,6 +1369,8 @@ github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e7
github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:xrKQcxQxz+IUF90ybtfENFeEXtlj9nAsX/3Fw0KEIeQ=
github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed h1:59JF1WhHLT+lNX89Tm1OzOEySMVMASAhaPbsRjtp8Kc=
github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed/go.mod h1:OIAAKNgG5fpuJQRNO1lUSj9nc18Xl3O7M8fjIlBO1cI=
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5 h1:MAQ2B0cu0V1S91ZjVa7NomNZFjaR2SmdtvdwhqBtyhU=
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 h1:6D2gGAwyQBElSrp3E+9lSr7k8gLuP3Aiy20rweLWeBw=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3/go.mod h1:YeND+6FDA7OuFgDzYODN8kfPhXLCehcpxe4T9mdnpCY=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 h1:4/BZkGObFWZf4cLbE2Vqg/1VTz67Q0AJ7LHspWLKJoQ=
@@ -1380,6 +1385,7 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
@@ -2329,6 +2335,7 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0/go.mod h1:uosvgpqTcTXtcPQORTbEkZNDQTCDOgTz1fe6aLSyqrQ=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
@@ -2341,6 +2348,7 @@ go.opentelemetry.io/contrib/propagators/b3 v1.27.0 h1:IjgxbomVrV9za6bRi8fWCNXENs
go.opentelemetry.io/contrib/propagators/b3 v1.27.0/go.mod h1:Dv9obQz25lCisDvvs4dy28UPh974CxkahRDUPsY7y9E=
go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ=
go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg=
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A=
go.opentelemetry.io/contrib/zpages v0.60.0 h1:wOM9ie1Hz4H88L9KE6GrGbKJhfm+8F1NfW/Y3q9Xt+8=
@@ -423,6 +423,35 @@ describe('field convert types transformer', () => {
]);
});
it('will support custom join separators for field type string, correctly handling undefined and null', () => {
const options = {
conversions: [{ targetField: 'mixed_values', destinationType: FieldType.string, joinWith: '&' }],
};
const mixedValues = toDataFrame({
fields: [
{
name: 'mixed_values',
type: FieldType.string,
values: [['a', 'b', 'c'], ['d', undefined, 'f'], undefined, 'regular string', null],
},
],
});
const stringified = convertFieldTypes(options, [mixedValues]);
expect(
stringified[0].fields.map(({ type, values }) => ({
type,
values,
}))
).toEqual([
{
type: FieldType.string,
values: ['a&b&c', 'd&&f', undefined, '"regular string"', 'null'],
},
]);
});
it('will convert time fields to strings', () => {
const options = {
conversions: [{ targetField: 'time', destinationType: FieldType.string, dateFormat: 'YYYY-MM' }],
@@ -218,7 +218,7 @@ export function fieldToStringField(
if (joinWith?.length && Array.isArray(v)) {
return v.join(joinWith);
}
return field.type === FieldType.other ? JSON.stringify(v) : `${v}`;
return JSON.stringify(v);
});
break;
@@ -46,7 +46,7 @@ describe('GroupToSubframe transformer', () => {
values: ['one', 'two', 'three'],
},
{
name: 'Nested frames',
name: '__nestedFrames',
type: FieldType.nestedFrames,
config: {},
values: [
@@ -153,7 +153,7 @@ describe('GroupToSubframe transformer', () => {
},
{
config: {},
name: 'Nested frames',
name: '__nestedFrames',
type: FieldType.nestedFrames,
values: [
[
@@ -124,7 +124,7 @@ export const groupToNestedTable: DataTransformerInfo<GroupToNestedTableTransform
fields.push({
config: {},
name: 'Nested frames',
name: '__nestedFrames',
type: FieldType.nestedFrames,
values: subFrames,
});
@@ -107,10 +107,6 @@ export interface FeatureToggles {
*/
lokiQuerySplitting?: boolean;
/**
* Give users the option to configure split durations for Loki queries
*/
lokiQuerySplittingConfig?: boolean;
/**
* Support overriding cookie preferences per user
*/
individualCookiePreferences?: boolean;
@@ -166,10 +162,6 @@ export interface FeatureToggles {
*/
extraThemes?: boolean;
/**
* Adds predefined query operations to Loki query editor
*/
lokiPredefinedOperations?: boolean;
/**
* Enables the plugins frontend sandbox
*/
pluginsFrontendSandbox?: boolean;
@@ -344,11 +336,6 @@ export interface FeatureToggles {
*/
cloudWatchBatchQueries?: boolean;
/**
* Enables the loki data source to request structured metadata from the Loki server
* @default true
*/
lokiStructuredMetadata?: boolean;
/**
* If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.
*/
cachingOptimizeSerializationMemoryUsage?: boolean;
@@ -1031,4 +1018,9 @@ export interface FeatureToggles {
* @default false
*/
newInfluxDSConfigPageDesign?: boolean;
/**
* Set this to true to enable all app chrome extensions registered by plugins.
* @default false
*/
enableAppChromeExtensions?: boolean;
}
@@ -194,6 +194,7 @@ export enum PluginExtensionPoints {
QueryEditorRowAdaptiveTelemetryV1 = 'grafana/query-editor-row/adaptivetelemetry/v1',
TraceViewResourceAttributes = 'grafana/traceview/resource-attributes',
LogsViewResourceAttributes = 'grafana/logsview/resource-attributes',
AppChrome = 'grafana/app/chrome/v1',
}
export type PluginExtensionPanelContext = {
@@ -71,9 +71,9 @@ async function loadGroupByLabels(
}
const expr = modeller.renderLabels(labels);
const result = await datasource.languageProvider.queryLabelKeys(timeRange, expr);
const result: string[] = await datasource.languageProvider.queryLabelKeys(timeRange, expr);
return Object.keys(result).map((x) => ({
return result.map((x) => ({
label: x,
value: x,
}));
@@ -948,6 +948,25 @@ describe('buildVisualQueryFromString', () => {
})
);
});
it('parses query with custom variable', () => {
expect(buildVisualQueryFromString('topk($custom, rate(metric_name[$__rate_interval]))')).toEqual(
noErrors({
metric: 'metric_name',
labels: [],
operations: [
{
id: 'rate',
params: ['$__rate_interval'],
},
{
id: 'topk',
params: ['$custom'],
},
],
})
);
});
});
function noErrors(query: PromVisualQuery) {
@@ -49,9 +49,9 @@ import { PromVisualQuery, PromVisualQueryBinary } from './types';
* It traverses the tree and uses sort of state machine to update the query model.
* The query model is modified during the traversal and sent to each handler as context.
*/
export function buildVisualQueryFromString(expr: string): Context {
export function buildVisualQueryFromString(expr: string): Omit<Context, 'replacements'> {
expr = replaceBuiltInVariable(expr);
const replacedExpr = replaceVariables(expr);
const { replacedExpr, replacedVariables } = replaceVariables(expr);
const tree = parser.parse(replacedExpr);
const node = tree.topNode;
@@ -64,6 +64,7 @@ export function buildVisualQueryFromString(expr: string): Context {
const context: Context = {
query: visQuery,
errors: [],
replacements: replacedVariables,
};
try {
@@ -83,6 +84,9 @@ export function buildVisualQueryFromString(expr: string): Context {
context.errors = [];
}
// No need to return replaced variables
delete context.replacements;
return context;
}
@@ -96,6 +100,7 @@ interface ParsingError {
interface Context {
query: PromVisualQuery;
errors: ParsingError[];
replacements?: Record<string, string>;
}
/**
@@ -359,6 +364,19 @@ function updateFunctionArgs(expr: string, node: SyntaxNode | null, context: Cont
break;
}
case VectorSelector: {
// When we replace a custom variable to prevent errors during parsing we receive VectorSelector and Identifier in it.
// But this is also a normal case for a normal function body. i.e. topk(5, http_requests_total{})
// In such cases we got identifier as http_requests_total. So we shouldn't push this as param.
// So we check whether the given VectorSelector is something we replaced earlier.
if (context.replacements?.[expr.substring(node.from, node.to)]) {
const identifierNode = node.getChild(Identifier);
const customVarName = getString(expr, identifierNode);
op.params.push(customVarName);
break;
}
}
default: {
// Means we get to something that does not seem like simple function arg and is probably nested query so jump
// back to main context
@@ -429,6 +447,7 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) {
handleExpression(expr, right, {
query: binQuery.query,
errors: context.errors,
replacements: context.replacements,
});
}
}
@@ -20,9 +20,15 @@ describe('getLeftMostChild', () => {
describe('replaceVariables', () => {
it('should replace variables', () => {
expect(replaceVariables('sum_over_time([[metric_var]]{bar="${app}"}[$__interval])')).toBe(
'sum_over_time(__V_1__metric_var__V__{bar="__V_2__app__V__"}[__V_0____interval__V__])'
const { replacedExpr, replacedVariables } = replaceVariables(
'sum_over_time([[metric_var]]{bar="${app}"}[$__interval])'
);
expect(replacedExpr).toBe('sum_over_time(__V_1__metric_var__V__{bar="__V_2__app__V__"}[__V_0____interval__V__])');
expect(replacedVariables).toEqual({
__V_1__metric_var__V__: '[[metric_var]]',
__V_2__app__V__: '${app}',
__V_0____interval__V__: '$__interval',
});
});
});
@@ -42,9 +48,14 @@ describe('getString', () => {
it('is symmetrical with replaceVariables', () => {
const expr = 'sum_over_time([[metric_var]]{bar="${app}"}[$__interval])';
const replaced = replaceVariables(expr);
const tree = parser.parse(replaced);
expect(getString(replaced, tree.topNode)).toBe(expr);
const { replacedExpr, replacedVariables } = replaceVariables(expr);
const tree = parser.parse(replacedExpr);
expect(getString(replacedExpr, tree.topNode)).toBe(expr);
expect(replacedVariables).toEqual({
__V_1__metric_var__V__: '[[metric_var]]',
__V_2__app__V__: '${app}',
__V_0____interval__V__: '$__interval',
});
});
});
@@ -36,7 +36,8 @@ const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\
* parsable and at the same time we can get the variable and its format back from it.
*/
export function replaceVariables(expr: string) {
return expr.replace(variableRegex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
const replacedVariables: Record<string, string> = {};
const replacedExpr = expr.replace(variableRegex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
const fmt = fmt2 || fmt3;
let variable = var1;
let varType = '0';
@@ -51,8 +52,12 @@ export function replaceVariables(expr: string) {
varType = '2';
}
return `__V_${varType}__` + variable + '__V__' + (fmt ? '__F__' + fmt + '__F__' : '');
const replacement = `__V_${varType}__` + variable + '__V__' + (fmt ? '__F__' + fmt + '__F__' : '');
replacedVariables[replacement] = match;
return replacement;
});
return { replacedExpr, replacedVariables };
}
const varTypeFunc = [
+3 -1
View File
@@ -49,7 +49,9 @@ if (process.env.NODE_ENV === 'development') {
* See https://github.com/mswjs/msw-storybook-addon#configuring-msw
* to learn how to customize it
*/
initialize();
initialize({
onUnhandledRequest: 'bypass',
});
const preview: Preview = {
decorators: [withTheme(handleThemeChange), withTimeZone()],
+1 -1
View File
@@ -107,7 +107,7 @@
"react-calendar": "^5.1.0",
"react-colorful": "5.6.1",
"react-custom-scrollbars-2": "4.5.0",
"react-data-grid": "grafana/react-data-grid#3420f7f2a9e0d707d3313ec5b143a6be53f720b5",
"react-data-grid": "grafana/react-data-grid#de920f0105cb2b7d774444e7443a675f3b568ad6",
"react-dropzone": "14.3.8",
"react-highlight-words": "0.21.0",
"react-hook-form": "^7.49.2",
@@ -688,7 +688,7 @@ function getValueStyles(
const styles: CSSProperties = {
color,
height: `${height}px`,
width: `${width}px`,
maxWidth: `${width}px`,
display: 'flex',
alignItems: 'center',
textWrap: 'nowrap',
@@ -3,7 +3,8 @@ import userEvent from '@testing-library/user-event';
import { Field, FieldType, LinkModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { TableCellDisplayMode } from '@grafana/schema';
import { TableCellDisplayMode } from '../../types';
import AutoCell from './AutoCell';
@@ -4,10 +4,10 @@ import { useState } from 'react';
import { GrafanaTheme2, formattedValueToString } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { TableCellDisplayMode, TableCellOptions } from '@grafana/schema';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
import { TableCellOptions, TableCellDisplayMode } from '../../types';
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
import { AutoCellProps } from '../types';
import { getCellLinks } from '../utils';
@@ -60,7 +60,6 @@ const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent
cell: css({
display: 'flex',
justifyContent: justifyContent,
a: {
color: 'inherit',
},
@@ -1,7 +1,8 @@
import { render, screen } from '@testing-library/react';
import { Field, FieldType, LinkModel } from '@grafana/data';
import { TableCellDisplayMode } from '@grafana/schema';
import { TableCellDisplayMode } from '../../types';
import { DataLinksCell } from './DataLinksCell';
@@ -1,6 +1,5 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import React, { useLayoutEffect, useRef, useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { Column, SortDirection } from 'react-data-grid';
import { Field, GrafanaTheme2 } from '@grafana/data';
@@ -9,22 +8,18 @@ import { useStyles2 } from '../../../../themes/ThemeContext';
import { getFieldTypeIcon } from '../../../../types/icon';
import { Icon } from '../../../Icon/Icon';
import { Filter } from '../Filter/Filter';
import { TableColumnResizeActionCallback, FilterType, TableRow, TableSummaryRow } from '../types';
import { FilterType, TableRow, TableSummaryRow } from '../types';
import { getDisplayName } from '../utils';
interface HeaderCellProps {
column: Column<TableRow, TableSummaryRow>;
rows: TableRow[];
field: Field;
onSort: (columnKey: string, direction: SortDirection, isMultiSort: boolean) => void;
direction?: SortDirection;
justifyContent: Property.JustifyContent;
filter: FilterType;
setFilter: React.Dispatch<React.SetStateAction<FilterType>>;
onColumnResize?: TableColumnResizeActionCallback;
headerCellRefs: React.MutableRefObject<Record<string, HTMLDivElement>>;
crossFilterOrder: React.MutableRefObject<string[]>;
crossFilterRows: React.MutableRefObject<{ [key: string]: TableRow[] }>;
crossFilterOrder: string[];
crossFilterRows: { [key: string]: TableRow[] };
showTypeIcons?: boolean;
}
@@ -32,113 +27,54 @@ const HeaderCell: React.FC<HeaderCellProps> = ({
column,
rows,
field,
onSort,
direction,
justifyContent,
filter,
setFilter,
onColumnResize,
headerCellRefs,
crossFilterOrder,
crossFilterRows,
showTypeIcons,
}) => {
const styles = useStyles2(getStyles, justifyContent);
const headerRef = useRef<HTMLDivElement>(null);
const styles = useStyles2(getStyles);
const displayName = useMemo(() => getDisplayName(field), [field]);
const filterable = useMemo(() => field.config.custom?.filterable ?? false, [field]);
const filterable = field.config?.custom?.filterable ?? false;
const displayName = getDisplayName(field);
let isColumnFilterable = filterable;
if (field.config.custom?.filterable !== filterable) {
isColumnFilterable = field.config.custom?.filterable || false;
}
// we have to remove/reset the filter if the column is not filterable
if (!isColumnFilterable && filter[displayName]) {
setFilter((filter: FilterType) => {
const newFilter = { ...filter };
delete newFilter[displayName];
return newFilter;
});
}
const handleSort = (event: React.MouseEvent<HTMLButtonElement>) => {
const isMultiSort = event.shiftKey;
onSort(column.key, direction === 'ASC' ? 'DESC' : 'ASC', isMultiSort);
};
// collecting header cell refs to handle manual column resize
useLayoutEffect(() => {
if (headerRef.current) {
headerCellRefs.current[column.key] = headerRef.current;
}
}, [headerRef, column.key]); // eslint-disable-line react-hooks/exhaustive-deps
// TODO: this is a workaround to handle manual column resize;
useEffect(() => {
const headerCellParent = headerRef.current?.parentElement;
if (headerCellParent) {
// `lastElement` is an HTML element added by react-data-grid for resizing columns.
// We add event listeners to `lastElement` to handle the resize operation.
const lastElement = headerCellParent.lastElementChild;
if (lastElement) {
const handleMouseUp = () => {
let newWidth = headerCellParent.clientWidth;
onColumnResize?.(column.key, newWidth);
};
lastElement.addEventListener('click', handleMouseUp);
return () => {
lastElement.removeEventListener('click', handleMouseUp);
};
}
if (!filterable && filter[displayName]) {
setFilter((filter: FilterType) => {
const newFilter = { ...filter };
delete newFilter[displayName];
return newFilter;
});
}
// to handle "Not all code paths return a value." error
return;
}, [column]); // eslint-disable-line react-hooks/exhaustive-deps
}, [filterable, displayName, filter, setFilter]);
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
ref={headerRef}
className={styles.headerCell}
// TODO find a better solution to this issue, see: https://github.com/adazzle/react-data-grid/issues/3535
// Unblock spacebar event
onKeyDown={(event) => {
if (event.key === ' ') {
event.stopPropagation();
}
}}
>
<button className={styles.headerCellLabel} onClick={handleSort}>
<>
<span className={styles.headerCellLabel}>
{showTypeIcons && <Icon name={getFieldTypeIcon(field)} title={field?.type} size="sm" />}
{/* Used cached displayName if available, otherwise use the column name (nested tables) */}
<div>{field.state?.displayName ?? column.name}</div>
<div>{getDisplayName(field)}</div>
{direction && (direction === 'ASC' ? <Icon name="arrow-up" size="lg" /> : <Icon name="arrow-down" size="lg" />)}
</button>
</span>
{isColumnFilterable && (
{filterable && (
<Filter
name={column.key}
rows={rows}
filter={filter}
setFilter={setFilter}
field={field}
crossFilterOrder={crossFilterOrder.current}
crossFilterRows={crossFilterRows.current}
crossFilterOrder={crossFilterOrder}
crossFilterRows={crossFilterRows}
/>
)}
</div>
</>
);
};
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({
headerCell: css({
display: 'flex',
gap: theme.spacing(0.5),
justifyContent,
}),
const getStyles = (theme: GrafanaTheme2) => ({
headerCellLabel: css({
border: 'none',
padding: 0,
@@ -3,10 +3,10 @@ import { Property } from 'csstype';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { TableCellDisplayMode } from '@grafana/schema';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
import { TableCellDisplayMode } from '../../types';
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
import { ImageCellProps } from '../types';
import { getCellLinks } from '../utils';
@@ -12,7 +12,7 @@ export function RowExpander({ height, onCellExpand, isExpanded }: RowExpanderNGP
function handleKeyDown(e: React.KeyboardEvent<HTMLSpanElement>) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onCellExpand();
onCellExpand(e);
}
}
return (
@@ -82,7 +82,7 @@ export const SparklineCell = (props: SparklineCellProps) => {
},
};
const hideValue = field.config.custom?.cellOptions?.hideValue;
const hideValue = cellOptions.hideValue;
let valueWidth = 0;
let valueElement: React.ReactNode = null;
if (!hideValue) {
@@ -0,0 +1,82 @@
import { WKT } from 'ol/format';
import { Geometry } from 'ol/geom';
import { FieldType } from '@grafana/data';
import { t } from '@grafana/i18n';
import { IconButton } from '../../../IconButton/IconButton';
import { TableCellInspectorMode } from '../../TableCellInspector';
import { TableCellDisplayMode } from '../../types';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellActionsProps } from '../types';
export function TableCellActions(props: TableCellActionsProps) {
const {
field,
value,
cellOptions,
displayName,
setIsInspecting,
setContextMenuProps,
onCellFilterAdded,
className,
cellInspect,
showFilters,
} = props;
return (
<div className={className}>
{cellInspect && (
<IconButton
name="eye"
aria-label={t('grafana-ui.table.cell-inspect-tooltip', 'Inspect value')}
onClick={() => {
let inspectValue = value;
let mode = TableCellInspectorMode.text;
if (field.type === FieldType.geo && value instanceof Geometry) {
inspectValue = new WKT().writeGeometry(value, {
featureProjection: 'EPSG:3857',
dataProjection: 'EPSG:4326',
});
mode = TableCellInspectorMode.code;
} else if ('cellType' in cellOptions && cellOptions.cellType === TableCellDisplayMode.JSONView) {
mode = TableCellInspectorMode.code;
}
setContextMenuProps({
value: String(inspectValue ?? ''),
mode,
});
setIsInspecting(true);
}}
/>
)}
{showFilters && (
<>
<IconButton
name={'search-plus'}
aria-label={t('grafana-ui.table.cell-filter-on', 'Filter for value')}
onClick={() => {
onCellFilterAdded?.({
key: displayName,
operator: FILTER_FOR_OPERATOR,
value: String(value ?? ''),
});
}}
/>
<IconButton
name={'search-minus'}
aria-label={t('grafana-ui.table.cell-filter-out', 'Filter out value')}
onClick={() => {
onCellFilterAdded?.({
key: displayName,
operator: FILTER_OUT_OPERATOR,
value: String(value ?? ''),
});
}}
/>
</>
)}
</div>
);
}
@@ -1,284 +0,0 @@
import { css } from '@emotion/css';
import { WKT } from 'ol/format';
import { Geometry } from 'ol/geom';
import { ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { FieldType, GrafanaTheme2, isDataFrame, isTimeSeriesFrame } from '@grafana/data';
import { t } from '@grafana/i18n';
import { TableAutoCellOptions, TableCellDisplayMode } from '@grafana/schema';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { IconButton } from '../../../IconButton/IconButton';
// import { GeoCell } from '../../Cells/GeoCell';
import { TableCellInspectorMode } from '../../TableCellInspector';
import {
CellColors,
CustomCellRendererProps,
FILTER_FOR_OPERATOR,
FILTER_OUT_OPERATOR,
TableCellNGProps,
} from '../types';
import { getCellColors, getDisplayName, getTextAlign } from '../utils';
import { ActionsCell } from './ActionsCell';
import AutoCell from './AutoCell';
import { BarGaugeCell } from './BarGaugeCell';
import { DataLinksCell } from './DataLinksCell';
import { GeoCell } from './GeoCell';
import { ImageCell } from './ImageCell';
import { JSONCell } from './JSONCell';
import { SparklineCell } from './SparklineCell';
export function TableCellNG(props: TableCellNGProps) {
const {
field,
frame,
value,
theme,
timeRange,
height,
rowIdx,
justifyContent,
shouldTextOverflow,
setIsInspecting,
setContextMenuProps,
getActions,
rowBg,
onCellFilterAdded,
replaceVariables,
} = props;
const cellInspect = field.config?.custom?.inspect ?? false;
const displayName = getDisplayName(field);
const { config: fieldConfig } = field;
const defaultCellOptions: TableAutoCellOptions = { type: TableCellDisplayMode.Auto };
const cellOptions = fieldConfig.custom?.cellOptions ?? defaultCellOptions;
const { type: cellType } = cellOptions;
const showFilters = field.config.filterable && onCellFilterAdded;
const isRightAligned = getTextAlign(field) === 'flex-end';
const displayValue = field.display!(value);
let colors: CellColors = { bgColor: '', textColor: '', bgHoverColor: '' };
if (rowBg) {
colors = rowBg(rowIdx);
} else {
colors = useMemo(() => getCellColors(theme, cellOptions, displayValue), [theme, cellOptions, displayValue]);
}
const styles = useStyles2(getStyles, isRightAligned, colors);
// TODO
// TableNG provides either an overridden cell width or 'auto' as the cell width value.
// While the overridden value gives the exact cell width, 'auto' does not.
// Therefore, we need to determine the actual cell width from the DOM.
const divWidthRef = useRef<HTMLDivElement>(null);
const [divWidth, setDivWidth] = useState(0);
const [isHovered, setIsHovered] = useState(false);
const actions = useMemo(
() => (getActions ? getActions(frame, field, rowIdx, replaceVariables) : []),
[getActions, frame, field, rowIdx, replaceVariables]
);
useLayoutEffect(() => {
if (divWidthRef.current && divWidthRef.current.clientWidth !== 0) {
setDivWidth(divWidthRef.current.clientWidth);
}
}, [divWidthRef.current]); // eslint-disable-line react-hooks/exhaustive-deps
// Common props for all cells
const commonProps = useMemo(
() => ({
value,
field,
rowIdx,
justifyContent,
}),
[value, field, rowIdx, justifyContent]
);
// Get the correct cell type
const renderedCell = useMemo(() => {
let cell: ReactNode = null;
switch (cellType) {
case TableCellDisplayMode.Sparkline:
cell = <SparklineCell {...commonProps} theme={theme} timeRange={timeRange} width={divWidth} />;
break;
case TableCellDisplayMode.Gauge:
case TableCellDisplayMode.BasicGauge:
case TableCellDisplayMode.GradientGauge:
case TableCellDisplayMode.LcdGauge:
cell = (
<BarGaugeCell
{...commonProps}
theme={theme}
timeRange={timeRange}
height={height}
width={divWidth}
actions={actions}
/>
);
break;
case TableCellDisplayMode.Image:
cell = <ImageCell {...commonProps} cellOptions={cellOptions} height={height} actions={actions} />;
break;
case TableCellDisplayMode.JSONView:
cell = <JSONCell {...commonProps} actions={actions} />;
break;
case TableCellDisplayMode.DataLinks:
cell = <DataLinksCell field={field} rowIdx={rowIdx} />;
break;
case TableCellDisplayMode.Actions:
cell = <ActionsCell actions={actions} />;
break;
case TableCellDisplayMode.Custom:
const CustomCellComponent: React.ComponentType<CustomCellRendererProps> = cellOptions.cellComponent;
cell = <CustomCellComponent field={field} value={value} rowIndex={rowIdx} frame={frame} />;
break;
case TableCellDisplayMode.Auto:
default:
// Handle auto cell type detection
if (field.type === FieldType.geo) {
cell = <GeoCell {...commonProps} height={height} />;
} else if (field.type === FieldType.frame) {
const firstValue = field.values[0];
if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) {
cell = <SparklineCell {...commonProps} theme={theme} timeRange={timeRange} width={divWidth} />;
} else {
cell = <JSONCell {...commonProps} actions={actions} />;
}
} else if (field.type === FieldType.other) {
cell = <JSONCell {...commonProps} actions={actions} />;
} else {
cell = <AutoCell {...commonProps} cellOptions={cellOptions} actions={actions} />;
}
break;
}
return cell;
}, [cellType, commonProps, theme, timeRange, divWidth, height, cellOptions, field, rowIdx, actions, value, frame]);
const handleMouseEnter = () => {
setIsHovered(true);
if (shouldTextOverflow()) {
// TODO: The table cell styles in TableNG do not update dynamically even if we change the state
const div = divWidthRef.current;
const tableCellDiv = div?.parentElement;
tableCellDiv?.style.setProperty('z-index', String(theme.zIndex.tooltip));
tableCellDiv?.style.setProperty('white-space', 'pre-line');
tableCellDiv?.style.setProperty('min-height', `100%`);
tableCellDiv?.style.setProperty('height', `fit-content`);
tableCellDiv?.style.setProperty('background', colors.bgHoverColor || 'none');
tableCellDiv?.style.setProperty('min-width', 'min-content');
}
};
const handleMouseLeave = () => {
setIsHovered(false);
if (shouldTextOverflow()) {
// TODO: The table cell styles in TableNG do not update dynamically even if we change the state
const div = divWidthRef.current;
const tableCellDiv = div?.parentElement;
tableCellDiv?.style.removeProperty('z-index');
tableCellDiv?.style.removeProperty('white-space');
tableCellDiv?.style.removeProperty('min-height');
tableCellDiv?.style.removeProperty('height');
tableCellDiv?.style.removeProperty('background');
tableCellDiv?.style.removeProperty('min-width');
}
};
const onFilterFor = useCallback(() => {
if (onCellFilterAdded) {
onCellFilterAdded({
key: displayName,
operator: FILTER_FOR_OPERATOR,
value: String(value ?? ''),
});
}
}, [displayName, onCellFilterAdded, value]);
const onFilterOut = useCallback(() => {
if (onCellFilterAdded) {
onCellFilterAdded({
key: displayName,
operator: FILTER_OUT_OPERATOR,
value: String(value ?? ''),
});
}
}, [displayName, onCellFilterAdded, value]);
return (
<div ref={divWidthRef} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} className={styles.cell}>
{renderedCell}
{isHovered && (cellInspect || showFilters) && (
<div className={styles.cellActions}>
{cellInspect && (
<IconButton
name="eye"
tooltip={t('grafana-ui.table.cell-inspect-tooltip', 'Inspect value')}
onClick={() => {
let inspectValue = value;
let mode = TableCellInspectorMode.text;
if (field.type === FieldType.geo && value instanceof Geometry) {
inspectValue = new WKT().writeGeometry(value, {
featureProjection: 'EPSG:3857',
dataProjection: 'EPSG:4326',
});
mode = TableCellInspectorMode.code;
} else if (cellType === TableCellDisplayMode.JSONView) {
mode = TableCellInspectorMode.code;
}
setContextMenuProps({
value: String(inspectValue ?? ''),
mode,
});
setIsInspecting(true);
}}
/>
)}
{showFilters && (
<>
<IconButton
name={'search-plus'}
onClick={onFilterFor}
tooltip={t('grafana-ui.table.cell-filter-on', 'Filter for value')}
/>
<IconButton
name={'search-minus'}
onClick={onFilterOut}
tooltip={t('grafana-ui.table.cell-filter-out', 'Filter out value')}
/>
</>
)}
</div>
)}
</div>
);
}
const getStyles = (theme: GrafanaTheme2, isRightAligned: boolean, color: CellColors) => ({
cell: css({
height: '100%',
alignContent: 'center',
paddingInline: '8px',
// TODO: follow-up on this: change styles on hover on table row level
background: color.bgColor || 'none',
color: color.textColor,
'&:hover': { background: color.bgHoverColor },
}),
cellActions: css({
display: 'flex',
position: 'absolute',
top: '1px',
left: isRightAligned ? 0 : undefined,
right: isRightAligned ? undefined : 0,
margin: 'auto',
height: '100%',
background: theme.colors.background.secondary,
color: theme.colors.text.primary,
padding: '4px 0px 4px 4px',
}),
});
@@ -0,0 +1,343 @@
import { render } from '@testing-library/react';
import { createDataFrame, createTheme, Field, FieldType } from '@grafana/data';
import { TableCellOptions, TableCellDisplayMode, TableCustomCellOptions } from '../../types';
import { getCellRenderer } from './renderers';
// Performance testing utilities
const measurePerformance = (fn: () => void, iterations = 100) => {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
fn();
}
const end = performance.now();
return (end - start) / iterations; // Average time per iteration
};
const createLargeTimeSeriesFrame = () => {
const timeValues = Array.from({ length: 100 }, (_, i) => Date.now() + i * 1000);
const valueValues = Array.from({ length: 100 }, (_, i) => Math.random() * 100);
return createDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: timeValues },
{ name: 'value', type: FieldType.number, values: valueValues },
],
});
};
const createLargeJSONData = () => {
return {
id: 1,
name: 'Test Object',
metadata: {
tags: Array.from({ length: 50 }, (_, i) => `tag-${i}`),
properties: Array.from({ length: 100 }, (_, i) => ({ key: `prop-${i}`, value: `value-${i}` })),
nested: {
level1: {
level2: {
level3: {
data: Array.from({ length: 20 }, (_, i) => ({ id: i, value: Math.random() })),
},
},
},
},
},
array: Array.from({ length: 200 }, (_, i) => ({ id: i, value: Math.random() * 1000 })),
};
};
describe('TableNG Cells renderers', () => {
describe('getCellRenderer', () => {
// Helper function to create a basic field
function createField<V>(type: FieldType, values: V[] = []): Field<V> {
return {
name: 'test',
type,
values,
config: {},
state: {},
display: jest.fn(() => ({ text: 'black', color: 'white', numeric: 0 })),
};
}
// Helper function to render a cell and get the test ID
const renderCell = (field: Field, cellOptions: TableCellOptions) =>
render(
getCellRenderer(
field,
cellOptions
)({
field,
value: 'test-value',
rowIdx: 0,
frame: createDataFrame({ fields: [field] }),
height: 100,
width: 100,
theme: createTheme(),
cellOptions,
cellInspect: false,
showFilters: false,
justifyContent: 'flex-start',
})
);
// Performance test helper
const benchmarkCellPerformance = (field: Field, cellOptions: TableCellOptions, iterations = 100) => {
// eslint-disable-next-line testing-library/render-result-naming-convention
const r = getCellRenderer(field, cellOptions);
return measurePerformance(() => {
render(
r({
field,
value: 'test-value',
rowIdx: 0,
frame: createDataFrame({ fields: [field] }),
height: 100,
width: 100,
theme: createTheme(),
cellOptions,
cellInspect: false,
showFilters: false,
justifyContent: 'flex-start',
})
);
}, iterations);
};
describe('explicit cell type cases', () => {
it.each([
{ type: TableCellDisplayMode.Sparkline, fieldType: FieldType.number },
{ type: TableCellDisplayMode.Gauge, fieldType: FieldType.number },
{ type: TableCellDisplayMode.JSONView, fieldType: FieldType.string },
{ type: TableCellDisplayMode.Image, fieldType: FieldType.string },
{ type: TableCellDisplayMode.DataLinks, fieldType: FieldType.string },
{ type: TableCellDisplayMode.Actions, fieldType: FieldType.string },
{ type: TableCellDisplayMode.ColorText, fieldType: FieldType.string },
{ type: TableCellDisplayMode.ColorBackground, fieldType: FieldType.string },
{ type: TableCellDisplayMode.Auto, fieldType: FieldType.string },
] as const)('should render $type cell into the document', ({ type, fieldType }) => {
const field = createField(fieldType);
const { container } = renderCell(field, { type });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
describe('invalid config cases', () => {
it('should return AutoCell when cellOptions.type is undefined', () => {
const field = createField(FieldType.string);
const { container } = renderCell(field, { type: undefined } as unknown as TableCellOptions);
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should return AutoCell when cellOptions is undefined', () => {
const field = createField(FieldType.string);
const { container } = renderCell(field, undefined as unknown as TableCellOptions);
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
});
});
describe('auto mode field type cases', () => {
it('should return GeoCell for geo field type', () => {
const field = createField(FieldType.geo);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should return SparklineCell for frame field type with time series', () => {
const timeSeriesFrame = createDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1, 2, 3] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
],
});
const field = createField(FieldType.frame, [timeSeriesFrame]);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should return JSONCell for frame field type with non-time series', () => {
const regularFrame = createDataFrame({
fields: [
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
],
});
const field = createField(FieldType.frame, [regularFrame]);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should return JSONCell for other field type', () => {
const field = createField(FieldType.other);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should return AutoCell for string field type', () => {
const field = createField(FieldType.string);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should return AutoCell for number field type', () => {
const field = createField(FieldType.number);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should return AutoCell for boolean field type', () => {
const field = createField(FieldType.boolean);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should return AutoCell for time field type', () => {
const field = createField(FieldType.time);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
});
describe('custom cell renderer cases', () => {
it('should return custom cell component for Custom type with valid cellComponent', () => {
const CustomComponent = () => <div data-testid="custom-cell">CustomCell</div>;
const field = createField(FieldType.string);
const { container } = renderCell(field, {
type: TableCellDisplayMode.Custom,
cellComponent: CustomComponent,
});
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('(invalid) should return null for Custom type without cellComponent', () => {
const field = createField(FieldType.string);
const { container } = renderCell(field, {
type: TableCellDisplayMode.Custom,
cellComponent: undefined,
} as unknown as TableCustomCellOptions);
expect(container.childNodes).toHaveLength(0);
});
});
describe('edge cases', () => {
it('should handle empty field values array', () => {
const field = createField(FieldType.frame, []);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should handle field with null values', () => {
const field = createField(FieldType.frame, [null]);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
it('should handle field with undefined values', () => {
const field = createField(FieldType.frame, [undefined]);
const { container } = renderCell(field, { type: TableCellDisplayMode.Auto });
expect(container).toBeInTheDocument();
expect(container.childNodes).toHaveLength(1);
});
});
describe.skip('performance benchmarks', () => {
// Performance thresholds (in milliseconds)
// these thresholds are tweaked based on performance on CI, not on a typical dev machine.
const PERFORMANCE_THRESHOLDS = {
FAST: 1, // Should render in under 1ms
MEDIUM: 2.5, // Should render in under 2.5ms
SLOW: 5, // Should render in under 5ms
};
describe('explicit cell type performance', () => {
it.each([
{ type: TableCellDisplayMode.Sparkline, threshold: PERFORMANCE_THRESHOLDS.MEDIUM },
{ type: TableCellDisplayMode.Gauge, threshold: PERFORMANCE_THRESHOLDS.SLOW },
{ type: TableCellDisplayMode.JSONView, threshold: PERFORMANCE_THRESHOLDS.FAST },
{ type: TableCellDisplayMode.Image, threshold: PERFORMANCE_THRESHOLDS.FAST },
{ type: TableCellDisplayMode.DataLinks, threshold: PERFORMANCE_THRESHOLDS.FAST },
{ type: TableCellDisplayMode.Actions, threshold: PERFORMANCE_THRESHOLDS.FAST },
{ type: TableCellDisplayMode.ColorText, threshold: PERFORMANCE_THRESHOLDS.FAST },
{ type: TableCellDisplayMode.ColorBackground, threshold: PERFORMANCE_THRESHOLDS.FAST },
{ type: TableCellDisplayMode.Auto, threshold: PERFORMANCE_THRESHOLDS.FAST },
] as const)('should render $type within performance threshold', ({ type, threshold }) => {
const field = createField(FieldType.number);
const avgTime = benchmarkCellPerformance(field, { type }, 100);
expect(avgTime).toBeLessThan(threshold);
});
});
describe('custom cell renderer performance', () => {
it('should render custom cell component within performance threshold', () => {
const CustomComponent = () => <div data-testid="custom-cell">CustomCell</div>;
const field = createField(FieldType.string);
const avgTime = benchmarkCellPerformance(
field,
{
type: TableCellDisplayMode.Custom,
cellComponent: CustomComponent,
},
50
);
expect(avgTime).toBeLessThan(PERFORMANCE_THRESHOLDS.FAST);
});
});
describe('large data performance', () => {
it('should render JSONCell with large JSON data within performance threshold', () => {
const largeJSON = createLargeJSONData();
const field = createField(FieldType.string, [JSON.stringify(largeJSON)]);
const avgTime = benchmarkCellPerformance(field, { type: TableCellDisplayMode.JSONView }, 20);
expect(avgTime).toBeLessThan(PERFORMANCE_THRESHOLDS.SLOW);
});
it('should render SparklineCell with large time series within performance threshold', () => {
const largeTimeSeriesFrame = createLargeTimeSeriesFrame();
const field = createField(FieldType.frame, [largeTimeSeriesFrame]);
const avgTime = benchmarkCellPerformance(field, { type: TableCellDisplayMode.Sparkline }, 10);
expect(avgTime).toBeLessThan(PERFORMANCE_THRESHOLDS.MEDIUM);
});
it('should render AutoCell with large string data within performance threshold', () => {
const largeString = 'x'.repeat(10000); // 10KB string
const field = createField(FieldType.string, [largeString]);
const avgTime = benchmarkCellPerformance(field, { type: TableCellDisplayMode.Auto }, 30);
expect(avgTime).toBeLessThan(PERFORMANCE_THRESHOLDS.MEDIUM);
});
});
});
});
});
@@ -0,0 +1,135 @@
import { ReactNode } from 'react';
import { Field, FieldType, isDataFrame, isTimeSeriesFrame } from '@grafana/data';
import { TableCellDisplayMode, TableCellOptions, TableCustomCellOptions } from '../../types';
import { TableCellRendererProps } from '../types';
import { ActionsCell } from './ActionsCell';
import AutoCell from './AutoCell';
import { BarGaugeCell } from './BarGaugeCell';
import { DataLinksCell } from './DataLinksCell';
import { GeoCell } from './GeoCell';
import { ImageCell } from './ImageCell';
import { JSONCell } from './JSONCell';
import { SparklineCell } from './SparklineCell';
export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode;
const GAUGE_RENDERER: TableCellRenderer = (props) => (
<BarGaugeCell
field={props.field}
value={props.value}
theme={props.theme}
height={props.height}
width={props.width}
rowIdx={props.rowIdx}
actions={props.actions}
/>
);
const AUTO_RENDERER: TableCellRenderer = (props) => (
<AutoCell
value={props.value}
field={props.field}
justifyContent={props.justifyContent}
rowIdx={props.rowIdx}
cellOptions={props.cellOptions}
actions={props.actions}
/>
);
const SPARKLINE_RENDERER: TableCellRenderer = (props) => (
<SparklineCell
value={props.value}
field={props.field}
justifyContent={props.justifyContent}
timeRange={props.timeRange}
rowIdx={props.rowIdx}
theme={props.theme}
width={props.width}
/>
);
const JSON_RENDERER: TableCellRenderer = (props) => (
<JSONCell
justifyContent={props.justifyContent}
value={props.value}
field={props.field}
rowIdx={props.rowIdx}
actions={props.actions}
/>
);
const GEO_RENDERER: TableCellRenderer = (props) => (
<GeoCell value={props.value} justifyContent={props.justifyContent} height={props.height} />
);
const IMAGE_RENDERER: TableCellRenderer = (props) => (
<ImageCell
cellOptions={props.cellOptions}
field={props.field}
height={props.height}
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} />;
function isCustomCellOptions(options: TableCellOptions): options is TableCustomCellOptions {
return options.type === TableCellDisplayMode.Custom;
}
const CUSTOM_RENDERER: TableCellRenderer = (props) => {
if (!isCustomCellOptions(props.cellOptions) || !props.cellOptions.cellComponent) {
return null; // nonsensical case, but better to typeguard it than throw.
}
const CustomCellComponent = props.cellOptions.cellComponent;
return <CustomCellComponent field={props.field} rowIndex={props.rowIdx} frame={props.frame} value={props.value} />;
};
const CELL_RENDERERS: Record<TableCellOptions['type'], TableCellRenderer> = {
[TableCellDisplayMode.Sparkline]: SPARKLINE_RENDERER,
[TableCellDisplayMode.Gauge]: GAUGE_RENDERER,
[TableCellDisplayMode.JSONView]: JSON_RENDERER,
[TableCellDisplayMode.Image]: IMAGE_RENDERER,
[TableCellDisplayMode.DataLinks]: DATA_LINKS_RENDERER,
[TableCellDisplayMode.Actions]: ACTIONS_RENDERER,
[TableCellDisplayMode.Custom]: CUSTOM_RENDERER,
[TableCellDisplayMode.ColorText]: AUTO_RENDERER,
[TableCellDisplayMode.ColorBackground]: AUTO_RENDERER,
[TableCellDisplayMode.Auto]: AUTO_RENDERER,
};
/** @internal */
export function getCellRenderer(field: Field, cellOptions: TableCellOptions): TableCellRenderer {
const cellType = cellOptions?.type ?? TableCellDisplayMode.Auto;
if (cellType === TableCellDisplayMode.Auto) {
return getAutoRendererResult(field);
}
return CELL_RENDERERS[cellType];
}
/** @internal */
export function getAutoRendererResult(field: Field): TableCellRenderer {
if (field.type === FieldType.geo) {
return GEO_RENDERER;
}
if (field.type === FieldType.frame) {
const firstValue = field.values[0];
if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) {
return SPARKLINE_RENDERER;
} else {
return JSON_RENDERER;
}
}
if (field.type === FieldType.other) {
return JSON_RENDERER;
}
return AUTO_RENDERER;
}
@@ -1,12 +1,12 @@
import { css, cx } from '@emotion/css';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { Icon } from '../../../Icon/Icon';
import { Popover } from '../../../Tooltip/Popover';
import { TableRow } from '../types';
import { FilterType, TableRow } from '../types';
import { REGEX_OPERATOR } from './FilterList';
import { FilterPopup } from './FilterPopup';
@@ -14,8 +14,8 @@ import { FilterPopup } from './FilterPopup';
interface Props {
name: string;
rows: any[];
filter: any;
setFilter: (value: any) => void;
filter: FilterType;
setFilter: (value: FilterType) => void;
field?: Field;
crossFilterOrder: string[];
crossFilterRows: { [key: string]: TableRow[] };
@@ -43,8 +43,6 @@ export const Filter = ({ name, rows, filter, setFilter, field, crossFilterOrder,
const [isPopoverVisible, setPopoverVisible] = useState<boolean>(false);
const styles = useStyles2(getStyles);
const filterEnabled = useMemo(() => Boolean(filterValue), [filterValue]);
const onShowPopover = useCallback(() => setPopoverVisible(true), [setPopoverVisible]);
const onClosePopover = useCallback(() => setPopoverVisible(false), [setPopoverVisible]);
const [searchFilter, setSearchFilter] = useState(filter[name]?.searchFilter || '');
const [operator, setOperator] = useState<SelectableValue<string>>(filter[name]?.operator || REGEX_OPERATOR);
@@ -53,7 +51,10 @@ export const Filter = ({ name, rows, filter, setFilter, field, crossFilterOrder,
className={cx(styles.headerFilter, filterEnabled ? styles.filterIconEnabled : styles.filterIconDisabled)}
ref={ref}
type="button"
onClick={onShowPopover}
onClick={(ev) => {
setPopoverVisible(true);
ev.stopPropagation();
}}
>
<Icon name="filter" />
{isPopoverVisible && ref.current && (
@@ -65,13 +66,18 @@ export const Filter = ({ name, rows, filter, setFilter, field, crossFilterOrder,
filterValue={filterValue}
setFilter={setFilter}
field={field}
onClose={onClosePopover}
onClose={() => setPopoverVisible(false)}
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
operator={operator}
setOperator={setOperator}
/>
}
onKeyDown={(event) => {
if (event.key === ' ') {
event.stopPropagation();
}
}}
placement="bottom-start"
referenceElement={ref.current}
show
@@ -12,6 +12,7 @@ import { FilterInput } from '../../../FilterInput/FilterInput';
import { Label } from '../../../Forms/Label';
import { Stack } from '../../../Layout/Stack/Stack';
import { FilterType } from '../types';
import { getDisplayName } from '../utils';
import { FilterList } from './FilterList';
import { calculateUniqueFieldValues, getFilteredOptions, valuesToOptions } from './utils';
@@ -110,7 +111,7 @@ export const FilterPopup = ({
<div className={styles.filterContainer} onClick={stopPropagation}>
<Stack direction="column">
<Stack alignItems="center">
{field && <Label className={styles.label}>{field.config.displayName || field.name}</Label>}
{field && <Label className={styles.label}>{getDisplayName(field)}</Label>}
<ButtonSelect
variant="canvas"
options={OPERATORS}
@@ -1,12 +1,13 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame, EventBus } from '@grafana/data';
import { TableCellDisplayMode } from '@grafana/schema';
import { applyFieldOverrides, createTheme, DataFrame, EventBus, FieldType, toDataFrame } from '@grafana/data';
import { TableCellBackgroundDisplayMode } from '@grafana/schema';
import { PanelContext } from '../../PanelChrome';
import { PanelContext, PanelContextProvider } from '../../../components/PanelChrome';
import { TableCellDisplayMode } from '../types';
import { TableNG, onRowHover, onRowLeave } from './TableNG';
import { TableNG } from './TableNG';
// Create a basic data frame for testing
const createBasicDataFrame = (): DataFrame => {
@@ -141,7 +142,7 @@ const createNestedDataFrame = (): DataFrame => {
config: { custom: { hidden: true } },
},
{
name: 'Nested frames',
name: '__nestedFrames',
type: FieldType.nestedFrames,
values: [[processedNestedFrame], [processedNestedFrame]],
config: { custom: {} },
@@ -251,7 +252,6 @@ const createSortingTestDataFrame = (): DataFrame => {
})[0];
};
// Create a data frame with time field for testing crosshair sharing functionality
const createTimeDataFrame = (): DataFrame => {
const frame = toDataFrame({
name: 'TimeTestData',
@@ -324,7 +324,7 @@ describe('TableNG', () => {
});
describe('Basic TableNG rendering', () => {
it('renders a simple table with columns and rows', () => {
it('renders a simple table with columns and rows', async () => {
const { container } = render(
<TableNG enableVirtualization={false} data={createBasicDataFrame()} width={800} height={600} />
);
@@ -649,9 +649,6 @@ describe('TableNG', () => {
describe('Sorting', () => {
it('allows sorting when clicking on column headers', async () => {
// Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = jest.fn();
const { container } = render(
<TableNG enableVirtualization={false} data={createBasicDataFrame()} width={800} height={600} />
);
@@ -661,50 +658,49 @@ describe('TableNG', () => {
expect(columnHeader).toBeInTheDocument();
// Find the sort button within the first header
if (columnHeader) {
// Store the initial state of the header
const initialSortAttribute = columnHeader.getAttribute('aria-sort');
if (!columnHeader) {
throw new Error('No column header found');
}
// Look for a button inside the header
const sortButton = columnHeader.querySelector('button') || columnHeader;
// Store the initial state of the header
const initialSortAttribute = columnHeader.getAttribute('aria-sort');
// Click the sort button
await user.click(sortButton);
// Look for a button inside the header
const sortButton = columnHeader.querySelector('button') || columnHeader;
// After clicking, the header should have an aria-sort attribute
const newSortAttribute = columnHeader.getAttribute('aria-sort');
// Click the sort button
await user.click(sortButton);
// The sort attribute should have changed
expect(newSortAttribute).not.toBe(initialSortAttribute);
// After clicking, the header should have an aria-sort attribute
const newSortAttribute = columnHeader.getAttribute('aria-sort');
// The sort attribute should be either 'ascending' or 'descending'
expect(['ascending', 'descending']).toContain(newSortAttribute);
// The sort attribute should have changed
expect(newSortAttribute).not.toBe(initialSortAttribute);
// Also verify the data is sorted by checking cell values
const cells = container.querySelectorAll('[role="gridcell"]');
const firstColumnCells = Array.from(cells).filter((_, index) => index % 2 === 0);
// The sort attribute should be either 'ascending' or 'descending'
expect(['ascending', 'descending']).toContain(newSortAttribute);
// Get the text content of the first column cells
const cellValues = firstColumnCells.map((cell) => cell.textContent);
// Also verify the data is sorted by checking cell values
const cells = container.querySelectorAll('[role="gridcell"]');
const firstColumnCells = Array.from(cells).filter((_, index) => index % 2 === 0);
// Verify we have values to check
expect(cellValues.length).toBeGreaterThan(0);
// Get the text content of the first column cells
const cellValues = firstColumnCells.map((cell) => cell.textContent);
// Verify the values are in sorted order based on the aria-sort attribute
const sortedValues = [...cellValues].sort();
// Verify we have values to check
expect(cellValues.length).toBeGreaterThan(0);
if (newSortAttribute === 'ascending') {
expect(JSON.stringify(cellValues)).toBe(JSON.stringify(sortedValues));
} else if (newSortAttribute === 'descending') {
expect(JSON.stringify(cellValues)).toBe(JSON.stringify([...sortedValues].reverse()));
}
// Verify the values are in sorted order based on the aria-sort attribute
const sortedValues = [...cellValues].sort();
if (newSortAttribute === 'ascending') {
expect(JSON.stringify(cellValues)).toBe(JSON.stringify(sortedValues));
} else if (newSortAttribute === 'descending') {
expect(JSON.stringify(cellValues)).toBe(JSON.stringify([...sortedValues].reverse()));
}
});
it('cycles through ascending, descending, and no sort states', async () => {
// Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = jest.fn();
const { container } = render(
<TableNG enableVirtualization={false} data={createBasicDataFrame()} width={800} height={600} />
);
@@ -733,10 +729,7 @@ describe('TableNG', () => {
}
});
it('supports multi-column sorting with shift key', async () => {
// Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = jest.fn();
it('supports multi-column sorting with cmd or ctrl key', async () => {
const { container } = render(
<TableNG enableVirtualization={false} data={createSortingTestDataFrame()} width={800} height={600} />
);
@@ -831,7 +824,7 @@ describe('TableNG', () => {
expect(categoryBValues).toContain('4');
// 2. Now add second sort column (Value) with shift key
await user.keyboard('{Shift>}');
await user.keyboard('{Control>}');
await user.click(valueColumnButton);
// Check data is sorted by Category and then by Value
@@ -866,7 +859,6 @@ describe('TableNG', () => {
expect(multiSortedRows[4][2]).toBe('Alice');
// 3. Change Value sort direction to descending
await user.keyboard('{Shift>}');
await user.click(valueColumnButton);
// Check data is sorted by Category (asc) and then by Value (desc)
@@ -901,7 +893,6 @@ describe('TableNG', () => {
expect(multiSortedRowsDesc[4][2]).toBe('Jane');
// 4. Test removing the secondary sort by clicking a third time
await user.keyboard('{Shift>}');
await user.click(valueColumnButton);
// The data should still be sorted by Category only
@@ -915,6 +906,56 @@ describe('TableNG', () => {
// Last 2 rows should still be 'B' category
expect(singleSortRows[3][0]).toBe('B');
expect(singleSortRows[4][0]).toBe('B');
// finally release control and prove that we exit multi-sort mode
await user.keyboard('{/Control}');
await user.click(categoryColumnButton);
const nonMultiSortCategoryRows = getCellTextContent();
expect(nonMultiSortCategoryRows[0][0]).toBe('B');
expect(nonMultiSortCategoryRows[0][1]).toBe('3');
expect(nonMultiSortCategoryRows[0][2]).toBe('Jane');
expect(nonMultiSortCategoryRows[1][0]).toBe('B');
expect(nonMultiSortCategoryRows[1][1]).toBe('4');
expect(nonMultiSortCategoryRows[1][2]).toBe('Alice');
expect(nonMultiSortCategoryRows[2][0]).toBe('A');
expect(nonMultiSortCategoryRows[2][1]).toBe('5');
expect(nonMultiSortCategoryRows[2][2]).toBe('John');
expect(nonMultiSortCategoryRows[3][0]).toBe('A');
expect(nonMultiSortCategoryRows[3][1]).toBe('1');
expect(nonMultiSortCategoryRows[3][2]).toBe('Bob');
expect(nonMultiSortCategoryRows[4][0]).toBe('A');
expect(nonMultiSortCategoryRows[4][1]).toBe('2');
expect(nonMultiSortCategoryRows[4][2]).toBe('Charlie');
await user.click(valueColumnButton);
const nonMultiSortValueRows = getCellTextContent();
expect(nonMultiSortValueRows[0][0]).toBe('A');
expect(nonMultiSortValueRows[0][1]).toBe('1');
expect(nonMultiSortValueRows[0][2]).toBe('Bob');
expect(nonMultiSortValueRows[1][0]).toBe('A');
expect(nonMultiSortValueRows[1][1]).toBe('2');
expect(nonMultiSortValueRows[1][2]).toBe('Charlie');
expect(nonMultiSortValueRows[2][0]).toBe('B');
expect(nonMultiSortValueRows[2][1]).toBe('3');
expect(nonMultiSortValueRows[2][2]).toBe('Jane');
expect(nonMultiSortValueRows[3][0]).toBe('B');
expect(nonMultiSortValueRows[3][1]).toBe('4');
expect(nonMultiSortValueRows[3][2]).toBe('Alice');
expect(nonMultiSortValueRows[4][0]).toBe('A');
expect(nonMultiSortValueRows[4][1]).toBe('5');
expect(nonMultiSortValueRows[4][2]).toBe('John');
});
it('correctly sorts different data types', async () => {
@@ -978,6 +1019,38 @@ describe('TableNG', () => {
// Verify number values are sorted numerically
expect(numberValues).toEqual(['1', '2', '3']);
});
it('triggers the onSortByChange callback', async () => {
const onSortByChange = jest.fn();
const { container } = render(
<TableNG
enableVirtualization={false}
data={createBasicDataFrame()}
width={800}
height={600}
onSortByChange={onSortByChange}
/>
);
// Ensure there are column headers
const columnHeader = container.querySelector('[role="columnheader"]');
expect(columnHeader).toBeInTheDocument();
// Find the sort button within the first header
if (!columnHeader) {
throw new Error('No column header found');
}
// Look for a button inside the header
const sortButton = columnHeader.querySelector('button') || columnHeader;
// Click the sort button
await user.click(sortButton);
// After clicking, the header should have an aria-sort attribute
expect(onSortByChange).toHaveBeenCalledTimes(1);
});
});
describe('Filtering', () => {
@@ -1159,8 +1232,28 @@ describe('TableNG', () => {
});
});
describe('Resizing', () => {
it('calls onColumnResize when column is resized', () => {
// TODO we need to test this with an e2e rather than a unit test, because the element dimensions calcs
// don't work in unit tests (no clientWidth/Height)
describe.skip('Resizing', () => {
beforeEach(() => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.setPointerCapture = jest.fn();
window.HTMLElement.prototype.hasPointerCapture = jest.fn();
window.HTMLElement.prototype.releasePointerCapture = jest.fn();
window.HTMLElement.prototype.getBoundingClientRect = jest.fn(() => ({
width: 100,
height: 20,
top: 0,
left: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
toJSON: jest.fn(() => ''),
}));
});
it('calls onColumnResize when column is resized', async () => {
const onColumnResize = jest.fn();
const { container } = render(
@@ -1174,22 +1267,19 @@ describe('TableNG', () => {
);
// Find resize handle
const resizeHandles = container.querySelectorAll('.rdg-header-row > [role="columnheader"] .rdg-resizer');
const resizeHandles = container.querySelectorAll('.rdg-header-row > [role="columnheader"] > div:last-child');
const handle = resizeHandles[0];
// TODO: This `if` doesn't even trigger - the test is evergreen.
// We should work out a reliable way to actually find and trigger the resize methods
// The querySelector doesn't return anything!
if (resizeHandles.length > 0) {
// Simulate resize by triggering mousedown, mousemove, mouseup
/* eslint-disable testing-library/prefer-user-event */
fireEvent.mouseDown(resizeHandles[0]);
fireEvent.mouseMove(resizeHandles[0], { clientX: 250 });
fireEvent.mouseUp(resizeHandles[0]);
/* eslint-enable testing-library/prefer-user-event */
// Check that onColumnResize was called
expect(onColumnResize).toHaveBeenCalled();
if (!handle) {
throw new Error('Resize handle not found');
}
// simulate a click, then drag, then release.
await userEvent.pointer({ keys: '[MouseLeft>]', coords: { x: 0, y: 0 }, target: handle });
await userEvent.pointer({ coords: { x: 250, y: 0 }, target: handle });
await userEvent.pointer({ keys: '[/MouseLeft]', coords: { x: 250, y: 0 }, target: handle });
await waitFor(() => expect(onColumnResize).toHaveBeenCalled());
});
});
@@ -1201,7 +1291,7 @@ describe('TableNG', () => {
const cells = container.querySelectorAll('[role="gridcell"]');
const cellStyles = window.getComputedStyle(cells[0]);
expect(cellStyles.getPropertyValue('white-space')).toBe('nowrap');
expect(cellStyles.getPropertyValue('white-space')).not.toBe('pre-line');
});
it('applies text wrapping styles when wrapText is true', () => {
@@ -1265,6 +1355,10 @@ describe('TableNG', () => {
// Check for the Inspect value menu item
const menuItem = await screen.findByText('Inspect value');
expect(menuItem).toBeInTheDocument();
// close the menu
await userEvent.click(container);
expect(menuItem).not.toBeInTheDocument();
});
});
@@ -1371,8 +1465,8 @@ describe('TableNG', () => {
expect(cells.length).toBeGreaterThan(0);
// Check the first div inside the cell for style attributes
const div = cells[0].querySelectorAll('div')[0];
const styleAttr = window.getComputedStyle(div);
const cell = cells[0];
const styleAttr = window.getComputedStyle(cell);
// Expected color is red
expect(styleAttr.background).toBe('rgb(255, 0, 0)');
@@ -1408,11 +1502,50 @@ describe('TableNG', () => {
expect(cells.length).toBeGreaterThan(0);
// Check the first div inside the cell for style attributes
const div = cells[0].querySelectorAll('div')[0];
const computedStyle = window.getComputedStyle(div);
const cell = cells[0];
const computedStyle = window.getComputedStyle(cell);
// Expected color is red
expect(computedStyle.color).toBe('rgb(255, 0, 0)');
// doesn't accidentally applyToRow
const otherCell = cells[1];
expect(window.getComputedStyle(otherCell).color).not.toBe('rgb(255, 0, 0)');
});
it("renders the background color correclty when using 'ColorBackground' display mode and applyToRow is true", () => {
// Create a frame with color background cells and applyToRow set to true
const frame = createBasicDataFrame();
frame.fields[0].config.custom = {
...frame.fields[0].config.custom,
cellOptions: {
type: TableCellDisplayMode.ColorBackground,
applyToRow: true,
mode: TableCellBackgroundDisplayMode.Basic,
},
};
// Add color to the display values
const originalDisplay = frame.fields[0].display;
const expectedColor = '#ff0000'; // Red color
frame.fields[0].display = (value: unknown) => {
const displayValue = originalDisplay ? originalDisplay(value) : { text: String(value), numeric: 0 };
return {
...displayValue,
color: expectedColor,
};
};
const { container } = render(<TableNG enableVirtualization={false} data={frame} width={800} height={600} />);
// Find rows in the table
const rows = container.querySelectorAll('[role="row"]');
const cells = rows[1].querySelectorAll('[role="gridcell"]'); // Skip header row
for (const cell of cells) {
const cellStyle = window.getComputedStyle(cell);
// Ensure each cell has the same background color
expect(cellStyle.backgroundColor).toBe('rgb(255, 0, 0)');
}
});
});
@@ -1451,44 +1584,77 @@ describe('TableNG', () => {
jest.clearAllMocks();
});
it('should publish DataHoverEvent when hovering over a row with time field', () => {
const frame = createTimeDataFrame();
const idx = 1;
onRowHover(idx, mockPanelContext, frame, true);
expect(mockEventBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
payload: {
point: {
time: new Date('2024-03-20T10:01:00Z').getTime(),
},
},
type: 'data-hover',
})
it('should publish DataHoverEvent when hovering over a row with time field', async () => {
const data = createTimeDataFrame();
render(
<PanelContextProvider value={mockPanelContext}>
<TableNG enableVirtualization={false} data={data} width={800} height={600} enableSharedCrosshair />
</PanelContextProvider>
);
await userEvent.hover(screen.getAllByRole('row')[1]);
expect(mockEventBus.publish).toHaveBeenCalledWith({
payload: {
point: {
time: data.fields[0].values[0],
},
},
type: 'data-hover',
});
});
it('should not publish DataHoverEvent when enableSharedCrosshair is false', () => {
const frame = createTimeDataFrame();
const idx = 1;
it('should not publish DataHoverEvent when enableSharedCrosshair is false', async () => {
render(
<PanelContextProvider value={mockPanelContext}>
<TableNG
enableVirtualization={false}
data={createTimeDataFrame()}
width={800}
height={600}
enableSharedCrosshair={false}
/>
</PanelContextProvider>
);
onRowHover(idx, mockPanelContext, frame, false);
await userEvent.hover(screen.getAllByRole('row')[1]);
expect(mockEventBus.publish).not.toHaveBeenCalled();
});
it('should not publish DataHoverEvent when time field is not present', () => {
const frame = createBasicDataFrame();
const idx = 1;
it('should not publish DataHoverEvent when time field is not present', async () => {
render(
<PanelContextProvider value={mockPanelContext}>
<TableNG
enableVirtualization={false}
data={createBasicDataFrame()}
width={800}
height={600}
enableSharedCrosshair
/>
</PanelContextProvider>
);
onRowHover(idx, mockPanelContext, frame, true);
await userEvent.hover(screen.getAllByRole('row')[1]);
expect(mockEventBus.publish).not.toHaveBeenCalled();
});
it('should publish DataHoverClearEvent when leaving a row', () => {
onRowLeave(mockPanelContext, true);
it('should publish DataHoverClearEvent when leaving a row', async () => {
render(
<PanelContextProvider value={mockPanelContext}>
<TableNG
enableVirtualization={false}
data={createTimeDataFrame()}
width={800}
height={600}
enableSharedCrosshair
/>
</PanelContextProvider>
);
await userEvent.hover(screen.getAllByRole('row')[1]);
await userEvent.unhover(screen.getAllByRole('row')[1]);
expect(mockEventBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1497,50 +1663,23 @@ describe('TableNG', () => {
);
});
it('should not publish DataHoverClearEvent when enableSharedCrosshair is false', () => {
onRowLeave(mockPanelContext, false);
it('should not publish DataHoverClearEvent when enableSharedCrosshair is false', async () => {
render(
<PanelContextProvider value={mockPanelContext}>
<TableNG
enableVirtualization={false}
data={createTimeDataFrame()}
width={800}
height={600}
enableSharedCrosshair={false}
/>
</PanelContextProvider>
);
await userEvent.hover(screen.getAllByRole('row')[1]);
await userEvent.unhover(screen.getAllByRole('row')[1]);
expect(mockEventBus.publish).not.toHaveBeenCalled();
});
});
describe('scroll position persistence', () => {
it('should persist scroll position after revId change', () => {
const data = createBasicDataFrame();
const { rerender } = render(<TableNG data={data} width={300} height={200} enableVirtualization={false} />);
// Find the DataGrid element
const dataGrid = screen.getByRole('grid');
// Simulate scrolling
fireEvent.scroll(dataGrid, {
target: {
scrollLeft: 100,
scrollTop: 50,
},
});
// Rerender with the same data but different fieldConfig to trigger revId change
rerender(
<TableNG
data={data}
width={300}
height={200}
enableVirtualization={false}
fieldConfig={{
defaults: {
custom: {
width: 200, // Different width to trigger revId change
},
},
overrides: [],
}}
/>
);
// Verify scroll position was restored
expect(dataGrid.scrollLeft).toBe(100);
expect(dataGrid.scrollTop).toBe(50);
});
});
});
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,8 @@ export const COLUMN = {
/** Table layout and display constants */
export const TABLE = {
CELL_PADDING: 8,
CELL_PADDING: 6,
HEADER_ROW_HEIGHT: 28,
MAX_CELL_HEIGHT: 48,
PAGINATION_LIMIT: 750,
SCROLL_BAR_WIDTH: 8,
@@ -0,0 +1,335 @@
import { act, renderHook } from '@testing-library/react';
import { Field, FieldType } from '@grafana/data';
import { useFilteredRows, usePaginatedRows, useSortedRows, useFooterCalcs } from './hooks';
import { getColumnTypes } from './utils';
describe('TableNG hooks', () => {
function setupData() {
// Mock data for testing
const fields: Field[] = [
{
name: 'name',
type: FieldType.string,
display: (v) => ({ text: v as string, numeric: NaN }),
config: {},
values: [],
},
{
name: 'age',
type: FieldType.number,
display: (v) => ({ text: (v as number).toString(), numeric: v as number }),
config: {},
values: [],
},
{
name: 'active',
type: FieldType.boolean,
display: (v) => ({ text: (v as boolean).toString(), numeric: NaN }),
config: {},
values: [],
},
];
const rows = [
{ name: 'Alice', age: 30, active: true, __depth: 0, __index: 0 },
{ name: 'Bob', age: 25, active: false, __depth: 0, __index: 1 },
{ name: 'Charlie', age: 35, active: true, __depth: 0, __index: 2 },
];
return { fields, rows };
}
describe('useFilteredRows', () => {
it('should correctly initialize with provided fields and rows', () => {
const { fields, rows } = setupData();
const { result } = renderHook(() => useFilteredRows(rows, fields, { hasNestedFrames: false }));
expect(result.current.rows[0].name).toBe('Alice');
});
it('should apply filters correctly', () => {
const { fields, rows } = setupData();
const { result } = renderHook(() => useFilteredRows(rows, fields, { hasNestedFrames: false }));
act(() => {
result.current.setFilter({
name: { filteredSet: new Set(['Alice']) },
});
});
expect(result.current.rows.length).toBe(1);
expect(result.current.rows[0].name).toBe('Alice');
});
it('should clear filters correctly', () => {
const { fields, rows } = setupData();
const { result } = renderHook(() => useFilteredRows(rows, fields, { hasNestedFrames: false }));
act(() => {
result.current.setFilter({
name: { filteredSet: new Set(['Alice']) },
});
});
expect(result.current.rows.length).toBe(1);
act(() => {
result.current.setFilter({});
});
expect(result.current.rows.length).toBe(3);
});
it.todo('should handle nested frames');
});
describe('useSortedRows', () => {
it('should correctly set up the table with an initial sort', () => {
const { fields, rows } = setupData();
const columnTypes = getColumnTypes(fields);
const { result } = renderHook(() =>
useSortedRows(rows, fields, {
columnTypes,
hasNestedFrames: false,
initialSortBy: [{ displayName: 'age', desc: false }],
})
);
// Initial state checks
expect(result.current.sortColumns).toEqual([{ columnKey: 'age', direction: 'ASC' }]);
expect(result.current.rows[0].name).toBe('Bob');
});
it('should change the sort on setSortColumns', () => {
const { fields, rows } = setupData();
const columnTypes = getColumnTypes(fields);
const { result } = renderHook(() =>
useSortedRows(rows, fields, {
columnTypes,
hasNestedFrames: false,
initialSortBy: [{ displayName: 'age', desc: false }],
})
);
expect(result.current.rows[0].name).toBe('Bob');
act(() => {
result.current.setSortColumns([{ columnKey: 'age', direction: 'DESC' }]);
});
expect(result.current.rows[0].name).toBe('Charlie');
act(() => {
result.current.setSortColumns([{ columnKey: 'name', direction: 'ASC' }]);
});
expect(result.current.rows[0].name).toBe('Alice');
});
it.todo('should handle nested frames');
});
describe('usePaginatedRows', () => {
it('should return defaults for pagination values when pagination is disabled', () => {
const { rows } = setupData();
const { result } = renderHook(() =>
usePaginatedRows(rows, { rowHeight: 30, height: 300, width: 800, enabled: false })
);
expect(result.current.page).toBe(-1);
expect(result.current.rowsPerPage).toBe(0);
expect(result.current.pageRangeStart).toBe(1);
expect(result.current.pageRangeEnd).toBe(3);
expect(result.current.rows.length).toBe(3);
});
it('should handle pagination correctly', () => {
// with the numbers provided here, we have 3 rows, with 2 rows per page, over 2 pages total.
const { rows } = setupData();
const { result } = renderHook(() =>
usePaginatedRows(rows, {
enabled: true,
height: 60,
width: 800,
rowHeight: 10,
})
);
expect(result.current.page).toBe(0);
expect(result.current.rowsPerPage).toBe(2);
expect(result.current.pageRangeStart).toBe(1);
expect(result.current.pageRangeEnd).toBe(2);
expect(result.current.rows.length).toBe(2);
act(() => {
result.current.setPage(1);
});
expect(result.current.page).toBe(1);
expect(result.current.rowsPerPage).toBe(2);
expect(result.current.pageRangeStart).toBe(3);
expect(result.current.pageRangeEnd).toBe(3);
expect(result.current.rows.length).toBe(1);
});
});
describe('useFooterCalcs', () => {
const rows = [
{ Field1: 1, Text: 'a', __depth: 0, __index: 0 },
{ Field1: 2, Text: 'b', __depth: 0, __index: 1 },
{ Field1: 3, Text: 'c', __depth: 0, __index: 2 },
{ Field2: 3, Text: 'd', __depth: 0, __index: 3 },
{ Field2: 10, Text: 'e', __depth: 0, __index: 4 },
];
const numericField: Field = {
name: 'Field1',
type: FieldType.number,
values: [1, 2, 3],
config: {
custom: {},
},
display: (value: unknown) => ({
text: String(value),
numeric: Number(value),
color: undefined,
prefix: undefined,
suffix: undefined,
}),
state: {},
getLinks: undefined,
};
const numericField2: Field = {
name: 'Field2',
type: FieldType.number,
values: [3, 10],
config: { custom: {} },
display: (value: unknown) => ({
text: String(value),
numeric: Number(value),
color: undefined,
prefix: undefined,
suffix: undefined,
}),
state: {},
getLinks: undefined,
};
const textField: Field = {
name: 'Text',
type: FieldType.string,
values: ['a', 'b', 'c'],
config: { custom: {} },
display: (value: unknown) => ({
text: String(value),
numeric: 0,
color: undefined,
prefix: undefined,
suffix: undefined,
}),
state: {},
getLinks: undefined,
};
it('should calculate sum for numeric fields', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, numericField], {
enabled: true,
footerOptions: { show: true, reducer: ['sum'] },
})
);
expect(result.current).toEqual(['Total', '6']); // 1 + 2 + 3
});
it('should calculate mean for numeric fields', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, numericField], {
enabled: true,
footerOptions: { show: true, reducer: ['mean'] },
})
);
expect(result.current).toEqual(['Mean', '2']); // (1 + 2 + 3) / 3
});
it('should return an empty string for non-numeric fields', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
enabled: true,
footerOptions: { show: true, reducer: ['sum'] },
})
);
expect(result.current).toEqual(['Total', '']);
});
it('should return empty array if no footerOptions are provided', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
enabled: true,
footerOptions: undefined,
})
);
expect(result.current).toEqual([]);
});
it('should return empty array when footer is disabled', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
enabled: false,
footerOptions: { show: true, reducer: ['sum'] },
})
);
expect(result.current).toEqual([]);
});
it('should return empty array when reducer is undefined', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
enabled: true,
footerOptions: { show: true, reducer: undefined },
})
);
expect(result.current).toEqual([]);
});
it('should return empty array when reducer is empty', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
enabled: true,
footerOptions: { show: true, reducer: [] },
})
);
expect(result.current).toEqual([]);
});
it('should return empty string if fields array doesnt include this field', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, numericField, numericField2], {
enabled: true,
footerOptions: { show: true, reducer: ['sum'], fields: ['Field2', 'Field3'] },
})
);
expect(result.current).toEqual(['Total', '', '13']);
});
it('should return the calculation if fields array includes this field', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, numericField, numericField2], {
enabled: true,
footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field2', 'Field3'] },
})
);
expect(result.current).toEqual(['Total', '6', '13']);
});
});
});
@@ -0,0 +1,506 @@
import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect } from 'react';
import { Column, DataGridProps, SortColumn } from 'react-data-grid';
import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data';
import { useTheme2 } from '../../../themes/ThemeContext';
import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types';
import { TABLE } from './constants';
import { ColumnTypes, FilterType, TableFooterCalc, TableRow, TableSortByFieldState, TableSummaryRow } from './types';
import { getDisplayName, processNestedTableRows, getCellHeightCalculator, applySort, getCellOptions } from './utils';
// Helper function to get displayed value
const getDisplayedValue = (row: TableRow, key: string, fields: Field[]) => {
const field = fields.find((field) => getDisplayName(field) === key);
if (!field || !field.display) {
return '';
}
const displayedValue = formattedValueToString(field.display(row[key]));
return displayedValue;
};
export interface FilteredRowsResult {
rows: TableRow[];
filter: FilterType;
setFilter: React.Dispatch<React.SetStateAction<FilterType>>;
crossFilterOrder: string[];
crossFilterRows: Record<string, TableRow[]>;
}
export interface FilteredRowsOptions {
hasNestedFrames: boolean;
}
export function useFilteredRows(
rows: TableRow[],
fields: Field[],
{ hasNestedFrames }: FilteredRowsOptions
): FilteredRowsResult {
// TODO: allow persisted filter selection via url
const [filter, setFilter] = useState<FilterType>({});
const filterValues = useMemo(() => Object.entries(filter), [filter]);
const crossFilterOrder: FilteredRowsResult['crossFilterOrder'] = useMemo(
() => Array.from(new Set(filterValues.map(([key]) => key))),
[filterValues]
);
const [filteredRows, crossFilterRows] = useMemo(() => {
const crossFilterRows: FilteredRowsResult['crossFilterRows'] = {};
const filterRows = (row: TableRow): boolean => {
for (const [key, value] of filterValues) {
const displayedValue = getDisplayedValue(row, key, fields);
if (!value.filteredSet.has(displayedValue)) {
return false;
}
// collect rows for crossFilter
crossFilterRows[key] = crossFilterRows[key] ?? [];
crossFilterRows[key].push(row);
}
return true;
};
const filteredRows = hasNestedFrames
? processNestedTableRows(rows, (parents) => parents.filter(filterRows))
: rows.filter(filterRows);
return [filteredRows, crossFilterRows];
}, [filterValues, rows, fields, hasNestedFrames]);
return {
rows: filteredRows,
filter,
setFilter,
crossFilterOrder,
crossFilterRows,
};
}
export interface SortedRowsOptions {
columnTypes: ColumnTypes;
hasNestedFrames: boolean;
initialSortBy?: TableSortByFieldState[];
}
export interface SortedRowsResult {
rows: TableRow[];
sortColumns: SortColumn[];
setSortColumns: React.Dispatch<React.SetStateAction<SortColumn[]>>;
}
export function useSortedRows(
rows: TableRow[],
fields: Field[],
{ initialSortBy, columnTypes, hasNestedFrames }: SortedRowsOptions
): SortedRowsResult {
const initialSortColumns = useMemo<SortColumn[]>(
() =>
initialSortBy?.flatMap(({ displayName, desc }) => {
if (!fields.some((f) => getDisplayName(f) === displayName)) {
return [];
}
return [
{
columnKey: displayName,
direction: desc ? ('DESC' as const) : ('ASC' as const),
},
];
}) ?? [],
[] // eslint-disable-line react-hooks/exhaustive-deps
);
const [sortColumns, setSortColumns] = useState<SortColumn[]>(initialSortColumns);
const sortedRows = useMemo(
() => applySort(rows, fields, sortColumns, columnTypes, hasNestedFrames),
[rows, fields, sortColumns, hasNestedFrames, columnTypes]
);
return {
rows: sortedRows,
sortColumns,
setSortColumns,
};
}
export interface PaginatedRowsOptions {
height: number;
width: number;
rowHeight: number | ((row: TableRow) => number);
hasHeader?: boolean;
hasFooter?: boolean;
paginationHeight?: number;
enabled?: boolean;
}
export interface PaginatedRowsResult {
rows: TableRow[];
page: number;
setPage: React.Dispatch<React.SetStateAction<number>>;
numPages: number;
rowsPerPage: number;
pageRangeStart: number;
pageRangeEnd: number;
smallPagination: boolean;
}
// hand-measured. pagination height is 30px, plus 8px top margin
const PAGINATION_HEIGHT = 38;
export function usePaginatedRows(
rows: TableRow[],
{ height, width, hasHeader, hasFooter, rowHeight, enabled }: PaginatedRowsOptions
): PaginatedRowsResult {
// TODO: allow persisted page selection via url
const [page, setPage] = useState(0);
const numRows = rows.length;
// calculate average row height if row height is variable.
const avgRowHeight = useMemo(() => {
if (typeof rowHeight === 'number') {
return rowHeight;
}
return rows.reduce((avg, row, _, { length }) => avg + rowHeight(row) / length, 0);
}, [rows, rowHeight]);
// using dimensions of the panel, calculate pagination parameters
const { numPages, rowsPerPage, pageRangeStart, pageRangeEnd, smallPagination } = useMemo((): {
numPages: number;
rowsPerPage: number;
pageRangeStart: number;
pageRangeEnd: number;
smallPagination: boolean;
} => {
if (!enabled) {
return { numPages: 0, rowsPerPage: 0, pageRangeStart: 1, pageRangeEnd: numRows, smallPagination: false };
}
// calculate number of rowsPerPage based on height stack
const rowAreaHeight =
height - (hasHeader ? TABLE.HEADER_ROW_HEIGHT : 0) - (hasFooter ? avgRowHeight : 0) - PAGINATION_HEIGHT;
const heightPerRow = Math.floor(rowAreaHeight / (avgRowHeight || 1));
// ensure at least one row per page is displayed
let rowsPerPage = heightPerRow > 1 ? heightPerRow : 1;
// calculate row range for pagination summary display
const pageRangeStart = page * rowsPerPage + 1;
let pageRangeEnd = pageRangeStart + rowsPerPage - 1;
if (pageRangeEnd > numRows) {
pageRangeEnd = numRows;
}
const smallPagination = width < TABLE.PAGINATION_LIMIT;
const numPages = Math.ceil(numRows / rowsPerPage);
return {
numPages,
rowsPerPage,
pageRangeStart,
pageRangeEnd,
smallPagination,
};
}, [width, height, hasHeader, hasFooter, avgRowHeight, enabled, numRows, page]);
// safeguard against page overflow on panel resize or other factors
useEffect(() => {
if (!enabled) {
return;
}
if (page > numPages) {
// resets pagination to end
setPage(numPages - 1);
}
}, [numPages, enabled, page, setPage]);
// apply pagination to the sorted rows
const paginatedRows = useMemo(() => {
if (!enabled) {
return rows;
}
const pageOffset = page * rowsPerPage;
return rows.slice(pageOffset, pageOffset + rowsPerPage);
}, [page, rowsPerPage, rows, enabled]);
return {
rows: paginatedRows,
page: enabled ? page : -1,
setPage,
numPages,
rowsPerPage,
pageRangeStart,
pageRangeEnd,
smallPagination,
};
}
export interface FooterCalcsOptions {
enabled?: boolean;
isCountRowsSet?: boolean;
footerOptions?: TableFooterCalc;
}
export function useFooterCalcs(
rows: TableRow[],
fields: Field[],
{ enabled, footerOptions, isCountRowsSet }: FooterCalcsOptions
): string[] {
return useMemo(() => {
const footerReducers = footerOptions?.reducer;
if (!enabled || !footerOptions || !Array.isArray(footerReducers) || !footerReducers.length) {
return [];
}
return fields.map((field, index) => {
if (field.state?.calcs) {
delete field.state?.calcs;
}
if (isCountRowsSet) {
return index === 0 ? `${rows.length}` : '';
}
if (index === 0) {
const footerCalcReducer = footerReducers[0];
return footerCalcReducer ? fieldReducers.get(footerCalcReducer).name : '';
}
if (field.type !== FieldType.number) {
return '';
}
// if field.display is undefined, don't throw
const displayFn = field.display;
if (!displayFn) {
return '';
}
// If fields array is specified, only show footer for fields included in that array
if (footerOptions.fields?.length && !footerOptions.fields?.includes(getDisplayName(field))) {
return '';
}
const calc = footerReducers[0];
const value = reduceField({
field: {
...field,
values: rows.map((row) => row[getDisplayName(field)]),
},
reducers: footerReducers,
})[calc];
return formattedValueToString(displayFn(value));
});
}, [fields, enabled, footerOptions, isCountRowsSet, rows]);
}
export function useTextWraps(fields: Field[]): Record<string, boolean> {
return useMemo(
() =>
fields.reduce<{ [key: string]: boolean }>((acc, field) => {
const cellOptions = getCellOptions(field);
const displayName = getDisplayName(field);
const wrapText = 'wrapText' in cellOptions && cellOptions.wrapText;
return { ...acc, [displayName]: !!wrapText };
}, {}),
[fields]
);
}
export function useTypographyCtx() {
const theme = useTheme2();
const { ctx, font, avgCharWidth } = useMemo(() => {
const font = `${theme.typography.fontSize}px ${theme.typography.fontFamily}`;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
// set in grafana/data in createTypography.ts
const letterSpacing = 0.15;
ctx.letterSpacing = `${letterSpacing}px`;
ctx.font = font;
const txt =
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s";
const txtWidth = ctx.measureText(txt).width;
const avgCharWidth = txtWidth / txt.length + letterSpacing;
return {
ctx,
font,
avgCharWidth,
};
}, [theme.typography.fontSize, theme.typography.fontFamily]);
return { ctx, font, avgCharWidth };
}
export function useRowHeight(
columnWidths: number[],
fields: Field[],
hasNestedFrames: boolean,
defaultRowHeight: number,
expandedRows: Record<string, boolean>
): number | ((row: TableRow) => number) {
const [wrappedColIdxs, hasWrappedCols] = useMemo(() => {
let hasWrappedCols = false;
return [
fields.map((field) => {
if (field.type !== FieldType.string) {
return false;
}
const cellOptions = getCellOptions(field);
const wrapText = 'wrapText' in cellOptions && cellOptions.wrapText;
const type = cellOptions.type;
const result = !!wrapText && type !== TableCellDisplayMode.Image;
if (result === true) {
hasWrappedCols = true;
}
return result;
}),
hasWrappedCols,
];
}, [fields]);
const { ctx, avgCharWidth } = useTypographyCtx();
const rowHeight = useMemo(() => {
// row height is only complicated when there are nested frames or wrapped columns.
if (!hasNestedFrames && !hasWrappedCols) {
return defaultRowHeight;
}
const HPADDING = TABLE.CELL_PADDING;
const VPADDING = TABLE.CELL_PADDING;
const BORDER_RIGHT = 0.666667;
const LINE_HEIGHT = 22;
const wrapWidths = columnWidths.map((c) => c - 2 * HPADDING - BORDER_RIGHT);
const calc = getCellHeightCalculator(ctx, LINE_HEIGHT, defaultRowHeight, VPADDING);
return (row: TableRow) => {
// nested rows
if (Number(row.__depth) > 0) {
// if unexpanded, height === 0
if (!expandedRows[row.__index]) {
return 0;
}
// Ensure we have a minimum height (defaultRowHeight) for the nested table even if data is empty
const headerCount = row?.data?.meta?.custom?.noHeader ? 0 : 1;
const rowCount = row.data?.length ?? 0;
return Math.max(defaultRowHeight, defaultRowHeight * (rowCount + headerCount));
}
// regular rows
let maxLines = 1;
let maxLinesIdx = -1;
let maxLinesText = '';
for (let i = 0; i < columnWidths.length; i++) {
if (wrappedColIdxs[i]) {
const cellTextRaw = fields[i].values[row.__index];
if (cellTextRaw != null) {
const cellText = String(cellTextRaw);
const charsPerLine = wrapWidths[i] / avgCharWidth;
const approxLines = cellText.length / charsPerLine;
if (approxLines > maxLines) {
maxLines = approxLines;
maxLinesIdx = i;
maxLinesText = cellText;
}
}
}
}
if (maxLinesIdx === -1) {
return defaultRowHeight;
}
return calc(maxLinesText, wrapWidths[maxLinesIdx]);
};
}, [
avgCharWidth,
columnWidths,
ctx,
defaultRowHeight,
expandedRows,
fields,
hasNestedFrames,
hasWrappedCols,
wrappedColIdxs,
]);
return rowHeight;
}
/**
* react-data-grid is a little unwieldy when it comes to column resize events.
* we want to detect a few different column resize signals:
* - dragging the handle (only want to dispatch when handle is released)
* - double-clicking the handle (sets the column to the minimum width to fit content)
* `onColumnResize` dispatches events throughout a dragged resize, and `onColumnWidthsChanged` doesn't
* emit an event when double-click resizing occurs, so we have to build something custom on top of these
* behaviors in order to get everything working.
*/
interface UseColumnResizeState {
columnKey: string | undefined;
width: number;
}
const INITIAL_COL_RESIZE_STATE = Object.freeze({ columnKey: undefined, width: 0 }) satisfies UseColumnResizeState;
export function useColumnResize(
onColumnResize: TableColumnResizeActionCallback = () => {}
): DataGridProps<TableRow, TableSummaryRow>['onColumnResize'] {
// these must be refs. if we used setState, we would run into race conditions with these event listeners
const colResizeState = useRef<UseColumnResizeState>({ ...INITIAL_COL_RESIZE_STATE });
const pointerIsDown = useRef(false);
// to detect whether we got a double-click resize, we track whether the pointer is currently down
useLayoutEffect(() => {
function pointerDown(_event: PointerEvent) {
pointerIsDown.current = true;
}
function pointerUp(_event: PointerEvent) {
pointerIsDown.current = false;
}
window.addEventListener('pointerdown', pointerDown);
window.addEventListener('pointerup', pointerUp);
return () => {
window.removeEventListener('pointerdown', pointerDown);
window.removeEventListener('pointerup', pointerUp);
};
});
const dispatchEvent = useCallback(() => {
if (colResizeState.current.columnKey) {
onColumnResize(colResizeState.current.columnKey, Math.floor(colResizeState.current.width));
colResizeState.current = { ...INITIAL_COL_RESIZE_STATE };
}
window.removeEventListener('click', dispatchEvent, { capture: true });
}, [onColumnResize]);
// this is the callback that gets passed to react-data-grid
const dataGridResizeHandler = useCallback(
(column: Column<TableRow, TableSummaryRow>, width: number) => {
if (!colResizeState.current.columnKey) {
window.addEventListener('click', dispatchEvent, { capture: true });
}
colResizeState.current.columnKey = column.key;
colResizeState.current.width = width;
// when double clicking to resize, this handler will fire, but the pointer will not be down,
// meaning that we should immediately flush the new width
if (!pointerIsDown.current) {
dispatchEvent();
}
},
[dispatchEvent]
);
return dataGridResizeHandler;
}
@@ -1,4 +1,5 @@
import { Property } from 'csstype';
import { SyntheticEvent } from 'react';
import { Column } from 'react-data-grid';
import {
@@ -12,10 +13,12 @@ import {
InterpolateFunction,
FieldType,
DataFrameWithValue,
SelectableValue,
} from '@grafana/data';
import { TableCellOptions, TableCellHeight, TableFieldOptions } from '@grafana/schema';
import { TableCellHeight, TableFieldOptions } from '@grafana/schema';
import { TableCellInspectorMode } from '../TableCellInspector';
import { TableCellOptions } from '../types';
export const FILTER_FOR_OPERATOR = '=';
export const FILTER_OUT_OPERATOR = '!=';
@@ -39,11 +42,15 @@ export type TableFieldOptionsType = Omit<TableFieldOptions, 'cellOptions'> & {
headerComponent?: React.ComponentType<CustomHeaderRendererProps>;
};
export type FilterType = {
[key: string]: {
export type FilterType = Record<
string,
{
filteredSet: Set<string>;
};
};
filtered?: Array<SelectableValue<unknown>>;
searchFilter?: string;
operator?: SelectableValue<string>;
}
>;
/* ----------------------------- Table specific types ----------------------------- */
export interface TableSummaryRow {
@@ -51,12 +58,9 @@ export interface TableSummaryRow {
}
export interface TableColumn extends Column<TableRow, TableSummaryRow> {
key: string; // Unique identifier used by DataGrid
name: string; // Display name in header
field: Field; // Grafana field data/config
width?: number | string; // Column width
minWidth?: number; // Min width constraint
cellClass?: string; // CSS styling
}
// Possible values for table cells based on field types
@@ -77,7 +81,8 @@ export interface TableRow {
// Nested table properties
data?: DataFrame;
'Nested frames'?: DataFrame[];
__nestedFrames?: DataFrame[];
__expanded?: boolean; // For row expansion state
// Generic typing for column values
[columnName: string]: TableCellValue;
@@ -104,7 +109,7 @@ export interface TableSortByFieldState {
export interface TableFooterCalc {
show: boolean;
reducer?: string[]; // Make this optional
reducer?: string[];
fields?: string[];
enablePagination?: boolean;
countRows?: boolean;
@@ -129,6 +134,7 @@ export interface BaseTableProps {
footerValues?: FooterItem[];
enablePagination?: boolean;
cellHeight?: TableCellHeight;
structureRev?: number;
/** @alpha Used by SparklineCell when provided */
timeRange?: TimeRange;
enableSharedCrosshair?: boolean;
@@ -144,28 +150,48 @@ export interface BaseTableProps {
/* ---------------------------- Table cell props ---------------------------- */
export interface TableNGProps extends BaseTableProps {}
export interface TableCellNGProps {
field: Field;
frame: DataFrame;
getActions?: GetActionsFunction;
height: number;
justifyContent: Property.JustifyContent;
export interface TableCellRendererProps {
actions?: ActionModel[];
rowIdx: number;
setContextMenuProps: (props: { value: string; top?: number; left?: number; mode?: TableCellInspectorMode }) => void;
setIsInspecting: (isInspecting: boolean) => void;
shouldTextOverflow: () => boolean;
theme: GrafanaTheme2;
timeRange: TimeRange;
frame: DataFrame;
timeRange?: TimeRange;
value: TableCellValue;
rowBg: Function | undefined;
height: number;
// flags that are static per column
field: Field;
cellOptions: TableCellOptions;
width: number;
theme: GrafanaTheme2;
cellInspect: boolean;
showFilters: boolean;
justifyContent: Property.JustifyContent;
}
export type ContextMenuProps = {
rowIdx?: number;
value: string;
mode?: TableCellInspectorMode.code | TableCellInspectorMode.text;
top?: number;
left?: number;
};
export interface TableCellActionsProps {
field: Field;
value: TableCellValue;
cellOptions: TableCellOptions;
displayName: string;
cellInspect: boolean;
showFilters: boolean;
setIsInspecting: React.Dispatch<React.SetStateAction<boolean>>;
setContextMenuProps: React.Dispatch<React.SetStateAction<ContextMenuProps | null>>;
className?: string;
onCellFilterAdded?: TableFilterActionCallback;
replaceVariables?: InterpolateFunction;
}
/* ------------------------- Specialized Cell Props ------------------------- */
export interface RowExpanderNGProps {
height: number;
onCellExpand: () => void;
onCellExpand: (e: SyntheticEvent) => void;
isExpanded?: boolean;
}
@@ -174,7 +200,7 @@ export interface SparklineCellProps {
justifyContent: Property.JustifyContent;
rowIdx: number;
theme: GrafanaTheme2;
timeRange: TimeRange;
timeRange?: TimeRange;
value: TableCellValue;
width: number;
}
@@ -186,7 +212,6 @@ export interface BarGaugeCellProps extends ActionCellProps {
theme: GrafanaTheme2;
value: TableCellValue;
width: number;
timeRange: TimeRange;
}
export interface ImageCellProps extends ActionCellProps {
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,5 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import React from 'react';
import { SortColumn, SortDirection } from 'react-data-grid';
import { SortColumn } from 'react-data-grid';
import tinycolor from 'tinycolor2';
import { varPreLine } from 'uwrap';
@@ -9,7 +7,6 @@ import {
FieldType,
Field,
formattedValueToString,
reduceField,
GrafanaTheme2,
DisplayValue,
LinkModel,
@@ -18,89 +15,24 @@ import {
} from '@grafana/data';
import {
BarGaugeDisplayMode,
TableAutoCellOptions,
TableCellBackgroundDisplayMode,
TableCellDisplayMode,
TableCellHeight,
TableCellOptions,
TableSortByFieldState,
} from '@grafana/schema';
import { getTextColorForAlphaBackground } from '../../../utils/colors';
import { TableCellInspectorMode } from '../TableCellInspector';
import { TableCellOptions } from '../types';
import { TABLE } from './constants';
import {
CellColors,
TableRow,
TableFieldOptionsType,
ColumnTypes,
FilterType,
FrameToRowsConverter,
TableNGProps,
Comparator,
TableFooterCalc,
} from './types';
import { COLUMN, TABLE } from './constants';
import { CellColors, TableRow, TableFieldOptionsType, ColumnTypes, FrameToRowsConverter, Comparator } from './types';
/* ---------------------------- Cell calculations --------------------------- */
export function getCellHeight(
text: string,
cellWidth: number, // width of the cell without padding
ctx: CanvasRenderingContext2D,
lineHeight: number,
defaultRowHeight: number,
padding = 0
) {
const PADDING = padding * 2;
if (typeof text === 'string') {
const words = text.split(/\s/);
const lines = [];
let currentLine = '';
// Let's just wrap the lines and see how well the measurement works
for (let i = 0; i < words.length; i++) {
const currentWord = words[i];
// TODO: this method is not accurate
let lineWidth = ctx.measureText(currentLine + ' ' + currentWord).width;
// if line width is less than the cell width, add the word to the current line and continue
// else add the current line to the lines array and start a new line with the current word
if (lineWidth < cellWidth) {
currentLine += ' ' + currentWord;
} else {
lines.push({
width: lineWidth,
line: currentLine,
});
currentLine = currentWord;
}
// if we are at the last word, add the current line to the lines array
if (i === words.length - 1) {
lines.push({
width: lineWidth,
line: currentLine,
});
}
}
if (lines.length === 1) {
return defaultRowHeight;
}
// TODO: double padding to adjust osContext.measureText() results
const height = lines.length * lineHeight + PADDING * 2;
return height;
}
return defaultRowHeight;
}
export type CellHeightCalculator = (text: string, cellWidth: number) => number;
/**
* @internal
* Returns a function that calculates the height of a cell based on its text content and width.
*/
export function getCellHeightCalculator(
// should be pre-configured with font and letterSpacing
ctx: CanvasRenderingContext2D,
@@ -111,15 +43,17 @@ export function getCellHeightCalculator(
const { count } = varPreLine(ctx);
return (text: string, cellWidth: number) => {
const effectiveCellWidth = Math.max(cellWidth, 20); // Minimum width to work with
const TOTAL_PADDING = padding * 2;
const numLines = count(text, effectiveCellWidth);
const totalHeight = numLines * lineHeight + TOTAL_PADDING;
const numLines = count(text, cellWidth);
const totalHeight = numLines * lineHeight + 2 * padding;
return Math.max(totalHeight, defaultRowHeight);
};
}
export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight: TableCellHeight | undefined): number {
/**
* @internal
* Returns the default row height based on the theme and cell height setting.
*/
export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight?: TableCellHeight): number {
const bodyFontSize = theme.typography.fontSize;
const lineHeight = theme.typography.body.lineHeight;
@@ -136,61 +70,12 @@ export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight: TableCellH
}
/**
* getRowHeight determines cell height based on cell width + text length. Used
* for when textWrap is enabled.
* @internal
* Returns true if text overflow handling should be applied to the cell.
*/
export function getRowHeight(
row: TableRow,
calc: CellHeightCalculator,
avgCharWidth: number,
defaultRowHeight: number,
fieldsData: {
headersLength: number;
textWraps: { [key: string]: boolean };
columnTypes: ColumnTypes;
columnWidths: Record<string, number>;
fieldDisplayType: Record<string, TableCellDisplayMode>;
}
): number {
let maxLines = 1;
let maxLinesCol = '';
for (const key in row) {
if (
fieldsData.columnTypes[key] === FieldType.string &&
fieldsData.textWraps[key] &&
fieldsData.fieldDisplayType[key] !== TableCellDisplayMode.Image
) {
const cellText = row[key] as string;
if (cellText != null) {
const charsPerLine = fieldsData.columnWidths[key] / avgCharWidth;
const approxLines = cellText.length / charsPerLine;
if (approxLines > maxLines) {
maxLines = approxLines;
maxLinesCol = key;
}
}
}
}
return maxLinesCol === '' ? defaultRowHeight : calc(row[maxLinesCol] as string, fieldsData.columnWidths[maxLinesCol]);
}
export function isTextCell(key: string, columnTypes: Record<string, string>): boolean {
return columnTypes[key] === FieldType.string;
}
export function shouldTextOverflow(
key: string,
row: TableRow,
columnTypes: ColumnTypes,
headerCellRefs: React.MutableRefObject<Record<string, HTMLDivElement>>,
ctx: CanvasRenderingContext2D,
lineHeight: number,
defaultRowHeight: number,
padding: number,
textWrap: boolean,
field: Field,
cellType: TableCellDisplayMode
@@ -199,13 +84,17 @@ export function shouldTextOverflow(
// Tech debt: Technically image cells are of type string, which is misleading (kinda?)
// so we need to ensure we don't apply overflow hover states fo type image
if (textWrap || cellInspect || cellType === TableCellDisplayMode.Image || !isTextCell(key, columnTypes)) {
if (textWrap || cellInspect || cellType === TableCellDisplayMode.Image || columnTypes[key] !== FieldType.string) {
return false;
}
return true;
}
/**
* @internal
* Returns the text alignment for a field based on its type and configuration.
*/
export function getTextAlign(field?: Field): Property.JustifyContent {
if (!field) {
return 'flex-start';
@@ -231,21 +120,22 @@ export function getTextAlign(field?: Field): Property.JustifyContent {
return 'flex-start';
}
const defaultCellOptions: TableAutoCellOptions = { type: TableCellDisplayMode.Auto };
const DEFAULT_CELL_OPTIONS = { type: TableCellDisplayMode.Auto } as const;
/**
* @internal
* Returns the cell options for a field, migrating from legacy displayMode if necessary.
*/
export function getCellOptions(field: Field): TableCellOptions {
if (field.config.custom?.displayMode) {
return migrateTableDisplayModeToCellOptions(field.config.custom?.displayMode);
}
if (!field.config.custom?.cellOptions) {
return defaultCellOptions;
}
return field.config.custom.cellOptions;
return field.config.custom?.cellOptions ?? DEFAULT_CELL_OPTIONS;
}
/**
* @internal
* Getting gauge or sparkline values to align is very tricky without looking at all values and passing them through display processor.
* For very large tables that could pretty expensive. So this is kind of a compromise. We look at the first 1000 rows and cache the longest value.
* If we have a cached value we just check if the current value is longer and update the alignmentFactor. This can obviously still lead to
@@ -271,7 +161,7 @@ export function getAlignmentFactor(
const maxIndex = Math.min(field.values.length, rowIndex + 1000);
for (let i = rowIndex + 1; i < maxIndex; i++) {
const nextDisplayValue = field.display!(field.values[i]);
const nextDisplayValue = field.display?.(field.values[i]) ?? field.values[i];
if (formattedValueToString(alignmentFactor).length > formattedValueToString(nextDisplayValue).length) {
alignmentFactor.text = displayValue.text;
}
@@ -287,69 +177,27 @@ export function getAlignmentFactor(
}
}
/* ------------------------------ Footer calculations ------------------------------ */
export function getFooterItemNG(rows: TableRow[], field: Field, options: TableFooterCalc | undefined): string {
if (options === undefined) {
return '';
}
if (field.type !== FieldType.number) {
return '';
}
// Check if reducer array exists and has at least one element
if (!options.reducer || !options.reducer.length) {
return '';
}
// If fields array is specified, only show footer for fields included in that array
if (options.fields && options.fields.length > 0) {
if (!options.fields.includes(field.name)) {
return '';
}
}
const calc = options.reducer[0];
const value = reduceField({
field: {
...field,
values: rows.map((row) => row[getDisplayName(field)]),
},
reducers: options.reducer,
})[calc];
const formattedValue = formattedValueToString(field.display!(value));
return formattedValue;
}
export const getFooterStyles = (justifyContent: Property.JustifyContent) => ({
footerCell: css({
display: 'flex',
justifyContent: justifyContent || 'space-between',
}),
});
/* ------------------------- Cell color calculation ------------------------- */
const CELL_COLOR_DARKENING_MULTIPLIER = 10;
const CELL_GRADIENT_DARKENING_MULTIPLIER = 15;
const CELL_GRADIENT_HUE_ROTATION_DEGREES = 5;
/**
* @internal
* Returns the text and background colors for a table cell based on its options and display value.
*/
export function getCellColors(
theme: GrafanaTheme2,
cellOptions: TableCellOptions,
displayValue: DisplayValue
): CellColors {
// Convert RGBA hover color to hex to prevent transparency issues on cell hover
const autoCellBackgroundHoverColor = convertRGBAToHex(theme.colors.background.primary, theme.colors.action.hover);
// How much to darken elements depends upon if we're in dark mode
const darkeningFactor = theme.isDark ? 1 : -0.7;
// Setup color variables
let textColor: string | undefined = undefined;
let bgColor: string | undefined = undefined;
let bgHoverColor: string = autoCellBackgroundHoverColor;
let bgHoverColor: string | undefined = undefined;
if (cellOptions.type === TableCellDisplayMode.ColorText) {
textColor = displayValue.color;
@@ -378,18 +226,14 @@ export function getCellColors(
return { textColor, bgColor, bgHoverColor };
}
/** Extracts numeric pixel value from theme spacing */
/**
* @internal
* Extracts numeric pixel value from theme spacing
*/
export const extractPixelValue = (spacing: string | number): number => {
return typeof spacing === 'number' ? spacing : parseFloat(spacing) || 0;
};
/** Converts an RGBA color to hex by blending it with a background color */
export const convertRGBAToHex = (backgroundColor: string, rgbaColor: string): string => {
const bg = tinycolor(backgroundColor);
const rgba = tinycolor(rgbaColor);
return tinycolor.mix(bg, rgba, rgba.getAlpha() * 100).toHexString();
};
/* ------------------------------- Data links ------------------------------- */
/**
* @internal
@@ -410,7 +254,7 @@ export const getCellLinks = (field: Field, rowIdx: number) => {
if (links[i].onClick) {
const origOnClick = links[i].onClick;
links[i].onClick = (event) => {
links[i].onClick = (event: MouseEvent) => {
// Allow opening in new tab
if (!(event.ctrlKey || event.metaKey || event.shiftKey)) {
event.preventDefault();
@@ -427,40 +271,48 @@ export const getCellLinks = (field: Field, rowIdx: number) => {
};
/* ----------------------------- Data grid sorting ---------------------------- */
export const handleSort = (
columnKey: string,
direction: SortDirection,
isMultiSort: boolean,
setSortColumns: React.Dispatch<React.SetStateAction<readonly SortColumn[]>>,
sortColumnsRef: React.MutableRefObject<readonly SortColumn[]>
) => {
let currentSortColumn: SortColumn | undefined;
const updatedSortColumns = sortColumnsRef.current.filter((column) => {
const isCurrentColumn = column.columnKey === columnKey;
if (isCurrentColumn) {
currentSortColumn = column;
}
return !isCurrentColumn;
});
// sorted column exists and is descending -> remove it to reset sorting
if (currentSortColumn && currentSortColumn.direction === 'DESC') {
setSortColumns(updatedSortColumns);
sortColumnsRef.current = updatedSortColumns;
} else {
// new sort column or changed direction
if (isMultiSort) {
setSortColumns([...updatedSortColumns, { columnKey, direction }]);
sortColumnsRef.current = [...updatedSortColumns, { columnKey, direction }];
} else {
setSortColumns([{ columnKey, direction }]);
sortColumnsRef.current = [{ columnKey, direction }];
}
/**
* @internal
*/
export function applySort(
rows: TableRow[],
fields: Field[],
sortColumns: SortColumn[],
columnTypes: ColumnTypes = getColumnTypes(fields),
hasNestedFrames: boolean = getIsNestedTable(fields)
): TableRow[] {
if (sortColumns.length === 0) {
return rows;
}
};
const compareRows = (a: TableRow, b: TableRow): number => {
let result = 0;
for (let i = 0; i < sortColumns.length; i++) {
const { columnKey, direction } = sortColumns[i];
const compare = getComparator(columnTypes[columnKey]);
const sortDir = direction === 'ASC' ? 1 : -1;
result = sortDir * compare(a[columnKey], b[columnKey]);
if (result !== 0) {
break;
}
}
return result;
};
// Handle nested tables
if (hasNestedFrames) {
return processNestedTableRows(rows, (parents) => [...parents].sort(compareRows));
}
// Regular sort for tables without nesting
return [...rows].sort(compareRows);
}
/* ----------------------------- Data grid mapping ---------------------------- */
/**
* @internal
*/
export const frameToRecords = (frame: DataFrame): TableRow[] => {
const fnBody = `
const rows = Array(frame.length);
@@ -473,8 +325,8 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => {
${frame.fields.map((field, fieldIdx) => `${JSON.stringify(getDisplayName(field))}: values[${fieldIdx}][i]`).join(',')}
};
rowCount += 1;
if (rows[rowCount-1]['Nested frames']){
const childFrame = rows[rowCount-1]['Nested frames'];
if (rows[rowCount-1]['__nestedFrames']){
const childFrame = rows[rowCount-1]['__nestedFrames'];
rows[rowCount] = {__depth: 1, __index: i, data: childFrame[0]}
rowCount += 1;
}
@@ -488,65 +340,68 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => {
return convert(frame);
};
export interface MapFrameToGridOptions extends TableNGProps {
columnTypes: ColumnTypes;
columnWidth: number | string;
crossFilterOrder: React.MutableRefObject<string[]>;
crossFilterRows: React.MutableRefObject<{ [key: string]: TableRow[] }>;
defaultLineHeight: number;
defaultRowHeight: number;
expandedRows: number[];
filter: FilterType;
headerCellRefs: React.MutableRefObject<Record<string, HTMLDivElement>>;
isCountRowsSet: boolean;
ctx: CanvasRenderingContext2D;
onSortByChange?: (sortBy: TableSortByFieldState[]) => void;
rows: TableRow[];
setContextMenuProps: (props: { value: string; top?: number; left?: number; mode?: TableCellInspectorMode }) => void;
setFilter: React.Dispatch<React.SetStateAction<FilterType>>;
setIsInspecting: (isInspecting: boolean) => void;
setSortColumns: React.Dispatch<React.SetStateAction<readonly SortColumn[]>>;
sortColumnsRef: React.MutableRefObject<readonly SortColumn[]>;
styles: { cell: string; cellWrapped: string; dataGrid: string };
textWraps: Record<string, boolean>;
theme: GrafanaTheme2;
showTypeIcons?: boolean;
}
/* ----------------------------- Data grid comparator ---------------------------- */
// The numeric: true option is used to sort numbers as strings correctly. It recognizes numeric sequences
// within strings and sorts numerically instead of lexicographically.
const compare = new Intl.Collator('en', { sensitivity: 'base', numeric: true }).compare;
const strCompare: Comparator = (a, b) => compare(String(a ?? ''), String(b ?? ''));
const numCompare: Comparator = (a, b) => {
if (a === b) {
return 0;
}
if (a == null) {
return -1;
}
if (b == null) {
return 1;
}
return Number(a) - Number(b);
};
const frameCompare: Comparator = (a, b) => {
// @ts-ignore The compared vals are DataFrameWithValue. the value is the rendered stat (first, last, etc.)
return (a?.value ?? 0) - (b?.value ?? 0);
};
/**
* @internal
*/
export function getComparator(sortColumnType: FieldType): Comparator {
switch (sortColumnType) {
// Handle sorting for frame type fields (sparklines)
case FieldType.frame:
return (a, b) => {
// @ts-ignore The values are DataFrameWithValue
return (a?.value ?? 0) - (b?.value ?? 0);
};
return frameCompare;
case FieldType.time:
case FieldType.number:
case FieldType.boolean:
return (a, b) => {
if (a === b) {
return 0;
}
if (a == null) {
return -1;
}
if (b == null) {
return 1;
}
return Number(a) - Number(b);
};
return numCompare;
case FieldType.string:
case FieldType.enum:
default:
return (a, b) => compare(String(a ?? ''), String(b ?? ''));
return strCompare;
}
}
type TableCellGaugeDisplayModes =
| TableCellDisplayMode.BasicGauge
| TableCellDisplayMode.GradientGauge
| TableCellDisplayMode.LcdGauge;
const TABLE_CELL_GAUGE_DISPLAY_MODES_TO_DISPLAY_MODES: Record<TableCellGaugeDisplayModes, BarGaugeDisplayMode> = {
[TableCellDisplayMode.BasicGauge]: BarGaugeDisplayMode.Basic,
[TableCellDisplayMode.GradientGauge]: BarGaugeDisplayMode.Gradient,
[TableCellDisplayMode.LcdGauge]: BarGaugeDisplayMode.Lcd,
};
type TableCellColorBackgroundDisplayModes =
| TableCellDisplayMode.ColorBackground
| TableCellDisplayMode.ColorBackgroundSolid;
const TABLE_CELL_COLOR_BACKGROUND_DISPLAY_MODES_TO_DISPLAY_MODES: Record<
TableCellColorBackgroundDisplayModes,
TableCellBackgroundDisplayMode
> = {
[TableCellDisplayMode.ColorBackground]: TableCellBackgroundDisplayMode.Gradient,
[TableCellDisplayMode.ColorBackgroundSolid]: TableCellBackgroundDisplayMode.Basic,
};
/* ---------------------------- Miscellaneous ---------------------------- */
/**
* Migrates table cell display mode to new object format.
@@ -558,49 +413,44 @@ export function getComparator(sortColumnType: FieldType): Comparator {
export function migrateTableDisplayModeToCellOptions(displayMode: TableCellDisplayMode): TableCellOptions {
switch (displayMode) {
// In the case of the gauge we move to a different option
case 'basic':
case 'gradient-gauge':
case 'lcd-gauge':
let gaugeMode = BarGaugeDisplayMode.Basic;
if (displayMode === 'gradient-gauge') {
gaugeMode = BarGaugeDisplayMode.Gradient;
} else if (displayMode === 'lcd-gauge') {
gaugeMode = BarGaugeDisplayMode.Lcd;
}
case TableCellDisplayMode.BasicGauge:
case TableCellDisplayMode.GradientGauge:
case TableCellDisplayMode.LcdGauge:
return {
type: TableCellDisplayMode.Gauge,
mode: gaugeMode,
mode: TABLE_CELL_GAUGE_DISPLAY_MODES_TO_DISPLAY_MODES[displayMode],
};
// Also true in the case of the color background
case 'color-background':
case 'color-background-solid':
let mode = TableCellBackgroundDisplayMode.Basic;
// Set the new mode field, somewhat confusingly the
// color-background mode is for gradient display
if (displayMode === 'color-background') {
mode = TableCellBackgroundDisplayMode.Gradient;
}
case TableCellDisplayMode.ColorBackground:
case TableCellDisplayMode.ColorBackgroundSolid:
return {
type: TableCellDisplayMode.ColorBackground,
mode: mode,
mode: TABLE_CELL_COLOR_BACKGROUND_DISPLAY_MODES_TO_DISPLAY_MODES[displayMode],
};
// catching a nonsense case: `displayMode`: 'custom' should pre-date the CustomCell.
// if it doesn't, we need to just nope out and return an auto cell.
case TableCellDisplayMode.Custom:
return {
type: TableCellDisplayMode.Auto,
};
default:
return {
// @ts-ignore
type: displayMode,
};
}
}
/** Returns true if the DataFrame contains nested frames */
export const getIsNestedTable = (dataFrame: DataFrame): boolean =>
dataFrame.fields.some(({ type }) => type === FieldType.nestedFrames);
/**
* @internal
* Returns true if the DataFrame contains nested frames
*/
export const getIsNestedTable = (fields: Field[]): boolean =>
fields.some(({ type }) => type === FieldType.nestedFrames);
/** Processes nested table rows */
/**
* @internal
* Processes nested table rows
*/
export const processNestedTableRows = (
rows: TableRow[],
processParents: (parents: TableRow[]) => TableRow[]
@@ -611,13 +461,13 @@ export const processNestedTableRows = (
const parentRows: TableRow[] = [];
const childRows: Map<number, TableRow> = new Map();
rows.forEach((row) => {
for (const row of rows) {
if (Number(row.__depth) === 0) {
parentRows.push(row);
} else {
childRows.set(Number(row.__index), row);
}
});
}
// Process parent rows (filter or sort)
const processedParents = processParents(parentRows);
@@ -635,6 +485,84 @@ export const processNestedTableRows = (
return result;
};
/**
* @internal
* returns the display name of a field
*/
export const getDisplayName = (field: Field): string => {
return field.state?.displayName ?? field.name;
};
/**
* @internal
* returns only fields that are not nested tables and not explicitly hidden
*/
export function getVisibleFields(fields: Field[]): Field[] {
return fields.filter((field) => field.type !== FieldType.nestedFrames && field.config.custom?.hidden !== true);
}
/**
* @internal
* returns a map of column types by display name
*/
export function getColumnTypes(fields: Field[]): ColumnTypes {
return fields.reduce<ColumnTypes>((acc, field) => {
switch (field.type) {
case FieldType.nestedFrames:
return { ...acc, ...getColumnTypes(field.values[0]?.[0]?.fields ?? []) };
default:
return { ...acc, [getDisplayName(field)]: field.type };
}
}, {});
}
/**
* @internal
* calculates the width of each field, with the following logic:
* 1. manual sizing minWidth is hard-coded to 50px, we set this in RDG since it enforces the hard limit correctly
* 2. if minWidth is configured in fieldConfig (or defaults to 150), it serves as the bottom of the auto-size clamp
*/
export function computeColWidths(fields: Field[], availWidth: number) {
let autoCount = 0;
let definedWidth = 0;
return (
fields
// first pass to add up how many fields have pre-defined widths and what that width totals to.
.map((field) => {
const width: number = field.config.custom?.width ?? 0;
if (width === 0) {
autoCount++;
} else {
definedWidth += width;
}
return width;
})
// second pass once `autoCount` and `definedWidth` are known.
.map(
(width, i) =>
width ||
Math.max(fields[i].config.custom?.minWidth ?? COLUMN.DEFAULT_WIDTH, (availWidth - definedWidth) / autoCount)
)
);
}
/**
* @internal
* if applyToRow is true in any field, return a function that gets the row background color
*/
export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowIndex: number) => CellColors) | void {
for (const field of fields) {
const cellOptions = getCellOptions(field);
const fieldDisplay = field.display;
if (
fieldDisplay !== undefined &&
cellOptions.type === TableCellDisplayMode.ColorBackground &&
cellOptions.applyToRow === true
) {
return (rowIndex: number) => getCellColors(theme, cellOptions, fieldDisplay(field.values[rowIndex]));
}
}
}
+1 -1
View File
@@ -418,7 +418,7 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) {
})
}
func TestDashboardAPIEndpoint(t *testing.T) {
func TestIntegrationDashboardAPIEndpoint(t *testing.T) {
t.Run("Given two dashboards with the same title in different folders", func(t *testing.T) {
dashOne := dashboards.NewDashboard("dash")
dashOne.ID = 2
+4 -4
View File
@@ -113,7 +113,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
return m, hs
}
func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
func TestIntegrationHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
type buildInfo struct {
Version string `json:"version"`
Commit string `json:"commit"`
@@ -182,7 +182,7 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
}
}
func TestHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.T) {
func TestIntegrationHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.T) {
type settings struct {
PluginsCDNBaseURL string `json:"pluginsCDNBaseURL"`
}
@@ -232,7 +232,7 @@ func TestHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.T) {
}
}
func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
type settings struct {
Apps map[string]*plugins.AppDTO `json:"apps"`
}
@@ -462,7 +462,7 @@ func newAppSettings(id string, enabled bool) map[string]*pluginsettings.DTO {
}
}
func TestHTTPServer_GetFrontendSettings_translations(t *testing.T) {
func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) {
type settings struct {
Datasources map[string]plugins.DataSourceDTO `json:"datasources"`
Panels map[string]*plugins.PanelDTO `json:"panels"`
+3 -3
View File
@@ -292,16 +292,16 @@ func (hs *HTTPServer) DeleteOrgByID(c *contextmodel.ReqContext) response.Respons
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
// before deleting an org, check if user does not belong to the current org
// before deleting an org, check if user is not active in the org
if c.GetOrgID() == orgID {
return response.Error(http.StatusBadRequest, "Can not delete org for current user", nil)
return response.Error(http.StatusBadRequest, "Cannot delete your active organization. Please switch to a different organization first.", nil)
}
if err := hs.orgDeletionService.Delete(c.Req.Context(), &org.DeleteOrgCommand{ID: orgID}); err != nil {
if errors.Is(err, org.ErrOrgNotFound) {
return response.Error(http.StatusNotFound, "Failed to delete organization. ID not found", nil)
}
return response.Error(http.StatusInternalServerError, "Failed to update organization", err)
return response.Error(http.StatusInternalServerError, "Failed to delete organization", err)
}
return response.Success("Organization deleted")
}

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