Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs
This commit is contained in:
+3
-9
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}"
|
||||
@@ -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
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
@@ -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** 
|
||||
The bar is drawn before the point. The point is placed on the trailing corner of the bar.
|
||||
- **Center** 
|
||||
The bar is drawn around the point. The point is placed in the center of the bar. This is the default.
|
||||
- **After** 
|
||||
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
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -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:
|
||||
|
||||
<!--
|
||||
|
||||
 -->
|
||||

|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||

|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||

|
||||

|
||||
|
||||
##### 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.
|
||||
|
||||

|
||||
{{< 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:
|
||||
|
||||

|
||||
|
||||
@@ -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
@@ -91,7 +91,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
url: '${HOST}/org/teams',
|
||||
threshold: 0,
|
||||
threshold: 1,
|
||||
},
|
||||
{
|
||||
url: '${HOST}/plugins',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user