From 125a8a41dd6af4b5da86094d893d3bac429a6e6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:51:58 +0000 Subject: [PATCH 001/174] Update dependency nx to v19.8.2 --- package.json | 2 +- yarn.lock | 103 +++++++++++++++++++++++++-------------------------- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index e2b7a7751f6..29d395637b3 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,7 @@ "mutationobserver-shim": "0.3.7", "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", - "nx": "19.8.0", + "nx": "19.8.2", "postcss": "8.4.47", "postcss-loader": "8.1.1", "postcss-reporter": "7.1.0", diff --git a/yarn.lock b/yarn.lock index 6b9c17569b0..c553f58b6ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5565,15 +5565,15 @@ __metadata: languageName: node linkType: hard -"@nrwl/tao@npm:19.8.0": - version: 19.8.0 - resolution: "@nrwl/tao@npm:19.8.0" +"@nrwl/tao@npm:19.8.2": + version: 19.8.2 + resolution: "@nrwl/tao@npm:19.8.2" dependencies: - nx: "npm:19.8.0" + nx: "npm:19.8.2" tslib: "npm:^2.3.0" bin: tao: index.js - checksum: 10/7dc94c5baf1c4cc3d9f2eba8c014cddebac4442a787ff5b47e9679eeb49250c76f372f2012c371323f06a7a3c12984b88c6bcc6add9bbba60ab83ed434a9347e + checksum: 10/5db4f551d302efba29c8ff3a0c4b214c6f0a05da2fe6fc4837214386088260f228b821907ed106501b7f93bcce85da7064f0883a6b06133d303c3e1c507ae7e2 languageName: node linkType: hard @@ -5596,72 +5596,72 @@ __metadata: languageName: node linkType: hard -"@nx/nx-darwin-arm64@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-darwin-arm64@npm:19.8.0" +"@nx/nx-darwin-arm64@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-darwin-arm64@npm:19.8.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@nx/nx-darwin-x64@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-darwin-x64@npm:19.8.0" +"@nx/nx-darwin-x64@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-darwin-x64@npm:19.8.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@nx/nx-freebsd-x64@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-freebsd-x64@npm:19.8.0" +"@nx/nx-freebsd-x64@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-freebsd-x64@npm:19.8.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@nx/nx-linux-arm-gnueabihf@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-linux-arm-gnueabihf@npm:19.8.0" +"@nx/nx-linux-arm-gnueabihf@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-linux-arm-gnueabihf@npm:19.8.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@nx/nx-linux-arm64-gnu@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-linux-arm64-gnu@npm:19.8.0" +"@nx/nx-linux-arm64-gnu@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-linux-arm64-gnu@npm:19.8.2" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@nx/nx-linux-arm64-musl@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-linux-arm64-musl@npm:19.8.0" +"@nx/nx-linux-arm64-musl@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-linux-arm64-musl@npm:19.8.2" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@nx/nx-linux-x64-gnu@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-linux-x64-gnu@npm:19.8.0" +"@nx/nx-linux-x64-gnu@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-linux-x64-gnu@npm:19.8.2" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@nx/nx-linux-x64-musl@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-linux-x64-musl@npm:19.8.0" +"@nx/nx-linux-x64-musl@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-linux-x64-musl@npm:19.8.2" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@nx/nx-win32-arm64-msvc@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-win32-arm64-msvc@npm:19.8.0" +"@nx/nx-win32-arm64-msvc@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-win32-arm64-msvc@npm:19.8.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@nx/nx-win32-x64-msvc@npm:19.8.0": - version: 19.8.0 - resolution: "@nx/nx-win32-x64-msvc@npm:19.8.0" +"@nx/nx-win32-x64-msvc@npm:19.8.2": + version: 19.8.2 + resolution: "@nx/nx-win32-x64-msvc@npm:19.8.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -19051,7 +19051,7 @@ __metadata: ngtemplate-loader: "npm:2.1.0" node-forge: "npm:^1.3.1" node-notifier: "npm:10.0.1" - nx: "npm:19.8.0" + nx: "npm:19.8.2" ol: "npm:7.4.0" ol-ext: "npm:4.0.23" pluralize: "npm:^8.0.0" @@ -24648,22 +24648,22 @@ __metadata: languageName: node linkType: hard -"nx@npm:19.8.0, nx@npm:>=17.1.2 < 20": - version: 19.8.0 - resolution: "nx@npm:19.8.0" +"nx@npm:19.8.2, nx@npm:>=17.1.2 < 20": + version: 19.8.2 + resolution: "nx@npm:19.8.2" dependencies: "@napi-rs/wasm-runtime": "npm:0.2.4" - "@nrwl/tao": "npm:19.8.0" - "@nx/nx-darwin-arm64": "npm:19.8.0" - "@nx/nx-darwin-x64": "npm:19.8.0" - "@nx/nx-freebsd-x64": "npm:19.8.0" - "@nx/nx-linux-arm-gnueabihf": "npm:19.8.0" - "@nx/nx-linux-arm64-gnu": "npm:19.8.0" - "@nx/nx-linux-arm64-musl": "npm:19.8.0" - "@nx/nx-linux-x64-gnu": "npm:19.8.0" - "@nx/nx-linux-x64-musl": "npm:19.8.0" - "@nx/nx-win32-arm64-msvc": "npm:19.8.0" - "@nx/nx-win32-x64-msvc": "npm:19.8.0" + "@nrwl/tao": "npm:19.8.2" + "@nx/nx-darwin-arm64": "npm:19.8.2" + "@nx/nx-darwin-x64": "npm:19.8.2" + "@nx/nx-freebsd-x64": "npm:19.8.2" + "@nx/nx-linux-arm-gnueabihf": "npm:19.8.2" + "@nx/nx-linux-arm64-gnu": "npm:19.8.2" + "@nx/nx-linux-arm64-musl": "npm:19.8.2" + "@nx/nx-linux-x64-gnu": "npm:19.8.2" + "@nx/nx-linux-x64-musl": "npm:19.8.2" + "@nx/nx-win32-arm64-msvc": "npm:19.8.2" + "@nx/nx-win32-x64-msvc": "npm:19.8.2" "@yarnpkg/lockfile": "npm:^1.1.0" "@yarnpkg/parsers": "npm:3.0.0-rc.46" "@zkochan/js-yaml": "npm:0.0.7" @@ -24678,7 +24678,6 @@ __metadata: figures: "npm:3.2.0" flat: "npm:^5.0.2" front-matter: "npm:^4.0.2" - fs-extra: "npm:^11.1.0" ignore: "npm:^5.0.4" jest-diff: "npm:^29.4.1" jsonc-parser: "npm:3.2.0" @@ -24729,7 +24728,7 @@ __metadata: bin: nx: bin/nx.js nx-cloud: bin/nx-cloud.js - checksum: 10/b67acf6e75b28dbd5eb97b945c4bc770e3d53b0b4098964385cb4e974857eb8e7a60953071150b8dbfc10c8aff2fc54feda5cf1d6ae21bd0a8b7a9ba4e477055 + checksum: 10/2caec97e60730256bf48d0a2d28f0a785253ef8c832a5372342c01fa58f5416baac0ec8bae71db3d40aa98b1f43bc9aa416e425d4a157cc1b410534f05c63e42 languageName: node linkType: hard From 6951feff88df6cdcea2ba9273cfa632f6f595c07 Mon Sep 17 00:00:00 2001 From: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:32:43 -0400 Subject: [PATCH 002/174] [DOC] Add videos to Explore docs (#93847) * Add videos to Explore docs * Apply suggestions from code review --- docs/sources/datasources/tempo/query-editor/_index.md | 9 +++++++++ docs/sources/explore/simplified-exploration/_index.md | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/docs/sources/datasources/tempo/query-editor/_index.md b/docs/sources/datasources/tempo/query-editor/_index.md index 28dd3659f63..d2f37e700f7 100644 --- a/docs/sources/datasources/tempo/query-editor/_index.md +++ b/docs/sources/datasources/tempo/query-editor/_index.md @@ -46,6 +46,11 @@ refs: destination: /docs/grafana//explore/explore-inspector/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//explore/explore-inspector/ + explore-traces-app: + - pattern: /docs/grafana/ + destination: /docs/grafana//explore/simplified-exploration/traces/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/simplified-exploration/traces/ --- # Query tracing data @@ -55,6 +60,10 @@ The queries use [TraceQL](/docs/tempo/latest/traceql), the query language design For general documentation on querying data sources in Grafana, refer to [Query and transform data](ref:query-transform-data). +{{< admonition type="tip" >}} +Don't know TraceQL? Try [Explore Traces](ref:explore-traces-app), an intuitive, queryless app that lets you explore your tracing data using RED metrics. +{{< /admonition >}} + ## Before you begin You can compose TraceQL queries in Grafana and Grafana Cloud using **Explore** and a Tempo data source. diff --git a/docs/sources/explore/simplified-exploration/_index.md b/docs/sources/explore/simplified-exploration/_index.md index b3b454a2c5c..2162eb693ce 100644 --- a/docs/sources/explore/simplified-exploration/_index.md +++ b/docs/sources/explore/simplified-exploration/_index.md @@ -40,4 +40,8 @@ The Grafana Explore apps are designed for effortless data exploration through in Easily explore telemetry signals with these specialized tools, tailored specifically for the Grafana databases to provide quick and accurate insights. +To learn more, read [A queryless experience for exploring metrics, logs, traces, and profiles: Introducing the Explore apps suite for Grafana](https://grafana.com/blog/2024/09/24/queryless-metrics-logs-traces-profiles/). + +{{< youtube id="MSHeWWsHaIA" >}} + {{< card-grid key="cards" type="simple" >}} From a8476080616c667c2fcd27bb6a246cd0abac8de0 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:19:19 -0500 Subject: [PATCH 003/174] CI: add promotion step for publish grafanacom (#93851) * add promotion step for publish grafanacom * publish-grafanacom depends on compile-build-cmd * use DRONE_TAG * add docstring comment for depends_on --- .drone.yml | 45 ++++++++++++++++++++++++++++++- scripts/drone/events/release.star | 11 ++++++++ scripts/drone/steps/lib.star | 9 +++---- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/.drone.yml b/.drone.yml index 09e7ec86cc7..0980c901ae2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4165,6 +4165,49 @@ volumes: path: /var/run/docker.sock name: docker --- +clone: + retries: 3 +depends_on: [] +image_pull_secrets: +- gcr +- gar +kind: pipeline +name: publish-grafanacom +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] + environment: + CGO_ENABLED: 0 + image: golang:1.23.1-alpine + name: compile-build-cmd +- commands: + - ./bin/build publish grafana-com --edition oss ${DRONE_TAG} + depends_on: + - compile-build-cmd + environment: + GCP_KEY: + from_secret: gcp_grafanauploads_base64 + GRAFANA_COM_API_KEY: + from_secret: grafana_api_key + image: grafana/grafana-ci-deploy:1.3.3 + name: publish-grafanacom +trigger: + event: + - promote + target: publish-grafanacom +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- clone: retries: 3 depends_on: @@ -6108,6 +6151,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 7335b2e56769f72716f5dac524741e423abb99eacf775fa635e59c2d658c8aee +hmac: 4a043d63a119ebf5920b600fce404d98f570504e855039cc4787c2fa1ab04669 ... diff --git a/scripts/drone/events/release.star b/scripts/drone/events/release.star index 94af7b2335c..9f7a5563e4a 100644 --- a/scripts/drone/events/release.star +++ b/scripts/drone/events/release.star @@ -242,6 +242,17 @@ def publish_packages_pipeline(): depends_on = deps, environment = {"EDITION": "oss"}, ), + pipeline( + name = "publish-grafanacom", + trigger = { + "event": ["promote"], + "target": "publish-grafanacom", + }, + steps = [ + compile_build_cmd(), + publish_grafanacom_step(ver_mode = "release", depends_on = ["compile-build-cmd"]), + ], + ), ] def publish_npm_pipelines(): diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index ccdc41ac03e..97f4bb40a70 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -727,6 +727,7 @@ def frontend_metrics_step(trigger = None): Returns: Drone step. """ + step = { "name": "publish-frontend-metrics", "image": images["node"], @@ -1178,7 +1179,7 @@ def upload_packages_step(ver_mode, trigger = None): step = dict(step, when = trigger) return step -def publish_grafanacom_step(ver_mode): +def publish_grafanacom_step(ver_mode, depends_on = ["publish-linux-packages-deb", "publish-linux-packages-rpm"]): """Publishes Grafana packages to grafana.com. Args: @@ -1186,6 +1187,7 @@ def publish_grafanacom_step(ver_mode): variable as the value for the --build-id option. TODO: is this actually used by the grafanacom subcommand? I think it might just use the environment variable directly. + depends_on: what other steps this one depends on (strings) Returns: Drone step. @@ -1203,10 +1205,7 @@ def publish_grafanacom_step(ver_mode): return { "name": "publish-grafanacom", "image": images["publish"], - "depends_on": [ - "publish-linux-packages-deb", - "publish-linux-packages-rpm", - ], + "depends_on": depends_on, "environment": { "GRAFANA_COM_API_KEY": from_secret("grafana_api_key"), "GCP_KEY": from_secret(gcp_grafanauploads_base64), From acb051b3141da5ff668a370e6c2989ee056f16ce Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Thu, 26 Sep 2024 23:27:40 +0200 Subject: [PATCH 004/174] Alerting: Fix logging for failed annotations writing. (#93856) --- pkg/services/ngalert/state/historian/annotation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index 14c7f97b251..d2353ec42d8 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -99,7 +99,7 @@ func (h *AnnotationBackend) Record(ctx context.Context, rule history_model.RuleM err := h.store.Save(ctx, panel, annotations, rule.OrgID, logger) if err != nil { - logger.Error("Failed to save history batch", len(annotations), "err", err) + logger.Error("Failed to save history batch", "samples", len(annotations), "err", err) errCh <- err return } From 378d92130d1d59453ee91bf3719e875d9c5e5d0a Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Thu, 26 Sep 2024 16:30:50 -0500 Subject: [PATCH 005/174] Alerting: Don't suppress translation errors in PointsFromFrames (#93747) * don't suppress error * reorder * re-add nilcheck --- pkg/services/ngalert/writer/prom.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/services/ngalert/writer/prom.go b/pkg/services/ngalert/writer/prom.go index 654eca9bf93..ea46b816125 100644 --- a/pkg/services/ngalert/writer/prom.go +++ b/pkg/services/ngalert/writer/prom.go @@ -66,9 +66,15 @@ func PointsFromFrames(name string, t time.Time, frames data.Frames, extraLabels points := make([]Point, 0, len(col.Refs)) for _, ref := range col.Refs { - fp, empty, _ := ref.NullableFloat64Value() - if empty || fp == nil { - return nil, fmt.Errorf("unable to read float64 value") + fp, empty, err := ref.NullableFloat64Value() + if err != nil { + return nil, fmt.Errorf("unable to read float64 value: %w", err) + } + if empty { + return nil, fmt.Errorf("empty frame") + } + if fp == nil { + return nil, fmt.Errorf("nil frame") } metric := Metric{ From dc03cc0f9a8c457f976c5a74e7eeffa67c107159 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Thu, 26 Sep 2024 18:58:21 -0600 Subject: [PATCH 006/174] CI: Bump `alpine` version (#93861) baldm0mma/bump_alpine --- .drone.yml | 86 ++++++++++++++++----------------- scripts/drone/utils/images.star | 2 +- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.drone.yml b/.drone.yml index 0980c901ae2..7f9517c3f25 100644 --- a/.drone.yml +++ b/.drone.yml @@ -18,7 +18,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -69,7 +69,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -120,7 +120,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -178,7 +178,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -279,7 +279,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -367,7 +367,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -454,7 +454,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -543,7 +543,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - mkdir -p bin @@ -648,7 +648,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.19.1 + image: alpine:3.20.2 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -868,7 +868,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ + --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.2 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -1016,7 +1016,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1205,7 +1205,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1562,7 +1562,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1638,7 +1638,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1696,7 +1696,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1762,7 +1762,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1842,7 +1842,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -1908,7 +1908,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1968,7 +1968,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - mkdir -p bin @@ -2072,7 +2072,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.19.1 + image: alpine:3.20.2 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -2328,7 +2328,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ + --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.2 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -2538,7 +2538,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2856,7 +2856,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2912,7 +2912,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2976,7 +2976,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3054,7 +3054,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -3170,7 +3170,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3398,7 +3398,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - mkdir -p bin @@ -3529,7 +3529,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - mkdir -p bin @@ -4231,7 +4231,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.2 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4348,7 +4348,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -4404,7 +4404,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4486,7 +4486,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.2 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4669,7 +4669,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.2 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4771,7 +4771,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -4825,7 +4825,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4905,7 +4905,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.2 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5052,7 +5052,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.2 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5162,7 +5162,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.2 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5365,7 +5365,7 @@ steps: name: grabpl - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.2 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -5863,7 +5863,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20-bookworm - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.19.1 + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.20.2 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM ubuntu:22.04 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM byrnedo/alpine-curl:0.1.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack @@ -5900,7 +5900,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL node:20-bookworm - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.19.1 + - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.20.2 - trivy --exit-code 1 --severity HIGH,CRITICAL ubuntu:22.04 - trivy --exit-code 1 --severity HIGH,CRITICAL byrnedo/alpine-curl:0.1.8 - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack @@ -6151,6 +6151,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 4a043d63a119ebf5920b600fce404d98f570504e855039cc4787c2fa1ab04669 +hmac: b228eb02ed1bf6bb40a3157eb269c70c1cfa9ec8f4b604b0a1b5671c224fadcc ... diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index a213e08c62c..0abe6749058 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -16,7 +16,7 @@ images = { "node_deb": "node:{}-bookworm".format(nodejs_version[:2]), "cloudsdk": "google/cloud-sdk:431.0.0", "publish": "grafana/grafana-ci-deploy:1.3.3", - "alpine": "alpine:3.19.1", + "alpine": "alpine:3.20.2", "ubuntu": "ubuntu:22.04", "curl": "byrnedo/alpine-curl:0.1.8", "plugins_slack": "plugins/slack", From b17b98aeb97afe8c40c7863900666205bd60a329 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:19:38 -0600 Subject: [PATCH 007/174] CI: Update retry_command function (#93863) * baldm0mma/update args * baldm0mma/update_args/ conflict --- .drone.yml | 18 +++++++++--------- scripts/drone/steps/lib.star | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.drone.yml b/.drone.yml index 7f9517c3f25..fdff68aae61 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3868,14 +3868,14 @@ steps: - echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list - 'echo "Step 5: Installing Grafana..."' - - for i in $(seq 1 10); do + - for i in $(seq 1 60); do - ' if apt-get update >/dev/null 2>&1 && DEBIAN_FRONTEND=noninteractive apt-get install -yq grafana=${TAG} >/dev/null 2>&1; then' - ' echo "Command succeeded on attempt $i"' - ' break' - ' else' - ' echo "Attempt $i failed"' - - ' if [ $i -eq 10 ]; then' + - ' if [ $i -eq 60 ]; then' - ' echo ''All attempts failed''' - ' exit 1' - ' fi' @@ -3918,13 +3918,13 @@ steps: - dnf list available grafana-${TAG} - if [ $? -eq 0 ]; then - ' echo "Grafana package found in repository. Installing from repo..."' - - for i in $(seq 1 5); do + - for i in $(seq 1 60); do - ' if dnf install -y --nogpgcheck grafana-${TAG} >/dev/null 2>&1; then' - ' echo "Command succeeded on attempt $i"' - ' break' - ' else' - ' echo "Attempt $i failed"' - - ' if [ $i -eq 5 ]; then' + - ' if [ $i -eq 60 ]; then' - ' echo ''All attempts failed''' - ' exit 1' - ' fi' @@ -4045,14 +4045,14 @@ steps: - echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list - 'echo "Step 5: Installing Grafana..."' - - for i in $(seq 1 10); do + - for i in $(seq 1 60); do - ' if apt-get update >/dev/null 2>&1 && DEBIAN_FRONTEND=noninteractive apt-get install -yq grafana=${TAG} >/dev/null 2>&1; then' - ' echo "Command succeeded on attempt $i"' - ' break' - ' else' - ' echo "Attempt $i failed"' - - ' if [ $i -eq 10 ]; then' + - ' if [ $i -eq 60 ]; then' - ' echo ''All attempts failed''' - ' exit 1' - ' fi' @@ -4096,13 +4096,13 @@ steps: - dnf list available grafana-${TAG} - if [ $? -eq 0 ]; then - ' echo "Grafana package found in repository. Installing from repo..."' - - for i in $(seq 1 5); do + - for i in $(seq 1 60); do - ' if dnf install -y --nogpgcheck grafana-${TAG} >/dev/null 2>&1; then' - ' echo "Command succeeded on attempt $i"' - ' break' - ' else' - ' echo "Attempt $i failed"' - - ' if [ $i -eq 5 ]; then' + - ' if [ $i -eq 60 ]; then' - ' echo ''All attempts failed''' - ' exit 1' - ' fi' @@ -6151,6 +6151,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: b228eb02ed1bf6bb40a3157eb269c70c1cfa9ec8f4b604b0a1b5671c224fadcc +hmac: 457587c64520712c793e9b743a3ab0e4988075123f78ee2a3d89aa373dc9bf30 ... diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 97f4bb40a70..500b3ab0305 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1303,7 +1303,7 @@ def verify_linux_DEB_packages_step(depends_on = []): 'echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list', 'echo "Step 5: Installing Grafana..."', # The packages take a bit of time to propogate within the repo. This retry will check their availability within 10 minutes. - ] + retry_command(install_command, attempts = 10) + [ + ] + retry_command(install_command) + [ 'echo "Step 6: Verifying Grafana installation..."', 'if dpkg -s grafana | grep -q "Version: ${TAG}"; then', ' echo "Successfully verified Grafana version ${TAG}"', @@ -1329,7 +1329,7 @@ def verify_linux_RPM_packages_step(depends_on = []): "sslcacert=/etc/pki/tls/certs/ca-bundle.crt\n" ) - repo_install_command = "dnf install -y --nogpgcheck grafana-${TAG} >/dev/null 2>&1" + install_command = "dnf install -y --nogpgcheck grafana-${TAG} >/dev/null 2>&1" return { "name": "verify-linux-RPM-packages", @@ -1348,7 +1348,7 @@ def verify_linux_RPM_packages_step(depends_on = []): "dnf list available grafana-${TAG}", "if [ $? -eq 0 ]; then", ' echo "Grafana package found in repository. Installing from repo..."', - ] + retry_command(repo_install_command, attempts = 5) + [ + ] + retry_command(install_command) + [ ' echo "Verifying GPG key..."', " rpm --import https://rpm.grafana.com/gpg.key", " rpm -qa gpg-pubkey* | xargs rpm -qi | grep -i grafana", From dc1670ed9a99ee627dca228a78e99ef387430b6b Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:50:28 -0600 Subject: [PATCH 008/174] CI: Bump alpine version (#93865) * baldm0mma/up_alpine/ update alpine * baldm0mma/resolve commits --- .drone.yml | 86 ++++++++++++++++----------------- scripts/drone/utils/images.star | 2 +- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.drone.yml b/.drone.yml index fdff68aae61..ba023a80be6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -18,7 +18,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -69,7 +69,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -120,7 +120,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -178,7 +178,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -279,7 +279,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -367,7 +367,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -454,7 +454,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -543,7 +543,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - mkdir -p bin @@ -648,7 +648,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.20.2 + image: alpine:3.20.3 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -868,7 +868,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.2 --tag-format='{{ + --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.3 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -1016,7 +1016,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1205,7 +1205,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1562,7 +1562,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1638,7 +1638,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1696,7 +1696,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1762,7 +1762,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1842,7 +1842,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -1908,7 +1908,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1968,7 +1968,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - mkdir -p bin @@ -2072,7 +2072,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.20.2 + image: alpine:3.20.3 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -2328,7 +2328,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.2 --tag-format='{{ + --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.3 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -2538,7 +2538,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2856,7 +2856,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2912,7 +2912,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2976,7 +2976,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3054,7 +3054,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -3170,7 +3170,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3398,7 +3398,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - mkdir -p bin @@ -3529,7 +3529,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - mkdir -p bin @@ -4231,7 +4231,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.2 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4348,7 +4348,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -4404,7 +4404,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4486,7 +4486,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.2 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4669,7 +4669,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.2 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4771,7 +4771,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -4825,7 +4825,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4905,7 +4905,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.2 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5052,7 +5052,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.2 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5162,7 +5162,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.2 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5365,7 +5365,7 @@ steps: name: grabpl - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.2 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -5863,7 +5863,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20-bookworm - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.20.2 + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.20.3 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM ubuntu:22.04 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM byrnedo/alpine-curl:0.1.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack @@ -5900,7 +5900,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL node:20-bookworm - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.20.2 + - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.20.3 - trivy --exit-code 1 --severity HIGH,CRITICAL ubuntu:22.04 - trivy --exit-code 1 --severity HIGH,CRITICAL byrnedo/alpine-curl:0.1.8 - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack @@ -6151,6 +6151,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 457587c64520712c793e9b743a3ab0e4988075123f78ee2a3d89aa373dc9bf30 +hmac: 495b2466a038f0e208edc8cf65c78edc4795a380d2f1c1ff31d10259e4338431 ... diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index 0abe6749058..c087127e664 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -16,7 +16,7 @@ images = { "node_deb": "node:{}-bookworm".format(nodejs_version[:2]), "cloudsdk": "google/cloud-sdk:431.0.0", "publish": "grafana/grafana-ci-deploy:1.3.3", - "alpine": "alpine:3.20.2", + "alpine": "alpine:3.20.3", "ubuntu": "ubuntu:22.04", "curl": "byrnedo/alpine-curl:0.1.8", "plugins_slack": "plugins/slack", From e67279663244d12b7194996f27d5891cd29137ca Mon Sep 17 00:00:00 2001 From: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:51:01 -0400 Subject: [PATCH 009/174] CloudMigrations: Fix OrderBy clause in GetSnapshotList sql handler (#93857) fix order_by clause in list query --- pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index 86dea62f7d2..33e3b392287 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -278,7 +278,7 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li offset := (query.Page - 1) * query.Limit sess.Limit(query.Limit, offset) } - sess.OrderBy("created DESC") + sess.OrderBy("cloud_migration_snapshot.created DESC") return sess.Find(&snapshots, &cloudmigration.CloudMigrationSnapshot{ SessionUID: query.SessionUID, }) From 87c81825b702941e47560e4edef3c8c9da09b8ec Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 27 Sep 2024 09:04:35 +0300 Subject: [PATCH 010/174] K8s: Move standalone apiserver CLI to enterprise (#93799) --- .github/CODEOWNERS | 1 - pkg/cmd/grafana-server/commands/buildinfo.go | 5 +- pkg/cmd/grafana-server/commands/cli.go | 19 +- pkg/cmd/grafana-server/commands/target.go | 10 +- pkg/cmd/grafana/apiserver/apiserver.md | 60 ------ pkg/cmd/grafana/apiserver/cmd.go | 157 -------------- .../deploy/aggregator-test/apiservice.yaml | 15 -- .../deploy/aggregator-test/externalname.yaml | 8 - .../deploy/aggregator-test/kustomization.yaml | 3 - pkg/cmd/grafana/apiserver/server.go | 201 ------------------ .../apiserver/testdata/certificates/README.md | 6 - .../apiserver/testdata/certificates/ca.crt | 24 --- .../apiserver/testdata/certificates/ca.key | 28 --- .../testdata/certificates/client.crt | 27 --- .../testdata/certificates/client.csr | 27 --- .../testdata/certificates/client.key | 52 ----- .../testdata/certificates/server.crt | 28 --- .../testdata/certificates/server.csr | 29 --- .../testdata/certificates/server.key | 52 ----- pkg/cmd/grafana/main.go | 49 +++-- pkg/extensions/main.go | 12 +- pkg/server/wire.go | 2 +- pkg/server/wireexts_oss.go | 2 +- pkg/services/apiserver/standalone/factory.go | 170 ++------------- 24 files changed, 63 insertions(+), 924 deletions(-) delete mode 100644 pkg/cmd/grafana/apiserver/apiserver.md delete mode 100644 pkg/cmd/grafana/apiserver/cmd.go delete mode 100644 pkg/cmd/grafana/apiserver/deploy/aggregator-test/apiservice.yaml delete mode 100644 pkg/cmd/grafana/apiserver/deploy/aggregator-test/externalname.yaml delete mode 100644 pkg/cmd/grafana/apiserver/deploy/aggregator-test/kustomization.yaml delete mode 100644 pkg/cmd/grafana/apiserver/server.go delete mode 100644 pkg/cmd/grafana/apiserver/testdata/certificates/README.md delete mode 100755 pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt delete mode 100755 pkg/cmd/grafana/apiserver/testdata/certificates/ca.key delete mode 100755 pkg/cmd/grafana/apiserver/testdata/certificates/client.crt delete mode 100755 pkg/cmd/grafana/apiserver/testdata/certificates/client.csr delete mode 100755 pkg/cmd/grafana/apiserver/testdata/certificates/client.key delete mode 100755 pkg/cmd/grafana/apiserver/testdata/certificates/server.crt delete mode 100755 pkg/cmd/grafana/apiserver/testdata/certificates/server.csr delete mode 100755 pkg/cmd/grafana/apiserver/testdata/certificates/server.key diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c30c40ee279..f00c143ce13 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -80,7 +80,6 @@ /pkg/apis/query @grafana/grafana-datasources-core-services /pkg/bus/ @grafana/grafana-search-and-storage /pkg/cmd/ @grafana/grafana-backend-group -/pkg/cmd/grafana/apiserver @grafana/grafana-app-platform-squad /pkg/components/apikeygen/ @grafana/identity-squad /pkg/components/satokengen/ @grafana/identity-squad /pkg/components/dashdiffs/ @grafana/grafana-app-platform-squad diff --git a/pkg/cmd/grafana-server/commands/buildinfo.go b/pkg/cmd/grafana-server/commands/buildinfo.go index 1051ca520bc..fe694d5e3b1 100644 --- a/pkg/cmd/grafana-server/commands/buildinfo.go +++ b/pkg/cmd/grafana-server/commands/buildinfo.go @@ -5,10 +5,11 @@ import ( "time" "github.com/grafana/grafana/pkg/extensions" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/setting" ) -func getBuildstamp(opts ServerOptions) int64 { +func getBuildstamp(opts standalone.BuildInfo) int64 { buildstampInt64, err := strconv.ParseInt(opts.BuildStamp, 10, 64) if err != nil || buildstampInt64 == 0 { buildstampInt64 = time.Now().Unix() @@ -16,7 +17,7 @@ func getBuildstamp(opts ServerOptions) int64 { return buildstampInt64 } -func SetBuildInfo(opts ServerOptions) { +func SetBuildInfo(opts standalone.BuildInfo) { setting.BuildVersion = opts.Version setting.BuildCommit = opts.Commit setting.EnterpriseBuildCommit = opts.EnterpriseCommit diff --git a/pkg/cmd/grafana-server/commands/cli.go b/pkg/cmd/grafana-server/commands/cli.go index 6e82a6362b4..5f438aad2fd 100644 --- a/pkg/cmd/grafana-server/commands/cli.go +++ b/pkg/cmd/grafana-server/commands/cli.go @@ -21,38 +21,29 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/process" "github.com/grafana/grafana/pkg/server" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/setting" ) -type ServerOptions struct { - Version string - Commit string - EnterpriseCommit string - BuildBranch string - BuildStamp string - Context *cli.Context -} - func ServerCommand(version, commit, enterpriseCommit, buildBranch, buildstamp string) *cli.Command { return &cli.Command{ Name: "server", Usage: "run the grafana server", Flags: commonFlags, Action: func(context *cli.Context) error { - return RunServer(ServerOptions{ + return RunServer(standalone.BuildInfo{ Version: version, Commit: commit, EnterpriseCommit: enterpriseCommit, BuildBranch: buildBranch, BuildStamp: buildstamp, - Context: context, - }) + }, context) }, Subcommands: []*cli.Command{TargetCommand(version, commit, buildBranch, buildstamp)}, } } -func RunServer(opts ServerOptions) error { +func RunServer(opts standalone.BuildInfo, cli *cli.Context) error { if Version || VerboseVersion { if opts.EnterpriseCommit != gcli.DefaultCommitValue && opts.EnterpriseCommit != "" { fmt.Printf("Version %s (commit: %s, branch: %s, enterprise-commit: %s)\n", opts.Version, opts.Commit, opts.BuildBranch, opts.EnterpriseCommit) @@ -106,7 +97,7 @@ func RunServer(opts ServerOptions) error { Config: ConfigFile, HomePath: HomePath, // tailing arguments have precedence over the options string - Args: append(configOptions, opts.Context.Args().Slice()...), + Args: append(configOptions, cli.Args().Slice()...), }) if err != nil { return err diff --git a/pkg/cmd/grafana-server/commands/target.go b/pkg/cmd/grafana-server/commands/target.go index 25df8f70e04..39f322396ac 100644 --- a/pkg/cmd/grafana-server/commands/target.go +++ b/pkg/cmd/grafana-server/commands/target.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/server" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/setting" ) @@ -22,18 +23,17 @@ func TargetCommand(version, commit, buildBranch, buildstamp string) *cli.Command Usage: "target specific grafana dskit services", Flags: commonFlags, Action: func(context *cli.Context) error { - return RunTargetServer(ServerOptions{ + return RunTargetServer(standalone.BuildInfo{ Version: version, Commit: commit, BuildBranch: buildBranch, BuildStamp: buildstamp, - Context: context, - }) + }, context) }, } } -func RunTargetServer(opts ServerOptions) error { +func RunTargetServer(opts standalone.BuildInfo, cli *cli.Context) error { if Version || VerboseVersion { fmt.Printf("Version %s (commit: %s, branch: %s)\n", opts.Version, opts.Commit, opts.BuildBranch) if VerboseVersion { @@ -83,7 +83,7 @@ func RunTargetServer(opts ServerOptions) error { Config: ConfigFile, HomePath: HomePath, // tailing arguments have precedence over the options string - Args: append(configOptions, opts.Context.Args().Slice()...), + Args: append(configOptions, cli.Args().Slice()...), }) if err != nil { return err diff --git a/pkg/cmd/grafana/apiserver/apiserver.md b/pkg/cmd/grafana/apiserver/apiserver.md deleted file mode 100644 index 41fe0362570..00000000000 --- a/pkg/cmd/grafana/apiserver/apiserver.md +++ /dev/null @@ -1,60 +0,0 @@ -# grafana apiserver (standalone) - -The example-apiserver closely resembles the -[sample-apiserver](https://github.com/kubernetes/sample-apiserver/tree/master) project in code and thus -allows the same -[CLI flags](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/) as kube-apiserver. -It is currently used for testing our deployment pipelines for aggregated servers. You can optionally omit the -aggregation path altogether and just run this example apiserver as a standalone process. - -## Standalone Mode - -### Usage - -For setting `--grafana.authn.signing-keys-url`, Grafana must be run with `idForwarding = true` while also ensuring -you have logged in to the instance at least once. - -```shell -go run ./pkg/cmd/grafana apiserver \ - --runtime-config=example.grafana.app/v0alpha1=true \ - --grafana-apiserver-dev-mode \ - --grafana.authn.signing-keys-url="http://localhost:3000/api/signing-keys/keys" \ - --verbosity 10 \ - --secure-port 7443 -``` - -### Verify that all works - -In dev mode, the standalone server's loopback kubeconfig is written to `./data/grafana-apiserver/apiserver.kubeconfig`. - -```shell -export KUBECONFIG=./data/grafana-apiserver/apiserver.kubeconfig - -kubectl api-resources -NAME SHORTNAMES APIVERSION NAMESPACED KIND -dummy example.grafana.app/v0alpha1 true DummyResource -runtime example.grafana.app/v0alpha1 false RuntimeInfo -``` - -### Observability - -Logs, metrics and traces are supported. See `--grafana.log.*`, `--grafana.metrics.*` and `--grafana.tracing.*` flags for details. - -```shell -go run ./pkg/cmd/grafana apiserver \ - --runtime-config=example.grafana.app/v0alpha1=true \ - --help -``` - -For example, to enable debug logs, metrics and traces (using [self-instrumentation](../../../../devenv/docker/blocks/self-instrumentation/readme.md)) use the following: - -```shell -go run ./pkg/cmd/grafana apiserver \ - --runtime-config=example.grafana.app/v0alpha1=true \ - --secure-port=7443 \ - --grafana.log.level=debug \ - --verbosity=10 \ - --grafana.metrics.enable \ - --grafana.tracing.jaeger.address=http://localhost:14268/api/traces \ - --grafana.tracing.sampler-param=1 -``` diff --git a/pkg/cmd/grafana/apiserver/cmd.go b/pkg/cmd/grafana/apiserver/cmd.go deleted file mode 100644 index a187772550e..00000000000 --- a/pkg/cmd/grafana/apiserver/cmd.go +++ /dev/null @@ -1,157 +0,0 @@ -package apiserver - -import ( - "context" - "os" - "sync" - - "github.com/spf13/cobra" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" - genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/component-base/cli" - - "github.com/grafana/grafana/pkg/cmd/grafana-server/commands" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/server" - "github.com/grafana/grafana/pkg/services/apiserver/standalone" -) - -func newCommandStartStandaloneAPIServer(o *APIServerOptions, stopCh <-chan struct{}) *cobra.Command { - devAcknowledgementNotice := "The apiserver command is in heavy development. The entire setup is subject to change without notice" - runtimeConfig := "" - - factory, err := server.InitializeAPIServerFactory() - if err != nil { - return nil - } - o.factory = factory - - cmd := &cobra.Command{ - Use: "apiserver [api group(s)]", - Short: "Run the grafana apiserver", - Long: "Run a standalone kubernetes based apiserver that can be aggregated by a root apiserver. " + - devAcknowledgementNotice, - Example: "grafana apiserver --runtime-config=example.grafana.app/v0alpha1=true", - RunE: func(c *cobra.Command, args []string) error { - if err := log.SetupConsoleLogger("debug"); err != nil { - return nil - } - - if err := o.Validate(); err != nil { - return err - } - - runtime, err := standalone.ReadRuntimeConfig(runtimeConfig) - if err != nil { - return err - } - apis, err := o.factory.GetEnabled(runtime) - if err != nil { - return err - } - - // Currently TracingOptions.ApplyTo, which will configure/initialize tracing, - // happens after loadAPIGroupBuilders. Hack to workaround this for now to allow - // the tracer to be initialized at a later stage, when tracer is available. - // TODO: Fix so that TracingOptions.ApplyTo happens before or during loadAPIGroupBuilders. - tracer := newLateInitializedTracingService() - - ctx, cancel := context.WithCancel(c.Context()) - go func() { - <-stopCh - cancel() - }() - - // Load each group from the args - if err := o.loadAPIGroupBuilders(ctx, tracer, apis); err != nil { - return err - } - - // Finish the config (a noop for now) - if err := o.Complete(); err != nil { - return err - } - - // o.Config(tracer) definitely needs to happen before we override the tracer below - // using tracer.InitTracer with the real tracer - config, err := o.Config(tracer) - if err != nil { - return err - } - - if o.Options.TracingOptions.TracingService != nil { - tracer.InitTracer(o.Options.TracingOptions.TracingService) - } - - defer o.factory.Shutdown() - - if err := o.RunAPIServer(ctx, config); err != nil { - return err - } - - return nil - }, - } - - cmd.Flags().StringVar(&runtimeConfig, "runtime-config", "", "A set of key=value pairs that enable or disable built-in APIs.") - - o.AddFlags(cmd.Flags()) - - return cmd -} - -func RunCLI(opts commands.ServerOptions) int { - stopCh := genericapiserver.SetupSignalHandler() - - commands.SetBuildInfo(opts) - - options := newAPIServerOptions(os.Stdout, os.Stderr) - cmd := newCommandStartStandaloneAPIServer(options, stopCh) - - return cli.Run(cmd) -} - -type lateInitializedTracingProvider struct { - trace.TracerProvider - tracer *lateInitializedTracingService -} - -func (tp lateInitializedTracingProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer { - return tp.tracer.getTracer() -} - -type lateInitializedTracingService struct { - tracing.Tracer - mutex sync.RWMutex -} - -func newLateInitializedTracingService() *lateInitializedTracingService { - ts := &lateInitializedTracingService{ - Tracer: tracing.NewNoopTracerService(), - } - - tp := &lateInitializedTracingProvider{ - tracer: ts, - } - - otel.SetTracerProvider(tp) - - return ts -} - -func (s *lateInitializedTracingService) getTracer() tracing.Tracer { - s.mutex.RLock() - t := s.Tracer - s.mutex.RUnlock() - return t -} - -func (s *lateInitializedTracingService) InitTracer(tracer *tracing.TracingService) { - s.mutex.Lock() - s.Tracer = tracer - s.mutex.Unlock() -} - -var _ tracing.Tracer = &lateInitializedTracingService{} diff --git a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/apiservice.yaml b/pkg/cmd/grafana/apiserver/deploy/aggregator-test/apiservice.yaml deleted file mode 100644 index 65cc2a5884a..00000000000 --- a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/apiservice.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: apiregistration.k8s.io/v1 -kind: APIService -metadata: - name: v0alpha1.example.grafana.app -spec: - version: v0alpha1 - insecureSkipTLSVerify: true - group: example.grafana.app - groupPriorityMinimum: 1000 - versionPriority: 15 - service: - name: example-apiserver - namespace: grafana - port: 7443 diff --git a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/externalname.yaml b/pkg/cmd/grafana/apiserver/deploy/aggregator-test/externalname.yaml deleted file mode 100644 index 75009779106..00000000000 --- a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/externalname.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: service.grafana.app/v0alpha1 -kind: ExternalName -metadata: - name: example-apiserver - namespace: grafana -spec: - host: localhost - diff --git a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/kustomization.yaml b/pkg/cmd/grafana/apiserver/deploy/aggregator-test/kustomization.yaml deleted file mode 100644 index 15829bf3c27..00000000000 --- a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/kustomization.yaml +++ /dev/null @@ -1,3 +0,0 @@ -resources: - - apiservice.yaml - - externalname.yaml diff --git a/pkg/cmd/grafana/apiserver/server.go b/pkg/cmd/grafana/apiserver/server.go deleted file mode 100644 index 96bdc935f8a..00000000000 --- a/pkg/cmd/grafana/apiserver/server.go +++ /dev/null @@ -1,201 +0,0 @@ -package apiserver - -import ( - "context" - "fmt" - "io" - "net" - "path" - - "github.com/grafana/pyroscope-go/godeltaprof/http/pprof" - "github.com/spf13/pflag" - "k8s.io/apimachinery/pkg/runtime/schema" - utilerrors "k8s.io/apimachinery/pkg/util/errors" - genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/apiserver/pkg/server/mux" - "k8s.io/client-go/tools/clientcmd" - netutils "k8s.io/utils/net" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - grafanaAPIServer "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/apiserver/standalone" - standaloneoptions "github.com/grafana/grafana/pkg/services/apiserver/standalone/options" - "github.com/grafana/grafana/pkg/services/apiserver/utils" - "github.com/grafana/grafana/pkg/setting" -) - -const ( - dataPath = "data/grafana-apiserver" // same as grafana core -) - -// APIServerOptions contains the state for the apiserver -type APIServerOptions struct { - factory standalone.APIServerFactory - builders []builder.APIGroupBuilder - Options *standaloneoptions.Options - AlternateDNS []string - logger log.Logger - - StdOut io.Writer - StdErr io.Writer -} - -func newAPIServerOptions(out, errOut io.Writer) *APIServerOptions { - logger := log.New("grafana-apiserver") - - return &APIServerOptions{ - logger: logger, - StdOut: out, - StdErr: errOut, - Options: standaloneoptions.New(logger, grafanaAPIServer.Codecs.LegacyCodec()), - } -} - -func (o *APIServerOptions) loadAPIGroupBuilders(ctx context.Context, tracer tracing.Tracer, apis []schema.GroupVersion) error { - o.builders = []builder.APIGroupBuilder{} - for _, gv := range apis { - api, err := o.factory.MakeAPIServer(ctx, tracer, gv) - if err != nil { - return err - } - o.builders = append(o.builders, api) - } - - if len(o.builders) < 1 { - return fmt.Errorf("no apis matched ") - } - - // Install schemas - for _, b := range o.builders { - if err := b.InstallSchema(grafanaAPIServer.Scheme); err != nil { - return err - } - } - return nil -} - -func (o *APIServerOptions) Config(tracer tracing.Tracer) (*genericapiserver.RecommendedConfig, error) { - if err := o.Options.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts( - "localhost", o.AlternateDNS, []net.IP{netutils.ParseIPSloppy("127.0.0.1")}, - ); err != nil { - return nil, fmt.Errorf("error creating self-signed certificates: %v", err) - } - - o.Options.RecommendedOptions.Authentication.RemoteKubeConfigFileOptional = true - - // TODO: determine authorization, currently insecure because Authorization provided by recommended options doesn't work - // reason: an aggregated server won't be able to post subjectaccessreviews (Grafana doesn't have this kind) - // exact error: the server could not find the requested resource (post subjectaccessreviews.authorization.k8s.io) - o.Options.RecommendedOptions.Authorization = nil - - o.Options.RecommendedOptions.Admission = nil - o.Options.RecommendedOptions.Etcd = nil - - if o.Options.RecommendedOptions.CoreAPI.CoreAPIKubeconfigPath == "" { - o.Options.RecommendedOptions.CoreAPI = nil - } - - serverConfig := genericapiserver.NewRecommendedConfig(grafanaAPIServer.Codecs) - - if err := o.Options.ApplyTo(serverConfig); err != nil { - return nil, fmt.Errorf("failed to apply options to server config: %w", err) - } - - if factoryOptions := o.factory.GetOptions(); factoryOptions != nil { - err := factoryOptions.ApplyTo(serverConfig) - if err != nil { - return nil, fmt.Errorf("factory's applyTo func failed: %s", err.Error()) - } - } - - serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("generic-apiserver-start-informers") - serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("priority-and-fairness-config-consumer") - - // Add OpenAPI specs for each group+version - err := builder.SetupConfig( - grafanaAPIServer.Scheme, - serverConfig, - o.builders, - setting.BuildStamp, - setting.BuildVersion, - setting.BuildCommit, - setting.BuildBranch, - o.factory.GetBuildHandlerChainFunc(tracer, o.builders), - ) - return serverConfig, err -} - -func (o *APIServerOptions) AddFlags(fs *pflag.FlagSet) { - o.Options.AddFlags(fs) - - if factoryOptions := o.factory.GetOptions(); factoryOptions != nil { - factoryOptions.AddFlags(fs) - } -} - -// Validate validates APIServerOptions -func (o *APIServerOptions) Validate() error { - errors := make([]error, 0) - - if factoryOptions := o.factory.GetOptions(); factoryOptions != nil { - errors = append(errors, factoryOptions.ValidateOptions()...) - } - - if errs := o.Options.Validate(); len(errs) > 0 { - errors = append(errors, errs...) - } - - return utilerrors.NewAggregate(errors) -} - -// Complete fills in fields required to have valid data -func (o *APIServerOptions) Complete() error { - return nil -} - -func (o *APIServerOptions) RunAPIServer(ctx context.Context, config *genericapiserver.RecommendedConfig) error { - delegationTarget := genericapiserver.NewEmptyDelegate() - completedConfig := config.Complete() - - server, err := completedConfig.New("standalone-apiserver", delegationTarget) - if err != nil { - return err - } - - // Install the API Group+version - // #TODO figure out how to configure storage type in o.Options.StorageOptions - err = builder.InstallAPIs(grafanaAPIServer.Scheme, grafanaAPIServer.Codecs, server, config.RESTOptionsGetter, o.builders, o.Options.StorageOptions, - o.Options.MetricsOptions.MetricsRegisterer, nil, nil, nil, // no need for server lock in standalone - ) - if err != nil { - return err - } - - // write the local config to disk - if o.Options.ExtraOptions.DevMode { - if err = clientcmd.WriteToFile( - utils.FormatKubeConfig(server.LoopbackClientConfig), - path.Join(dataPath, "apiserver.kubeconfig"), - ); err != nil { - return err - } - } - - if config.EnableProfiling { - deltaProfiling{}.Install(server.Handler.NonGoRestfulMux) - } - - return server.PrepareRun().RunWithContext(ctx) -} - -// deltaProfiling adds godeltapprof handlers for pprof under /debug/pprof. -type deltaProfiling struct{} - -// Install register godeltapprof handlers to the given mux. -func (d deltaProfiling) Install(c *mux.PathRecorderMux) { - c.UnlistedHandleFunc("/debug/pprof/delta_heap", pprof.Heap) - c.UnlistedHandleFunc("/debug/pprof/delta_block", pprof.Block) - c.UnlistedHandleFunc("/debug/pprof/delta_mutex", pprof.Mutex) -} diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/README.md b/pkg/cmd/grafana/apiserver/testdata/certificates/README.md deleted file mode 100644 index e048128c8fb..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# apiserver-certificates - -These certificates are used for development and testing ONLY. They are generated using the script under -[hack/make-aggregator-pki.sh](./hack/make-aggregator-pki.sh). - -The CA, server and client certificates are each 10 years of expiration. diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt b/pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt deleted file mode 100755 index 1991fe2da6d..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID9zCCAt+gAwIBAgIUeRrA5l+Rl4LkHPP1DmMFlYzrhW4wDQYJKoZIhvcNAQEL -BQAwgYoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApOZXcgU3dlZGVuMRMwEQYDVQQH -DApTdG9ja2hvbG0gMRAwDgYDVQQKDAdHcmFmYW5hMQwwCgYDVQQLDANSJkQxEDAO -BgNVBAMMB3Rlc3QtY2ExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZ3JhZmFuYS5hcHAw -HhcNMjQwODA1MjMxMzUzWhcNMzQwODAzMjMxMzUzWjCBijELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV -BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G -CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBANj6qsutYwof0e0zHrp35Dey+kQxi+VTr/sAKlSoyySP4fmQ -9Qn8mDY4HyJ1oOJFpFAlD0Qp1xGdbvcrlvjoieqmfenW342fza0wqS5K8qkd2rJ7 -khdAE2mACZTFSjmAa8+1rIRWnR0SaHBmDgdxBfNkET+n+cX+WsDMhmzNvPoPDS/V -8LaNih/eOUzb/5hamvD8CNLKakes0u/EsdxOsGFWCkpE1mg9yg0YPms5qUAj9pdV -iPH8B5zA1JoukZCrVGPv6R76fJI1LEohiASNFt9cgs2dhdk6QHzGyqNq3T3Cw8yI -Cug/Kk9DGqwq9OeXtADa4hhPebj04C4hxk0AT2UCAwEAAaNTMFEwHQYDVR0OBBYE -FBBn2SXiiItJQsJZ7MTvIn1s1t3jMB8GA1UdIwQYMBaAFBBn2SXiiItJQsJZ7MTv -In1s1t3jMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABcCcwsg -jrcNZheAaKXtdcZT01+doQhaOvhZQhY5L+bCnB0wkBXRJzv8ne+KgLx7auFQP8/2 -OoPgaiA0R+XtImCkyBd+cr4mo2tYVpHt9+B0HaYGzGoXt0dre47ihlgkqoSwmgvG -9++pfrbQGRd5Xb/j0468sd5uQy1PPhsjCzFZTuxXcaAN13MDNikYjjn5mc5coklu -hCFH54PgP/PUDXxI0v/QUjNOj7hAdMkqOjzFD9Fze1KjtS3aSZvaaZVrM3x/YS8y -1IUgyocgoOKCqBOeEict+g/xghFDe7r2Dlgps/hPD1ojijBl83g5i079jW4y9jm+ -osFOTGnRx2u0CpE= ------END CERTIFICATE----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/ca.key b/pkg/cmd/grafana/apiserver/testdata/certificates/ca.key deleted file mode 100755 index 2b427227dff..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/ca.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDY+qrLrWMKH9Ht -Mx66d+Q3svpEMYvlU6/7ACpUqMskj+H5kPUJ/Jg2OB8idaDiRaRQJQ9EKdcRnW73 -K5b46Inqpn3p1t+Nn82tMKkuSvKpHdqye5IXQBNpgAmUxUo5gGvPtayEVp0dEmhw -Zg4HcQXzZBE/p/nF/lrAzIZszbz6Dw0v1fC2jYof3jlM2/+YWprw/AjSympHrNLv -xLHcTrBhVgpKRNZoPcoNGD5rOalAI/aXVYjx/AecwNSaLpGQq1Rj7+ke+nySNSxK -IYgEjRbfXILNnYXZOkB8xsqjat09wsPMiAroPypPQxqsKvTnl7QA2uIYT3m49OAu -IcZNAE9lAgMBAAECggEAYUTyNy+J1BqSrd66WkZv6SZTeimp+Mrs+71FvMEUnFXi -LFJ2/xydEcVT88s+reEheYo7j0egcfWdLrH8UqZQWYB8ts0MV715YzgKx8Vyhizr -gxLRWZnwed2brfVJwoBXFHzxkzwO398GMckWZfCdhdBoyRwg5UkS3xZw9qq+mmw8 -hvt88sDzHwnk/9rMY6KLNhWNiCSAxU65AlpktWRGy2e9wAtyzN+WX/iQwsiAyhk2 -TV2bHQxRE7FT9hg4UBxzWruYRj4jkLKdH4tVqVKLsV7KutAOiYj3hCjYzOac/5QG -XrTRhN/ewAqJCTOI9K8BVT27l3QxREcq480auDwIawKBgQDzOr6+ILgPdqdGX9Nu -HoLnNuU6OdFsTJ3qXSp4AGv8ufq47PJmTp3F2ZN9RTDr6Fih7UKhApVvFOy6/Hlr -vb0MbL9FOe+2ejtnVmNi13AMexW+nmnOs1oSghXYIbt51/n+5bFmzAo6RFDEG0YT -mtRWMj4mifMI0XH31yRk+Yjp6wKBgQDkXxdG0OmoDwlrlbHjVOpEg7/A0diBHfb9 -yh4MbaVmd6rGgdNkJYq1qB6ctJcLYpKq1QGgj6FyNDNyY/T4lIxRuSHqC2v1Mjt/ -TaL1BhMsH2Q6+bT8mlmM4cNNgSVHfISJv2mMuKfEqb+uY0y8UU6/lYmwzC5rH4bs -TgPNSLmH7wKBgQCk8F9M2y81/UZt6Kmd8T7fwFAt7etgP4yO02LrQY35Mb0eDkBK -tGE1O9hSiMsmDseb9yLJwNDJJS1rl65XK7G5bT0/mow9+CG0b9axvlqTfBxAyXgC -3YjlKCXcDPPvKlCzU9u7U/5TiOQkOEKLJOF9GlEfHUkb37wjT1e0yarYxQKBgFNF -nT40RU8DlKLHJeNH/lhXVh9gJTsHix2Fiqlrfck8T2gsxMEas1aD5A2uB/mdyu9B -1mMOnIcBI9VNP3E48WWHRSeLXKU+2NUVoRsJSQpos+qRTP5i5c5qMAXd1pMXg1ib -FEi8uGgMoZlcGgn89+MCCwANo8tp5o/Z7qb3Ire/AoGANmc8BAGgKanJVAfu7gDU -AgxRg5T0/C6vpWh5k/gFS7AQqgnx8EcRYtuCD4Er5GFRdcyb9AuZccyKzWI5JS89 -YOmJPrfDd7VzBsrOjwJCXcV6tUUq6yO4Ra8vKlIw7T+mMYZh7xWwGnT8UA+X9F+7 -yV6kPpraMD/dFbo2TZoNMjc= ------END PRIVATE KEY----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/client.crt b/pkg/cmd/grafana/apiserver/testdata/certificates/client.crt deleted file mode 100755 index 4b451c2fac9..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/client.crt +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEjDCCA3SgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBijELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV -BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G -CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDAeFw0yNDA4MDUyMzEzNTRaFw0z -NDA4MDMyMzEzNTRaMC8xFDASBgNVBAMMC2RldmVsb3BtZW50MRcwFQYDVQQKDA5z -eXN0ZW06bWFzdGVyczCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMz3 -SSM62uPCNG7mbdlgE0bYcpT8f1ZFmyaauXzbqdTF0/VxGCh9YVBFnqlAdfAUFasN -HBLQz63PniOWbzmhXZBkrCF/nozg7PgiXJ7aM5ppJyYulOI3sldH49V8KWzcGiJo -ZE13kKIlCqnOanErj4RnaxVJvMmWOlt6Zn/ljkDFvwypaOeIX/YO1zuZ95BqGpDh -u4CXipKeCM0AjXNvKrHCi5QbNEF0FMyvsX+s76wZtZSpB7vbIfj/h5Z4Scfyovmy -cZeqgL2mDAnosKw8/KYAgkcArMHoScTfdCjWthJQlPybJKgMKOJI9OehuP1a89A9 -t+T/T8ZT75x0YADw+56WDcsYGECiaixrkpB0aiPBtgDm1i1z7ooqSfAfZa0PEgPT -L680TK66qXI2GFtNqUkU6+xZEfvuxOdZRXkuIVeQ4UMqaiW9y37ppvKzX4llOD2D -VP86D0JxCCuJqxlzSAGbxK61c8mdqM1SKAa5O5Kl0KHxVkGk4zHYtrHqWsy6YOIM -CqLMA2vLAeqYwSEn2eQBkTNdhSulrB6JPNBDPNB9+wX5EOX3w+x9lRxn59kQgKyV -anf1cm1UYeqjsLPhBjpuwVtHhEAc8uJrSZBM3oKPSYEYxwEo+QG8rcKIpjV9gtCu -T3e/WBWlDVBm0NwbxtY/rYc5ggPHMtPhFxBDhjJpAgMBAAGjVzBVMBMGA1UdJQQM -MAoGCCsGAQUFBwMCMB0GA1UdDgQWBBQ+z7tF4/VANXrXgsY0PVQFFo6csjAfBgNV -HSMEGDAWgBQQZ9kl4oiLSULCWezE7yJ9bNbd4zANBgkqhkiG9w0BAQsFAAOCAQEA -NagU3hqJr5wEU79202Rj+aqbzWJlz1jvZVR6PHILB0deTtvYk1EXeVgyjwmz7PW3 -DLYcpwgW3bTdxhejduFNzKazDOZZ2blUlZlHs1PoBb/ipnw4g+ozO+ZyGs05gCu8 -DrsxUKX7bpVcKGfPNVg8L4xZbanizO1XUiv6PDBBRZhKXSl+KO9+aN2C/yRnYmpV -9dyuMI9nFoMB0K7rxTxiRCIPIWs8nsGouLa6lg6/I+xTAjV0IqNz0rQ66UWJOr3x -vLGFdMMaDUSbsNlu18/sCJd+G0rkh24YE6e3I1wGqE9jr4iYsMhkhfp0u4Qojmfp -3/7IGZYzvVIN/PvSFBBgZw== ------END CERTIFICATE----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/client.csr b/pkg/cmd/grafana/apiserver/testdata/certificates/client.csr deleted file mode 100755 index b6848a532aa..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/client.csr +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIEmjCCAoICAQAwLzEUMBIGA1UEAwwLZGV2ZWxvcG1lbnQxFzAVBgNVBAoMDnN5 -c3RlbTptYXN0ZXJzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzPdJ -Izra48I0buZt2WATRthylPx/VkWbJpq5fNup1MXT9XEYKH1hUEWeqUB18BQVqw0c -EtDPrc+eI5ZvOaFdkGSsIX+ejODs+CJcntozmmknJi6U4jeyV0fj1XwpbNwaImhk -TXeQoiUKqc5qcSuPhGdrFUm8yZY6W3pmf+WOQMW/DKlo54hf9g7XO5n3kGoakOG7 -gJeKkp4IzQCNc28qscKLlBs0QXQUzK+xf6zvrBm1lKkHu9sh+P+HlnhJx/Ki+bJx -l6qAvaYMCeiwrDz8pgCCRwCswehJxN90KNa2ElCU/JskqAwo4kj056G4/Vrz0D23 -5P9PxlPvnHRgAPD7npYNyxgYQKJqLGuSkHRqI8G2AObWLXPuiipJ8B9lrQ8SA9Mv -rzRMrrqpcjYYW02pSRTr7FkR++7E51lFeS4hV5DhQypqJb3Lfumm8rNfiWU4PYNU -/zoPQnEIK4mrGXNIAZvErrVzyZ2ozVIoBrk7kqXQofFWQaTjMdi2sepazLpg4gwK -oswDa8sB6pjBISfZ5AGRM12FK6WsHok80EM80H37BfkQ5ffD7H2VHGfn2RCArJVq -d/VybVRh6qOws+EGOm7BW0eEQBzy4mtJkEzego9JgRjHASj5AbytwoimNX2C0K5P -d79YFaUNUGbQ3BvG1j+thzmCA8cy0+EXEEOGMmkCAwEAAaAmMCQGCSqGSIb3DQEJ -DjEXMBUwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggIBAB7x -82Hy942/7nSbz9gJUsJ70sYcre0VRduIMzP1Ytk/ni+1QXDZSmdSrgR0cbTL4VIZ -zSt5Bp2vxBJqHmGPOXv5oWI+nGrfWzmzBGoGNk4cdNW2ZZCn6Om4L16gLYIlxmm1 -vgCgnPOYMEi5qtgG6676wSX7gf5+j1gWVDEwNFil8wBUIlA9QRbc2NN0T6479Z4b -4tUHzUpGuf788OPUqAdxpkH1xZBKW8HEGsk1+NdHgb5krE2ElwvN9qWX5f2DzzwL -sQf3A2IrO4lrso4RHao4X3V+/DOZCx+FK/oXqzyoS4YNnNy6B0377LgYkuuJsbOo -IFC7vRhBORvAXEXD/scLEsGKuQzs1vLLiwOp7pBOcGZzxOoqoXc2A6h64qpZKriq -T1rK6rmrttaXqYUokjbg/ggpcXCC1BGxsoRvGQoN6aQNV27zUh2wpZqpCpVLoN21 -eCqM9LVoPMCRqn7ItXE9oJhasPlKDO6amHL3CtxjvU2meXoa+nCia8lL4Sb9pSkB -O5eX4k3H/m4zjrpbqp7UdnidcYn4zfrjRBr65bqNr1sOxwUDWOa0kqaOKrgqwhWn -ld6zkuekuwzrOK4+Dpf5ybWnVFi8WSz6k9TqQIaeMrzeCBVboufl/ygoIRsTqp7N -WcKuZdUx8t16hFZ/NgpHRgfZI9pYdzi2vNSZ6WMM ------END CERTIFICATE REQUEST----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/client.key b/pkg/cmd/grafana/apiserver/testdata/certificates/client.key deleted file mode 100755 index 0a195f34480..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/client.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDM90kjOtrjwjRu -5m3ZYBNG2HKU/H9WRZsmmrl826nUxdP1cRgofWFQRZ6pQHXwFBWrDRwS0M+tz54j -lm85oV2QZKwhf56M4Oz4Ilye2jOaaScmLpTiN7JXR+PVfCls3BoiaGRNd5CiJQqp -zmpxK4+EZ2sVSbzJljpbemZ/5Y5Axb8MqWjniF/2Dtc7mfeQahqQ4buAl4qSngjN -AI1zbyqxwouUGzRBdBTMr7F/rO+sGbWUqQe72yH4/4eWeEnH8qL5snGXqoC9pgwJ -6LCsPPymAIJHAKzB6EnE33Qo1rYSUJT8mySoDCjiSPTnobj9WvPQPbfk/0/GU++c -dGAA8Puelg3LGBhAomosa5KQdGojwbYA5tYtc+6KKknwH2WtDxID0y+vNEyuuqly -NhhbTalJFOvsWRH77sTnWUV5LiFXkOFDKmolvct+6abys1+JZTg9g1T/Og9CcQgr -iasZc0gBm8SutXPJnajNUigGuTuSpdCh8VZBpOMx2Lax6lrMumDiDAqizANrywHq -mMEhJ9nkAZEzXYUrpaweiTzQQzzQffsF+RDl98PsfZUcZ+fZEICslWp39XJtVGHq -o7Cz4QY6bsFbR4RAHPLia0mQTN6Cj0mBGMcBKPkBvK3CiKY1fYLQrk93v1gVpQ1Q -ZtDcG8bWP62HOYIDxzLT4RcQQ4YyaQIDAQABAoICAAP5CpPqMauJZv9bzCGy9aix -bGt5ce/pMTrNiV5zel5yDQZ/FXpqb2WRNeQ0CEfZzHo4arStfgsGy6WcW7tMnAie -6kG2IdZQoUiQJPFcy3QuSS3u/Z/dYmxD69VRCZNZ4vqgq/S0RmDIMkWB515acMGT -82zHapH8jWoawcwalgK+JSf7YXnJvZ9tC0xf0tSRI+2ZPIDKHyuHReZ7ALg4iEWy -CUclgwJTm/gZiIman497dOwviGN3zxf5aaZtCN69Pp4I+1/sKYA+N3yag/smAvkm -PcrfHITKf3c3ZBQLEQl4Qk0GUPlzY+L6n57yQOijQnd6Yhs9kiKnBkLRyXnB3K9T -M/ZDp7BHSO+e+qENsYk7POzgeU8udF3mS/h17/7kXIdvi/7l/6Dh2YlEqjH+p3XX -AKLKwx01c++h9aa9RXU+PpwyZoKpYhe16kMceF/bxbr74w5Ukqzny5iEX1t0oCfn -KncheoJQiIcC2O1pZLGRzxSlizvbReP9fGPV9qFmDMvdawK14472hT+mNZw4fziq -dLGr9MZ3hVThwIl1ylA9qCJQ0VbaD7Z6YIx0fO2owro4M6Xv247/KZQ7hM1rUo3F -crqe82qysOrAysgDjvho4iCPKW9d/kUPqYA7DHUOvHBkvsgUoc2TNmV48moYOaox -BoHBtDN49SyQpHaoG+NxAoIBAQD2dHhiFyQlTUeym6jpc3l0tLt1Y6OTwkNi0vQo -7j+aNSGSPkcQkhObM54G1pE+DDy2pkpS8m2teKGmnXMBK+TLS/WjcuJbLvGAxlY7 -mRzJh1bzL+1aEa8ZOCtc3FcVBdGTvwCInEzx2r5ghWEgBvy6SQcVs76ALfteUDvI -HxZG1Xou3Il3O+vaxK8ure2SJJk710NQfSF0qaUR6qpLnL3zsRS9GXQ1V/6Ek8CQ -0tdMME+reTC7wh51s/oCNh3QoJThsv4DQQD69SBX8kfy1+qaLOxbNAefrTQk6Pd2 -xNCu4szAG7QyiF/5jOzV5JJYVTnrWqsa5obzCcIbjZWTEPSRAoIBAQDU53bHHDSA -S0oGRpxf9gzz2xVqbG+slraCs9pNA9fkcQostATP9pWvnrch40yOTMyXi2ldVBW8 -ZO8R7jVVA8mbSIyPv9a5V/AuqpRX1fFgojaFY3mEw52CYpmg6/thCESBb6ILXB4e -2cv+lmkb3fYGABxzA+VUEVEsBXprqeJdcBlVdXs3/ZB/jO13ASdSAZrguc2Gknrv -9wg7cPkWIH19m/7DXvQxkX+ROsLSft9AJp3wFzAh4lEGe5zTWaHJS+hfxVrniQEX -M/0qTMRD5PYmsm/8vv6Wx4FlLCx9kWYtygh6Za1h7RDd6iMosDlpV4NASla7fTKV -eOmAvi1uNGxZAoIBAQDyJi3B8wrIu82eZ+LmvVawnIMzK9sk6tJa3vqW3MARO/Lo -Rdh9J4msDGNQRLIgTNW6gFi2dwvcTZJGqpy8oewC83c+STqubMlMxZMkq6PlPtzn -xEdpH8by+IVij/vf4/+vMxPLJgdT+qDjJSnw1eyq++XCJQEf4A4C9MJINoMkxctv -D7DhPjbWlDmrm6i41szYRwEUrF2ayrQtjmwULsVUEsFVqxTK9NJWYPXrVb3EVhNx -X3nKgUh8TYFvesyAl8awm7WIbO4RpZdJ0ftvV0ZihZEVa2GyOfPp8Bx0zZxcuOqE -NrQukl/6ScTJw/MmZ1apMES+AZLGaOgXOl2kShyBAoIBAGiC0xi8rL0JuGXKRbsJ -gqQ2OJYMculq9l7EwPWrXFBkeRUmrXIU3rfeFpHJDWyRIKGHqwpIW38moQDRSVbZ -TB8xBucNye8jzuBplfZkLGA+YLsr8JwOloRJuJZ5IOYp888CKK6g4pxMV8o6tZAb -bkjVxyFimTGiapFMgyLUuy0Y+SatS/ZZP5SNbohLhazI4ulL7CsSPs0LG/xp4axN -+KwvZmkrdH3cqZ12FerUouzPyBnymAsaGKIxDfPl/Phejcxtick8xM9KEw2vr2yQ -uZCXaUfNzhXgC5HJlHFzbZAuq+jBftIiWHRHGkk/8H7YmbJ2i4rv02PyfdVYBd8i -VskCggEANfeuwEYrzuraOl0r60S2XCJURl78s8ug4hkG24/zW+VkjabDw6YY9OEO -INVrlw5dSU7JMz9bqms7fXi7MW+qy5Sfl0OaTGP80I3NsQ8T09OYg0ToKJf/IXHn -GQiGbHRHVTrcjL4blERkzLYlJAr3FuWLIFYusGKFRr/PAeq0GY5yJowaYz3Oqpit -6bf5kM4CK8HQyfMvs0lO3OQ66gRjTq6L/GDHJa+yp6jd7n7p3ocpUBMpTdncCnNb -qYmscnU8UIdrdTGKtySCiBAUup8AzS/Z8D7EnYPg3EiNhG0OJEPSPJPKQqFBnF8w -nL1C44m2dHWfIuDuGDs008g/12A53A== ------END PRIVATE KEY----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/server.crt b/pkg/cmd/grafana/apiserver/testdata/certificates/server.crt deleted file mode 100755 index 1a5a8714498..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/server.crt +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIE0jCCA7qgAwIBAgIBAjANBgkqhkiG9w0BAQsFADCBijELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV -BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G -CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDAeFw0yNDA4MDUyMzEzNTVaFw0z -NDA4MDMyMzEzNTVaMCkxEjAQBgNVBAMMCWxvY2FsaG9zdDETMBEGA1UECgwKYWdn -cmVnYXRlZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMbgGuvCKonn -9KgW58wqgxiykVbbmT82HmhgOBw5CSg7lVnKQlRzfvUtwZX7UhHXYxOCu0no7RqD -Y19QiGOx6SQlaXSMxqPL1PpWMQdX6j/I/H1pHMJ8Ynyd956VlhKUOIQBJMVd46Bg -6IEU4izuJ9uqEuHQrO8lb2eaTXAjsOmtIImxBI7xFsxp42wJyo6ptGetyaNCw7UF -e8xYmBVSLyfiN/drBx/lcwMiDgzfzf8BsavC96Fa/HujXkybAIyVid2DxKODPT9+ -7MA1ZY0/Nz3ySKSxIIbfL+1cPihR2GGy1Qwa/GuuJSGAwzy6D/9am5/K3yR927Bk -OgMUCIlpGlSDY1qFd79o/n9BJ9FJmlTMgIZ6RokaXAU2uaiE6v7YPYGkXoIz3ncc -SBprVom2h3RmM3qDbmXio1gGsDL/2SI+Hpnq2zhCzL28K+bZA6ukYpCNyyaYF9O6 -X6qi56zGCw/igTJgaYKMsNMSoX+3eHSFxj5JeF4bWcC4qTqmzvfz0HhvX+jjky/V -LHCh9ddH1umT/Ss/IxaNarXxXHoUmN33FgFQ+tML4eTvosG6IxUdJ/34qBT+dWk4 -zNIhStTUNiYPbcSfv8qKLHhMAuZOLsTATLsFQt7bzGZwV8HAuCiqzjg9p4R/dRjE -OLhQLZfKkYl99Mgli6v/hooEBSORCulVAgMBAAGjgaIwgZ8wPgYDVR0RBDcwNYIo -djBhbHBoYTEuZXhhbXBsZS5ncmFmYW5hLmFwcC5kZWZhdWx0LnN2Y4IJbG9jYWxo -b3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUFvrZ -B0vSR3B7HcNPCZrZ2H7sFw0wHwYDVR0jBBgwFoAUEGfZJeKIi0lCwlnsxO8ifWzW -3eMwDQYJKoZIhvcNAQELBQADggEBAGYZYrY+eFmgwFp/ohIcWnekdOiCr7qjnmRo -LyNU/EbFz0HUV+yTrtoXLjv8S9D9Yc029Dgj2f31Hp7cOGjUfd0t1DgTBChcFgVr -E5ZbhmKEM0tQiBMI6mHvy6hFT+nc9/yftnndHRUyR4xm6E1dMFqpMyMdYKojRmbn -F6znVTcjBr4OiDnfTUkqYO8kc3I0qvA5ou4jXAJ9mu3UEbEjwc6C2/Mrr48a42Df -rHrwqwnM3DC2+SWVocctk1PRZqMFWypJ9U/HbKPbId79YyHNsI0XtxizV2sX5oXD -1NPPEBeEXZ3Nv/gn4d0h57UoBML9165fRRQSe7CkIl8kZigHGCg= ------END CERTIFICATE----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/server.csr b/pkg/cmd/grafana/apiserver/testdata/certificates/server.csr deleted file mode 100755 index 7fc2e09691f..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/server.csr +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIE3jCCAsYCAQAwKTESMBAGA1UEAwwJbG9jYWxob3N0MRMwEQYDVQQKDAphZ2dy -ZWdhdGVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxuAa68Iqief0 -qBbnzCqDGLKRVtuZPzYeaGA4HDkJKDuVWcpCVHN+9S3BlftSEddjE4K7SejtGoNj -X1CIY7HpJCVpdIzGo8vU+lYxB1fqP8j8fWkcwnxifJ33npWWEpQ4hAEkxV3joGDo -gRTiLO4n26oS4dCs7yVvZ5pNcCOw6a0gibEEjvEWzGnjbAnKjqm0Z63Jo0LDtQV7 -zFiYFVIvJ+I392sHH+VzAyIODN/N/wGxq8L3oVr8e6NeTJsAjJWJ3YPEo4M9P37s -wDVljT83PfJIpLEght8v7Vw+KFHYYbLVDBr8a64lIYDDPLoP/1qbn8rfJH3bsGQ6 -AxQIiWkaVINjWoV3v2j+f0En0UmaVMyAhnpGiRpcBTa5qITq/tg9gaRegjPedxxI -GmtWibaHdGYzeoNuZeKjWAawMv/ZIj4emerbOELMvbwr5tkDq6RikI3LJpgX07pf -qqLnrMYLD+KBMmBpgoyw0xKhf7d4dIXGPkl4XhtZwLipOqbO9/PQeG9f6OOTL9Us -cKH110fW6ZP9Kz8jFo1qtfFcehSY3fcWAVD60wvh5O+iwbojFR0n/fioFP51aTjM -0iFK1NQ2Jg9txJ+/yooseEwC5k4uxMBMuwVC3tvMZnBXwcC4KKrOOD2nhH91GMQ4 -uFAtl8qRiX30yCWLq/+GigQFI5EK6VUCAwEAAaBwMG4GCSqGSIb3DQEJDjFhMF8w -PgYDVR0RBDcwNYIodjBhbHBoYTEuZXhhbXBsZS5ncmFmYW5hLmFwcC5kZWZhdWx0 -LnN2Y4IJbG9jYWxob3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAN -BgkqhkiG9w0BAQsFAAOCAgEAGmr6CnGfcm+DapmhQnQLyq0HyCqKD0PKKamofQLb -YVMiCF/zKDV4+gIs2kMK3uWeZ9r4xDa9nEgBw6U2vi14AI79hmpETCrfvK6hterI -Znb2TQMYnUY3rt26DFNf3/21/jb/1cn/9z55TaHJGqqlJmvB1LfYJoMN8t6A6xg4 -J8TYjBuwQQOqFAPRuxmGag44PSC9V5e6gajz56RPyZz2kmdbfPZRNCnqileDWZla -7pilwP8QAhrJCPP25edc/5hP2WNTEH/GTa5FFmkNMKEHn6+dnBuMu5w1SygKUYWz -37qE7jntZC/RGVZ//npwsVyaa+NbgJNjhg/EMj+sWb/Eet2ETq7v9FCM0QG3HNUk -6d6af2YHI3Fo89y5ty1DOydBa5lIxy6gDTwameJYoTem71nPlRtU2b4VFBWZ4xwE -ac7Xmon+Z7tOHVwcCPp1cTwJ2TNwha0JxsW0C1g3QG59ILU5FMPlqOuDJi41uWql -Q56O+a6MnK7GfGxBMMf4FSlbV3xjUGxqyGy5KwIcsy+u/3axCWPUcoz3g0pApbGO -pfsu5Ptr/xMtYjLRXbEcH9Byqx/LrBvD2upwNnlfMtgWIlg/EnZ26Mgardu/NwQq -3fpYv00VWbvBE/B/5p5zmEpA67COFejiwVTsbeN345Ue2mq59DNg6BZmaxKH3Sbq -Kz8= ------END CERTIFICATE REQUEST----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/server.key b/pkg/cmd/grafana/apiserver/testdata/certificates/server.key deleted file mode 100755 index 820abf97e95..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/server.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDG4BrrwiqJ5/So -FufMKoMYspFW25k/Nh5oYDgcOQkoO5VZykJUc371LcGV+1IR12MTgrtJ6O0ag2Nf -UIhjsekkJWl0jMajy9T6VjEHV+o/yPx9aRzCfGJ8nfeelZYSlDiEASTFXeOgYOiB -FOIs7ifbqhLh0KzvJW9nmk1wI7DprSCJsQSO8RbMaeNsCcqOqbRnrcmjQsO1BXvM -WJgVUi8n4jf3awcf5XMDIg4M383/AbGrwvehWvx7o15MmwCMlYndg8Sjgz0/fuzA -NWWNPzc98kiksSCG3y/tXD4oUdhhstUMGvxrriUhgMM8ug//Wpufyt8kfduwZDoD -FAiJaRpUg2NahXe/aP5/QSfRSZpUzICGekaJGlwFNrmohOr+2D2BpF6CM953HEga -a1aJtod0ZjN6g25l4qNYBrAy/9kiPh6Z6ts4Qsy9vCvm2QOrpGKQjcsmmBfTul+q -ouesxgsP4oEyYGmCjLDTEqF/t3h0hcY+SXheG1nAuKk6ps7389B4b1/o45Mv1Sxw -ofXXR9bpk/0rPyMWjWq18Vx6FJjd9xYBUPrTC+Hk76LBuiMVHSf9+KgU/nVpOMzS -IUrU1DYmD23En7/Kiix4TALmTi7EwEy7BULe28xmcFfBwLgoqs44PaeEf3UYxDi4 -UC2XypGJffTIJYur/4aKBAUjkQrpVQIDAQABAoICAFTLxj7Czc96OvuWtKP9dmNH -9Cd0P63PnfyEFjiWaxyf9yjPUCPhEP9qUJHqFE6uJzzw73lumvZEklDYLidP+ufi -GcpLogDCDt/kc0g9yJAE2v+AG3ajgXzAAA46mr/2Ofiy4iJTS5Sc7VXoeR2OOCl1 -pVJqXuoi7JLgnGcVmL+yBV8gPqDSFBX5ijINJLRakKTqWUDG3VpoaaYyGjpxDdE0 -KAfTNzj25Oivkw0TOiqiZsalPV+rw17WRAVmy7+lnSB5qBTOBwX1UO4NdmzYyO2d -SjMKoSNQs4dB3vDjIN9bWHKuaPVizcsws05HyT1oPVXPMwDEtzDJM2EPoCoyyboB -sIaMnobL3QpfpumLOcpWl+W9rcOCY4fcBU6kgQXRT8XlvSa9hu0UIo4XD9tbYVzq -+REpJlDrocvaWIRjrq6UtcHZzvUZ3YB8v6FZx6lFjZN8dBt8QOoRKgnnAFUTZHT0 -CddFOLMnEeCof+oKF7GrWVAF/BpXHR1hI5Wq7/3wf5MKUOIDj9Z2DM4WzTQCMGEk -PpxCO37xFpH4mOSkZN2lydo1ZBQG0WAc4sFBaJX/dKijci26q9JWbn1fkmQ5XQsG -nXI58QoXFwf7Qlk0zIo0219a9wFR0hls4+JNlyqAoZ6iZevIPR7T954WokaNpvm8 -mZjmrKa59GrSSgyC0IY7AoIBAQDqvTNlKrEaxXaQq97ws3EtkjvpOCkPykRy2Fx6 -nskEtln7AsgJdLu6Q4W0/SYpIDko/cGAKjZLthq8t81As2CdZMiPuJVNufZtziQj -Lai2jcObeuWGMkFMGnODwP3bIoPvcPnsiSt0P/bJc/MPZIZRvgyPzLlF9OvNof2H -Rca8iX42nxZb0V3Uet6n2sqpEsV6uA3R1V1hc0T/b04c3D3GZgOpMXnqfCrIsqdr -6VpeNJZ7O/x8uyHl9kqztqmvlle8bikOZ0s0n93PdcXuHBoWossPZWW08A68B68d -lPldxYeSIkDfL3jDdvN6U12oUARo1hdt0Ozvww7NtS+SPWv3AoIBAQDY41kyOErf -SPVxuMWIhirstDI14xKFbC9DpIO9BALni25toWdsYeAJb4YW+ELUmBKHEGz3kB1N -0mDkqSipXaEE3rbsx44oegmSiOQrGLNypQdEctttE3HJxLCyabh/kbeFVv+L3+8G -VB+0jq/SgtSqZZAwkwwaUwD4/+lVVZ1xjEAliuZ4cNHxtl/DPq8gBR+8nUr4LZac -HEdF6zyMEEAG19Cyzz+zE86bY5IvUQvDjGEnZh71LIG5jns+Lh4b1Nw9JPNmCZ9d -mddIetFd031vI/NkkBuQ0SUqd+eQwDt0jgt2nQEO60Lw9D9UUks/c++M5GkQeGjy -1KyzKOLqTcoTAoIBADBOtXf5XC8lOew14pBobT8ym++35gNg3ctAqW92o+m7WTMl -9GK1yjhf0vFXM3Y9MmY0KpEknr3gAQqbTLsm7xgU+I1TMC6puYQJazhuGg1PiVTC -6t8+EmAGBYW0vslNBhfNiTFbXTz0OOZmXTvqtRW3ZcBmIi66Y5iS4Kjo/Cgqp3W4 -MZK9uHCUxKOIjDJVMZy6qeVn4mq+nRFwJ4Qa8v+UWOaFzxApc2iQE5JKmJVQfzNn -OeO1Yxl/IQpw6eS/rNiTVxGmwjxXNf+OviftUpUb9Wv6sv6UdIPPlQMieFsK3oZ9 -VBpaG6EmJp8i7uBHb1Df1jx8RXZmDvLYeay/xSsCggEBAIqnZTl2xV7TfJ30Gsw5 -wa1LUaIjhY6oZ9rdjJ7Etrqh57nMapreQ2Sk2FtM4SSaB5YzCQaHKkS7Dth/0A/e -XHcJjnX26Um1IvN78iofA3FyUSAQMXkc6iysQq38akebt3BV+s7IHT21gANlCMAS -hbRdc32qNB2MHN4SdG/qaNnTaJrXnpk2vvDAv53JMBnPTMe+4tOgCV3JskLfrPh5 -1wTI6ZG2bqmkKvwp/qWjMVsVHnMalQX2KwSeMunAf90ZCqdIPRZpZmlnVTrv0XMj -Jlhr6kjK2+SL4C+zMeXXDutnd6qfmrKX8laqPuZAKfzpuCYhS42M/MLo9XMf21kg -2+MCggEAepV2WsaeUfXu5MxFdCrsJ+MntBlQDVELULVt8Lw1lJBvs89SQVSF2y4Z -ng8gbZyRZeZe56NXQBgOdRQxxYxfWC8+lfRB0Vx3htteEuuQmMSVsyoLfTcpWHx+ -4aX9WIrnFnjJvPZtVrApu00esjoLOBvTgcn8LTcpK0JvUGTyNKZ2dCYHx0uy07S2 -FIN/zWyrt1+bTXdQnRzgRIfE6pIhPRyxZJk3es+2yY/hgdkf6vdxKYCB+RLCU9jw -CrbmG/OXisvGK3A26Bknje4Rm6/5tdyU8cmqeV4rzaH5QTRWvG/d3qJ9PSWTlX6a -C+DUGz2rplZQ/4PtNOcz7reeAFvu6A== ------END PRIVATE KEY----- diff --git a/pkg/cmd/grafana/main.go b/pkg/cmd/grafana/main.go index e9003dd078a..5679d272a06 100644 --- a/pkg/cmd/grafana/main.go +++ b/pkg/cmd/grafana/main.go @@ -8,8 +8,9 @@ import ( "github.com/urfave/cli/v2" gcli "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands" - gsrv "github.com/grafana/grafana/pkg/cmd/grafana-server/commands" - "github.com/grafana/grafana/pkg/cmd/grafana/apiserver" + "github.com/grafana/grafana/pkg/cmd/grafana-server/commands" + "github.com/grafana/grafana/pkg/server" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" ) // The following variables cannot be constants, since they can be overridden through the -X link flag @@ -31,7 +32,7 @@ func main() { } func MainApp() *cli.App { - return &cli.App{ + app := &cli.App{ Name: "grafana", Usage: "Grafana server and command line interface", Authors: []*cli.Author{ @@ -43,30 +44,32 @@ func MainApp() *cli.App { Version: version, Commands: []*cli.Command{ gcli.CLICommand(version), - gsrv.ServerCommand(version, commit, enterpriseCommit, buildBranch, buildstamp), - { - // The kubernetes standalone apiserver service runner - Name: "apiserver", - Usage: "run a standalone api service (experimental)", - // Skip parsing flags because the command line is actually managed by cobra - SkipFlagParsing: true, - Action: func(context *cli.Context) error { - // exit here because apiserver handles its own error output - os.Exit(apiserver.RunCLI(gsrv.ServerOptions{ - Version: version, - Commit: commit, - EnterpriseCommit: enterpriseCommit, - BuildBranch: buildBranch, - BuildStamp: buildstamp, - Context: context, - })) - return nil - }, - }, + commands.ServerCommand(version, commit, enterpriseCommit, buildBranch, buildstamp), }, CommandNotFound: cmdNotFound, EnableBashCompletion: true, } + + // Set the global build info + buildInfo := standalone.BuildInfo{ + Version: version, + Commit: commit, + EnterpriseCommit: enterpriseCommit, + BuildBranch: buildBranch, + BuildStamp: buildstamp, + } + commands.SetBuildInfo(buildInfo) + + // Add the enterprise command line to build an api server + f, err := server.InitializeAPIServerFactory() + if err == nil { + cmd := f.GetCLICommand(buildInfo) + if cmd != nil { + app.Commands = append(app.Commands, cmd) + } + } + + return app } func cmdNotFound(c *cli.Context, command string) { diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index fc46292a84d..98e324f8e5e 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -15,11 +15,6 @@ import ( _ "github.com/go-jose/go-jose/v3" _ "github.com/gobwas/glob" _ "github.com/googleapis/gax-go/v2" - _ "github.com/grafana/dskit/backoff" - _ "github.com/grafana/dskit/flagext" - _ "github.com/grafana/e2e" - _ "github.com/grafana/gofpdf" - _ "github.com/grafana/gomemcache/memcache" _ "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" _ "github.com/grpc-ecosystem/go-grpc-middleware/v2" _ "github.com/hashicorp/go-multierror" @@ -29,9 +24,16 @@ import ( _ "github.com/phpdave11/gofpdi" _ "github.com/robfig/cron/v3" _ "github.com/russellhaering/goxmldsig" + _ "github.com/spf13/cobra" // used by the standalone apiserver cli _ "github.com/stretchr/testify/require" _ "golang.org/x/time/rate" _ "xorm.io/builder" + + _ "github.com/grafana/dskit/backoff" + _ "github.com/grafana/dskit/flagext" + _ "github.com/grafana/e2e" + _ "github.com/grafana/gofpdf" + _ "github.com/grafana/gomemcache/memcache" ) var IsEnterprise bool = false diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 81574ec9677..f23c64fbb54 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -465,5 +465,5 @@ func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOp // Initialize the standalone APIServer factory func InitializeAPIServerFactory() (standalone.APIServerFactory, error) { wire.Build(wireExtsStandaloneAPIServerSet) - return &standalone.DummyAPIFactory{}, nil // Wire will replace this with a real interface + return &standalone.NoOpAPIServerFactory{}, nil // Wire will replace this with a real interface } diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 564aad51ea5..3d97c42aafd 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -141,5 +141,5 @@ var wireExtsModuleServerSet = wire.NewSet( ) var wireExtsStandaloneAPIServerSet = wire.NewSet( - standalone.GetDummyAPIFactory, + standalone.ProvideAPIServerFactory, ) diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go index 76d0a68a1da..c2c5bc9ef92 100644 --- a/pkg/services/apiserver/standalone/factory.go +++ b/pkg/services/apiserver/standalone/factory.go @@ -1,168 +1,28 @@ package standalone import ( - "context" - "fmt" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/prometheus/client_golang/prometheus" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - genericapiserver "k8s.io/apiserver/pkg/server" - - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/registry/apis/datasource" - "github.com/grafana/grafana/pkg/registry/apis/query" - "github.com/grafana/grafana/pkg/registry/apis/query/client" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/apiserver/options" - "github.com/grafana/grafana/pkg/services/featuremgmt" - testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" + "github.com/urfave/cli/v2" ) +type BuildInfo struct { + Version string + Commit string + EnterpriseCommit string + BuildBranch string + BuildStamp string +} + type APIServerFactory interface { - // Called before the groups are loaded so any custom command can be registered - GetOptions() options.OptionsProvider - - // Given the flags, what can we produce - GetEnabled(runtime []RuntimeConfig) ([]schema.GroupVersion, error) - - // Optional override for apiserver's BuildHandlerChainFunc, return nil if you want to use the grafana's default chain func defined in pkg/services/apiserver/builder/helper.go - GetBuildHandlerChainFunc(tracer tracing.Tracer, builders []builder.APIGroupBuilder) builder.BuildHandlerChainFunc - - // Make an API server for a given group+version - MakeAPIServer(ctx context.Context, tracer tracing.Tracer, gv schema.GroupVersion) (builder.APIGroupBuilder, error) - - Shutdown() + GetCLICommand(info BuildInfo) *cli.Command } -// Zero dependency provider for testing -func GetDummyAPIFactory() APIServerFactory { - return &DummyAPIFactory{} +// NOOP +func ProvideAPIServerFactory() APIServerFactory { + return &NoOpAPIServerFactory{} } -type DummyAPIFactory struct{} +type NoOpAPIServerFactory struct{} -func (p *DummyAPIFactory) GetOptions() options.OptionsProvider { +func (f *NoOpAPIServerFactory) GetCLICommand(info BuildInfo) *cli.Command { return nil } - -func (p *DummyAPIFactory) GetBuildHandlerChainFunc(_ tracing.Tracer, builders []builder.APIGroupBuilder) builder.BuildHandlerChainFunc { - return nil -} - -func (p *DummyAPIFactory) GetEnabled(runtime []RuntimeConfig) ([]schema.GroupVersion, error) { - gv := []schema.GroupVersion{} - for _, cfg := range runtime { - if !cfg.Enabled { - return nil, fmt.Errorf("only enabled supported now") - } - if cfg.Group == "all" { - return nil, fmt.Errorf("all not yet supported") - } - gv = append(gv, schema.GroupVersion{Group: cfg.Group, Version: cfg.Version}) - } - return gv, nil -} - -func (p *DummyAPIFactory) ApplyTo(config *genericapiserver.RecommendedConfig) error { - return nil -} - -func (p *DummyAPIFactory) MakeAPIServer(_ context.Context, tracer tracing.Tracer, gv schema.GroupVersion) (builder.APIGroupBuilder, error) { - if gv.Version != "v0alpha1" { - return nil, fmt.Errorf("only alpha supported now") - } - - switch gv.Group { - // Only works with testdata - case "query.grafana.app": - return query.NewQueryAPIBuilder( - featuremgmt.WithFeatures(), - &query.CommonDataSourceClientSupplier{ - Client: client.NewTestDataClient(), - }, - client.NewTestDataRegistry(), - nil, // legacy lookup - prometheus.NewRegistry(), // ??? - tracer, - ) - - case "testdata.datasource.grafana.app": - return datasource.NewDataSourceAPIBuilder( - plugins.JSONData{ - ID: "grafana-testdata-datasource", - }, - testdatasource.ProvideService(), // the client - &pluginDatasourceImpl{ - startup: v1.Now(), - }, - &pluginDatasourceImpl{}, // stub - &actest.FakeAccessControl{ExpectedEvaluate: true}, - true, // show query types - ) - } - - return nil, fmt.Errorf("unsupported group") -} - -func (p *DummyAPIFactory) Shutdown() {} - -// Simple stub for standalone datasource testing -type pluginDatasourceImpl struct { - startup v1.Time -} - -var ( - _ datasource.PluginDatasourceProvider = (*pluginDatasourceImpl)(nil) -) - -// Get implements PluginDatasourceProvider. -func (p *pluginDatasourceImpl) Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) { - all, err := p.List(ctx) - if err != nil { - return nil, err - } - for idx, v := range all.Items { - if v.Name == uid { - return &all.Items[idx], nil - } - } - return nil, fmt.Errorf("not found") -} - -// List implements PluginConfigProvider. -func (p *pluginDatasourceImpl) List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - return &v0alpha1.DataSourceConnectionList{ - TypeMeta: v0alpha1.GenericConnectionResourceInfo.TypeMeta(), - Items: []v0alpha1.DataSourceConnection{ - { - ObjectMeta: v1.ObjectMeta{ - Name: "PD8C576611E62080A", - Namespace: info.Value, // the raw namespace value - CreationTimestamp: p.startup, - }, - Title: "gdev-testdata", - }, - }, - }, nil -} - -// PluginContextForDataSource implements PluginConfigProvider. -func (*pluginDatasourceImpl) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) { - return &backend.DataSourceInstanceSettings{}, nil -} - -// PluginContextWrapper -func (*pluginDatasourceImpl) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) { - return backend.PluginContext{DataSourceInstanceSettings: datasourceSettings}, nil -} From 40bcd0df412c2dff9966ed15b3a9f99670b865f1 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 27 Sep 2024 08:48:56 +0200 Subject: [PATCH 011/174] LibraryElements: export GetAllElements to service (#93782) --- pkg/api/dashboard_test.go | 5 +++++ pkg/services/libraryelements/libraryelements.go | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 2ad87a053c6..7f3b8dce121 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1081,3 +1081,8 @@ func (l *mockLibraryElementService) DisconnectElementsFromDashboard(c context.Co func (l *mockLibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error { return nil } + +// GetAll gets all library elements with support to query filters. +func (l *mockLibraryElementService) GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) { + return model.LibraryElementSearchResult{}, nil +} diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index 6108e585ef0..02fb5f0ad73 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -40,6 +40,7 @@ type Service interface { ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error + GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) } // LibraryElementService is the service for the Library Element feature. @@ -85,6 +86,11 @@ func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, return l.deleteLibraryElementsInFolderUID(c, signedInUser, folderUID) } +// GetAll gets all library elements with support to query filters. +func (l *LibraryElementService) GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) { + return l.getAllLibraryElements(c, signedInUser, query) +} + func (l *LibraryElementService) addUidToLibraryPanel(model []byte, newUid string) (json.RawMessage, error) { var modelMap map[string]any err := json.Unmarshal(model, &modelMap) From 2cfba519f1d7dcafac8efda469419400fef21360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Sep 2024 09:10:49 +0200 Subject: [PATCH 012/174] DashboardScene: Fixes urlsync issue when going from normal to home dashboard (#93758) * DashboardScene: Fixes urlsync issue when going from normal to home dashboard * Better fix * Update --- .../dashboard-scene/pages/DashboardScenePage.tsx | 13 ++++++------- .../pages/DashboardScenePageStateManager.test.ts | 16 ++++++++++++++++ .../pages/DashboardScenePageStateManager.ts | 2 +- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index f74e53c0d89..ada4df7acac 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -1,5 +1,6 @@ // Libraries import { useEffect, useMemo } from 'react'; +import { usePrevious } from 'react-use'; import { PageLayoutType } from '@grafana/data'; import { UrlSyncContextProvider } from '@grafana/scenes'; @@ -19,8 +20,8 @@ import { getDashboardScenePageStateManager } from './DashboardScenePageStateMana export interface Props extends GrafanaRouteComponentProps {} export function DashboardScenePage({ match, route, queryParams, history }: Props) { + const prevMatch = usePrevious(match); const stateManager = getDashboardScenePageStateManager(); - const { dashboard, isLoading, loadError } = stateManager.useState(); // After scene migration is complete and we get rid of old dashboard we should refactor dashboardWatcher so this route reload is not need @@ -83,12 +84,10 @@ export function DashboardScenePage({ match, route, queryParams, history }: Props } // Do not render anything when transitioning from one dashboard to another - if ( - match.params.type !== 'snapshot' && - match.params.uid && - dashboard.state.uid !== match.params.uid && - route.routeName !== DashboardRoutes.Home - ) { + // A bit tricky for transition to or from Home dashbord that does not have a uid in the url (but could have it in the dashboard model) + // if prevMatch is undefined we are going from normal route to home route or vice versa + if (match.params.type !== 'snapshot' && (!prevMatch || match.params.uid !== prevMatch?.params.uid)) { + console.log('skipping rendering'); return null; } diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 3c0c7b5a2ed..25509df7fc3 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -40,6 +40,22 @@ describe('DashboardScenePageStateManager', () => { expect(loader.state.loadError).toBe('Dashboard not found'); }); + it('should clear current dashboard while loading next', async () => { + setupLoadDashboardMock({ dashboard: { uid: 'fake-dash', editable: true }, meta: {} }); + + const loader = new DashboardScenePageStateManager({}); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); + + expect(loader.state.dashboard).toBeDefined(); + + setupLoadDashboardMock({ dashboard: { uid: 'fake-dash2', editable: true }, meta: {} }); + + loader.loadDashboard({ uid: 'fake-dash2', route: DashboardRoutes.Normal }); + + expect(loader.state.isLoading).toBe(true); + expect(loader.state.dashboard).toBeUndefined(); + }); + it('shoud fetch dashboard from local storage and remove it after if it exists', async () => { const loader = new DashboardScenePageStateManager({}); const localStorageDashboard = { uid: 'fake-dash' }; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 089cfa0f417..1a04ca0c0d9 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -208,7 +208,7 @@ export class DashboardScenePageStateManager extends StateManagerBase Date: Fri, 27 Sep 2024 09:11:59 +0200 Subject: [PATCH 013/174] ManagedServiceAccounts: Add a config option to disable the feature on-prem (#93571) * ManagedServiceAccounts: Add a config option to disabled by default * Update log in pkg/services/extsvcauth/registry/service.go Co-authored-by: Ieva --- conf/defaults.ini | 3 ++ conf/sample.ini | 3 ++ pkg/api/plugins.go | 4 +-- pkg/services/accesscontrol/acimpl/service.go | 4 +-- .../accesscontrol/acimpl/service_test.go | 2 ++ pkg/services/extsvcauth/registry/service.go | 30 ++++++++++++------- .../extsvcauth/registry/service_test.go | 3 +- .../serviceregistration.go | 6 ++-- pkg/services/serviceaccounts/api/api.go | 3 +- .../serviceaccounts/extsvcaccounts/service.go | 12 ++++---- .../extsvcaccounts/service_test.go | 3 +- pkg/services/serviceaccounts/proxy/service.go | 2 +- pkg/setting/setting.go | 5 ++++ 13 files changed, 53 insertions(+), 27 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 3e7fc20ef90..fc2f2eeaa19 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -615,6 +615,9 @@ id_response_header_prefix = X-Grafana # The header value will encode the namespace ("user:", "api-key:", "service-account:") id_response_header_namespaces = user api-key service-account +# Enables the use of managed service accounts for plugin authentication +managed_service_accounts_enabled = false + #################################### SSO Settings ########################### [sso_settings] # interval for reloading the SSO Settings from the database diff --git a/conf/sample.ini b/conf/sample.ini index a36d17ff618..236ad499da5 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -619,6 +619,9 @@ # The header value will encode the namespace ("user:", "api-key:", "service-account:") ;id_response_header_namespaces = user api-key service-account +# Enables the use of managed service accounts for plugin authentication +; managed_service_accounts_enabled = false + #################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 256c50d5f6b..f10f1a48d2b 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -144,7 +144,7 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons AngularDetected: pluginDef.Angular.Detected, } - if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { + if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { listItem.IAM = pluginDef.IAM } @@ -484,7 +484,7 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons return response.ErrOrFallback(http.StatusInternalServerError, "Failed to install plugin", err) } - if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { + if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { // This is a non-blocking function that verifies that the installer has // the permissions that the plugin requests to have on Grafana. // If we want to make this blocking, the check will have to happen before or during the installation. diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 50616a33d3f..f9300fc1545 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -735,7 +735,7 @@ func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.SaveExternalServiceRole") defer span.End() - if !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.") return nil } @@ -751,7 +751,7 @@ func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalService ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeleteExternalServiceRole") defer span.End() - if !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.") return nil } diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index 7096b2fcac0..4c1dc562d8b 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -900,6 +900,7 @@ func TestService_SaveExternalServiceRole(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() ac := setupTestEnv(t) + ac.cfg.ManagedServiceAccountsEnabled = true ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts) for _, r := range tt.runs { err := ac.SaveExternalServiceRole(ctx, r.cmd) @@ -946,6 +947,7 @@ func TestService_DeleteExternalServiceRole(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() ac := setupTestEnv(t) + ac.cfg.ManagedServiceAccountsEnabled = true ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts) if tt.initCmd != nil { diff --git a/pkg/services/extsvcauth/registry/service.go b/pkg/services/extsvcauth/registry/service.go index cd6c603bcd8..9990ce15b87 100644 --- a/pkg/services/extsvcauth/registry/service.go +++ b/pkg/services/extsvcauth/registry/service.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" + "github.com/grafana/grafana/pkg/setting" ) var _ extsvcauth.ExternalServiceRegistry = &Registry{} @@ -28,9 +29,9 @@ type serverLocker interface { } type Registry struct { - features featuremgmt.FeatureToggles - logger log.Logger - saReg extsvcauth.ExternalServiceRegistry + enabled bool + logger log.Logger + saReg extsvcauth.ExternalServiceRegistry // FIXME (gamab): we can remove this field and use the saReg.GetExternalServiceNames directly extSvcProviders map[string]extsvcauth.AuthProvider @@ -38,10 +39,11 @@ type Registry struct { serverLock serverLocker } -func ProvideExtSvcRegistry(saSvc *extsvcaccounts.ExtSvcAccountsService, serverLock *serverlock.ServerLockService, features featuremgmt.FeatureToggles) *Registry { +func ProvideExtSvcRegistry(cfg *setting.Cfg, saSvc *extsvcaccounts.ExtSvcAccountsService, serverLock *serverlock.ServerLockService, features featuremgmt.FeatureToggles) *Registry { + enabled := features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) && cfg.ManagedServiceAccountsEnabled return &Registry{ extSvcProviders: map[string]extsvcauth.AuthProvider{}, - features: features, + enabled: enabled, lock: sync.Mutex{}, logger: log.New("extsvcauth.registry"), saReg: saSvc, @@ -110,8 +112,12 @@ func (r *Registry) RemoveExternalService(ctx context.Context, name string) error switch provider { case extsvcauth.ServiceAccounts: - if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { - r.logger.Debug("Skipping External Service removal, flag disabled", "service", name, "flag", featuremgmt.FlagExternalServiceAccounts) + if !r.enabled { + r.logger.Warn("Skipping External Service authentication, flag or configuration option is disabled", + "service", name, + "flag", featuremgmt.FlagExternalServiceAccounts, + "option", "ManagedServiceAccountsEnabled", + ) return nil } r.logger.Debug("Routing External Service removal to the External Service Account service", "service", name) @@ -140,8 +146,12 @@ func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.Exte switch cmd.AuthProvider { case extsvcauth.ServiceAccounts: - if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { - ctxLogger.Warn("Skipping External Service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAccounts) + if !r.enabled { + ctxLogger.Warn("Skipping External Service authentication, flag or configuration option disabled", + "service", cmd.Name, + "flag", featuremgmt.FlagExternalServiceAccounts, + "option", "ManagedServiceAccountsEnabled", + ) return } ctxLogger.Debug("Routing the External Service registration to the External Service Account service", "service", cmd.Name) @@ -160,7 +170,7 @@ func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.Exte // retrieveExtSvcProviders fetches external services from store and map their associated provider func (r *Registry) retrieveExtSvcProviders(ctx context.Context) (map[string]extsvcauth.AuthProvider, error) { extsvcs := map[string]extsvcauth.AuthProvider{} - if r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if r.enabled { names, err := r.saReg.GetExternalServiceNames(ctx) if err != nil { return nil, err diff --git a/pkg/services/extsvcauth/registry/service_test.go b/pkg/services/extsvcauth/registry/service_test.go index 40c046f4ceb..30b5ef81138 100644 --- a/pkg/services/extsvcauth/registry/service_test.go +++ b/pkg/services/extsvcauth/registry/service_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/extsvcauth/tests" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -30,7 +29,7 @@ func setupTestEnv(t *testing.T) *TestEnv { env := TestEnv{} env.saReg = tests.NewExternalServiceRegistryMock(t) env.r = &Registry{ - features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts), + enabled: true, logger: log.New("extsvcauth.registry.test"), saReg: env.saReg, extSvcProviders: map[string]extsvcauth.AuthProvider{}, diff --git a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go index f80ca8213bc..90eb7e67a61 100644 --- a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go +++ b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" + "github.com/grafana/grafana/pkg/setting" ) type Service struct { @@ -20,9 +21,10 @@ type Service struct { settingsSvc pluginsettings.Service } -func ProvideService(features featuremgmt.FeatureToggles, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { +func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { + enabled := features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) && cfg.ManagedServiceAccountsEnabled s := &Service{ - featureEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), + featureEnabled: enabled, log: log.New("plugins.external.registration"), reg: reg, settingsSvc: settingsSvc, diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index a0db940a2bc..322fc34d9ad 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -40,6 +40,7 @@ func NewServiceAccountsAPI( permissionService accesscontrol.ServiceAccountPermissionsService, features featuremgmt.FeatureToggles, ) *ServiceAccountsAPI { + enabled := features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) && cfg.ManagedServiceAccountsEnabled return &ServiceAccountsAPI{ cfg: cfg, service: service, @@ -48,7 +49,7 @@ func NewServiceAccountsAPI( RouterRegister: routerRegister, log: log.New("serviceaccounts.api"), permissionService: permissionService, - isExternalSAEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), + isExternalSAEnabled: enabled, } } diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service.go b/pkg/services/serviceaccounts/extsvcaccounts/service.go index d4802d993cc..bd4b7ff9cf3 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service.go @@ -28,12 +28,12 @@ import ( type ExtSvcAccountsService struct { acSvc ac.Service defaultOrgID int64 - features featuremgmt.FeatureToggles logger log.Logger metrics *metrics saSvc sa.Service skvStore kvstore.SecretsKVStore tracer tracing.Tracer + enabled bool } func ProvideExtSvcAccountsService(acSvc ac.Service, cfg *setting.Cfg, bus bus.Bus, db db.DB, features featuremgmt.FeatureToggles, reg prometheus.Registerer, saSvc *manager.ServiceAccountsService, secretsSvc secrets.Service, tracer tracing.Tracer) *ExtSvcAccountsService { @@ -43,12 +43,12 @@ func ProvideExtSvcAccountsService(acSvc ac.Service, cfg *setting.Cfg, bus bus.Bu defaultOrgID: cfg.DefaultOrgID(), logger: logger, saSvc: saSvc, - features: features, skvStore: kvstore.NewSQLSecretsKVStore(db, secretsSvc, logger), // Using SQL store to avoid a cyclic dependency tracer: tracer, + enabled: cfg.ManagedServiceAccountsEnabled && features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), } - if features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) { + if esa.enabled { // Register the metrics esa.metrics = newMetrics(reg, esa.defaultOrgID, saSvc, logger) @@ -138,7 +138,7 @@ func (esa *ExtSvcAccountsService) GetExternalServiceNames(ctx context.Context) ( // SaveExternalService creates, updates or delete a service account (and its token) with the requested permissions. func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !esa.enabled { esa.logger.FromContext(ctx).Warn("This feature is behind a feature flag, please set it if you want to save external services") return nil, nil } @@ -184,7 +184,7 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd * func (esa *ExtSvcAccountsService) RemoveExternalService(ctx context.Context, name string) error { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !esa.enabled { esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services") return nil } @@ -225,7 +225,7 @@ func (esa *ExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, orgID // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *sa.ManageExtSvcAccountCmd) (int64, error) { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !esa.enabled { esa.logger.FromContext(ctx).Warn("This feature is behind a feature flag, please set it if you want to save external services") return 0, nil } diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go index dbb1fa67c4e..c687b717ed9 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go @@ -41,6 +41,7 @@ func setupTestEnv(t *testing.T) *TestEnv { t.Helper() cfg := setting.NewCfg() + cfg.ManagedServiceAccountsEnabled = true fmgt := featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts) env := &TestEnv{ @@ -50,13 +51,13 @@ func setupTestEnv(t *testing.T) *TestEnv { } logger := log.New("extsvcaccounts.test") env.S = &ExtSvcAccountsService{ + enabled: true, acSvc: acimpl.ProvideOSSService( cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{}, localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, nil, permreg.ProvidePermissionRegistry(), ), defaultOrgID: autoAssignOrgID, - features: fmgt, logger: logger, metrics: newMetrics(nil, autoAssignOrgID, env.SaSvc, logger), saSvc: env.SaSvc, diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index 3cb5f404721..8da7af34a41 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -38,7 +38,7 @@ func ProvideServiceAccountsProxy( s := &ServiceAccountsProxy{ log: log.New("serviceaccounts.proxy"), proxiedService: proxiedService, - isProxyEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), + isProxyEnabled: cfg.ManagedServiceAccountsEnabled && features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), } serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, accesscontrolService, routeRegister, permissionService, features) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 2488ea5fddd..306ccff0392 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -245,6 +245,7 @@ type Cfg struct { IDResponseHeaderEnabled bool IDResponseHeaderPrefix string IDResponseHeaderNamespaces map[string]struct{} + ManagedServiceAccountsEnabled bool // AWS Plugin Auth AWSAllowedAuthProviders []string @@ -1668,6 +1669,10 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { for _, provider := range util.SplitString(providers) { cfg.SSOSettingsConfigurableProviders[provider] = true } + + // Managed Service Accounts + cfg.ManagedServiceAccountsEnabled = auth.Key("managed_service_accounts_enabled").MustBool(false) + return nil } From 826245f511d7508f8f50d02cf88c8f6195ed7746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Fri, 27 Sep 2024 09:22:38 +0200 Subject: [PATCH 014/174] CloudMigrations: Avoid building GMS base path when provided (#93793) Avoid building GMS base path when provided --- .../cloudmigration/gmsclient/gms_client.go | 2 +- .../cloudmigration/gmsclient/gms_client_test.go | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/services/cloudmigration/gmsclient/gms_client.go b/pkg/services/cloudmigration/gmsclient/gms_client.go index d9804d4e157..935ee003e53 100644 --- a/pkg/services/cloudmigration/gmsclient/gms_client.go +++ b/pkg/services/cloudmigration/gmsclient/gms_client.go @@ -297,7 +297,7 @@ func (c *gmsClientImpl) ReportEvent(ctx context.Context, session cloudmigration. func (c *gmsClientImpl) buildBasePath(clusterSlug string) string { domain := c.cfg.CloudMigration.GMSDomain - if strings.HasPrefix(domain, "http://localhost") { + if strings.HasPrefix(domain, "http://") || strings.HasPrefix(domain, "https://") { return domain } return fmt.Sprintf("https://cms-%s.%s/cloud-migrations", clusterSlug, domain) diff --git a/pkg/services/cloudmigration/gmsclient/gms_client_test.go b/pkg/services/cloudmigration/gmsclient/gms_client_test.go index 0e04bb0ad75..a2eb92d76cb 100644 --- a/pkg/services/cloudmigration/gmsclient/gms_client_test.go +++ b/pkg/services/cloudmigration/gmsclient/gms_client_test.go @@ -35,13 +35,19 @@ func Test_buildBasePath(t *testing.T) { expected string }{ { - description: "domain starts with http://localhost, should return domain", - domain: "http://localhost:8080", + description: "domain starts with http://, should return domain", + domain: "http://some-domain:8080", clusterSlug: "anything", - expected: "http://localhost:8080", + expected: "http://some-domain:8080", }, { - description: "domain doesn't start with http://localhost, should build a string using the domain and clusterSlug", + description: "domain starts with https://, should return domain", + domain: "https://some-domain:8080", + clusterSlug: "anything", + expected: "https://some-domain:8080", + }, + { + description: "domain doesn't start with http or https, should build a string using the domain and clusterSlug", domain: "gms-dev", clusterSlug: "us-east-1", expected: "https://cms-us-east-1.gms-dev/cloud-migrations", From 3dbba3e50978a1397629792e3647b66561ca8964 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:07:48 +0200 Subject: [PATCH 015/174] Alerting: Fix Grafana recording rules expressions (#93878) fix grafana recording rules expressions --- .../query-and-alert-condition/QueryAndExpressionsStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 65ffe897e4a..794c9be09ec 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -601,7 +601,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P /> )} {/* Expression Queries */} - {isAdvancedMode && isGrafanaAlertingType && ( + {isAdvancedMode && ( <> Expressions From 52611d4d028a7cd8dde7b8180346e22c423ecc1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 09:42:03 +0100 Subject: [PATCH 016/174] Update dependency webpack-dev-server to v5.1.0 (#93845) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 58 ++++++++++++++++------------------------------------ 2 files changed, 19 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 29d395637b3..3f61438cb59 100644 --- a/package.json +++ b/package.json @@ -238,7 +238,7 @@ "webpack-assets-manifest": "^5.1.0", "webpack-bundle-analyzer": "4.10.2", "webpack-cli": "5.1.4", - "webpack-dev-server": "5.0.4", + "webpack-dev-server": "5.1.0", "webpack-manifest-plugin": "5.0.0", "webpack-merge": "5.10.0", "webpackbar": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index c553f58b6ac..7f212e21804 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15437,15 +15437,6 @@ __metadata: languageName: node linkType: hard -"default-gateway@npm:^6.0.3": - version: 6.0.3 - resolution: "default-gateway@npm:6.0.3" - dependencies: - execa: "npm:^5.0.0" - checksum: 10/126f8273ecac8ee9ff91ea778e8784f6cd732d77c3157e8c5bdd6ed03651b5291f71446d05bc02d04073b1e67583604db5394ea3cf992ede0088c70ea15b7378 - languageName: node - linkType: hard - "defaults@npm:^1.0.3": version: 1.0.3 resolution: "defaults@npm:1.0.3" @@ -17474,7 +17465,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.3": +"express@npm:^4.17.3, express@npm:^4.19.2": version: 4.21.0 resolution: "express@npm:4.21.0" dependencies: @@ -18612,7 +18603,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:10.4.1, glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": +"glob@npm:10.4.1, glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10": version: 10.4.1 resolution: "glob@npm:10.4.1" dependencies: @@ -19139,7 +19130,7 @@ __metadata: webpack-assets-manifest: "npm:^5.1.0" webpack-bundle-analyzer: "npm:4.10.2" webpack-cli: "npm:5.1.4" - webpack-dev-server: "npm:5.0.4" + webpack-dev-server: "npm:5.1.0" webpack-manifest-plugin: "npm:5.0.0" webpack-merge: "npm:5.10.0" webpackbar: "npm:^6.0.0" @@ -28805,17 +28796,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^5.0.5": - version: 5.0.7 - resolution: "rimraf@npm:5.0.7" - dependencies: - glob: "npm:^10.3.7" - bin: - rimraf: dist/esm/bin.mjs - checksum: 10/1e3cecfe59ee2383dfd9ba5373caeed48ed941318a0360119419b7dffc63115661408b9427f67e1f66b5bbb8855a3953db09e55a7362b3df904a44453dfa22fb - languageName: node - linkType: hard - "rimraf@npm:~2.6.2": version: 2.6.3 resolution: "rimraf@npm:2.6.3" @@ -32770,9 +32750,9 @@ __metadata: languageName: node linkType: hard -"webpack-dev-middleware@npm:^7.1.0": - version: 7.1.0 - resolution: "webpack-dev-middleware@npm:7.1.0" +"webpack-dev-middleware@npm:^7.4.2": + version: 7.4.2 + resolution: "webpack-dev-middleware@npm:7.4.2" dependencies: colorette: "npm:^2.0.10" memfs: "npm:^4.6.0" @@ -32785,13 +32765,13 @@ __metadata: peerDependenciesMeta: webpack: optional: true - checksum: 10/9be007cad293051995c7a6a0a385a43a8e1a38c9d23954249add76c3beb9279993375b7af2d52c989c88a57a049e2971803feea835726a850e94e978dc6eac79 + checksum: 10/608d101b82081a5bc6c0237f9945e14a8eefce1664c10877f3feb0042710f6c8b4288b07986505f791302d81b3c51180f679b97c91c3cdabd3fd0687a464ca1c languageName: node linkType: hard -"webpack-dev-server@npm:5.0.4": - version: 5.0.4 - resolution: "webpack-dev-server@npm:5.0.4" +"webpack-dev-server@npm:5.1.0": + version: 5.1.0 + resolution: "webpack-dev-server@npm:5.1.0" dependencies: "@types/bonjour": "npm:^3.5.13" "@types/connect-history-api-fallback": "npm:^1.5.4" @@ -32806,8 +32786,7 @@ __metadata: colorette: "npm:^2.0.10" compression: "npm:^1.7.4" connect-history-api-fallback: "npm:^2.0.0" - default-gateway: "npm:^6.0.3" - express: "npm:^4.17.3" + express: "npm:^4.19.2" graceful-fs: "npm:^4.2.6" html-entities: "npm:^2.4.0" http-proxy-middleware: "npm:^2.0.3" @@ -32815,14 +32794,13 @@ __metadata: launch-editor: "npm:^2.6.1" open: "npm:^10.0.3" p-retry: "npm:^6.2.0" - rimraf: "npm:^5.0.5" schema-utils: "npm:^4.2.0" selfsigned: "npm:^2.4.1" serve-index: "npm:^1.9.1" sockjs: "npm:^0.3.24" spdy: "npm:^4.0.2" - webpack-dev-middleware: "npm:^7.1.0" - ws: "npm:^8.16.0" + webpack-dev-middleware: "npm:^7.4.2" + ws: "npm:^8.18.0" peerDependencies: webpack: ^5.0.0 peerDependenciesMeta: @@ -32832,7 +32810,7 @@ __metadata: optional: true bin: webpack-dev-server: bin/webpack-dev-server.js - checksum: 10/3896866abf15a1d5cc31ab4fc9c36aacf3431356837ad6debe25cde29289a70c58dcbe40914bbb275ff455463d37437532093d0e8d7744e7643ce1364491fdb4 + checksum: 10/f23255681cc5e2c2709b23ca7b2185aeed83b1c9912657d4512eda8685625a46d7a103a92446494a55fe2afdfab936f9bd4f037d20b52f7fdfff303e7e7199c7 languageName: node linkType: hard @@ -33297,9 +33275,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.16.0, ws@npm:^8.2.3, ws@npm:^8.9.0": - version: 8.17.1 - resolution: "ws@npm:8.17.1" +"ws@npm:^8.18.0, ws@npm:^8.2.3, ws@npm:^8.9.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -33308,7 +33286,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d + checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 languageName: node linkType: hard From f22bee8ca20525417dbc7169cc97285d2d31061b Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Fri, 27 Sep 2024 11:00:13 +0200 Subject: [PATCH 017/174] Build: Migrate packages to rollup v4 (#93731) * chore(packages): bump rollup and rollup plugins to latest * chore(packages): fix rollup node-externals plugin imports * chore(packages): update build/bundle scripts to pass configPlugin arg to rollup * feat(packages): migrate rollup configs to be esm compliant * feat(packages): build using es2018 target and use same tsconfig and tsc for rollup --- packages/grafana-data/package.json | 10 +- packages/grafana-data/rollup.config.ts | 15 +- packages/grafana-e2e-selectors/package.json | 12 +- .../grafana-e2e-selectors/rollup.config.ts | 15 +- packages/grafana-flamegraph/package.json | 12 +- packages/grafana-flamegraph/rollup.config.ts | 15 +- packages/grafana-icons/package.json | 14 +- packages/grafana-icons/rollup.config.ts | 15 +- packages/grafana-prometheus/package.json | 12 +- packages/grafana-prometheus/rollup.config.ts | 16 +- packages/grafana-runtime/package.json | 12 +- packages/grafana-runtime/rollup.config.ts | 15 +- packages/grafana-schema/package.json | 12 +- packages/grafana-schema/rollup.config.ts | 25 +- packages/grafana-ui/package.json | 14 +- packages/grafana-ui/rollup.config.ts | 16 +- yarn.lock | 362 ++++++++++++------ 17 files changed, 392 insertions(+), 200 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 45378752c73..d72cadae714 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -28,7 +28,7 @@ "LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -75,10 +75,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "typescript": "5.5.4" }, "peerDependencies": { diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts index 1ac767eddcf..4c115636a7c 100644 --- a/packages/grafana-data/rollup.config.ts +++ b/packages/grafana-data/rollup.config.ts @@ -1,15 +1,24 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 022a12f9c4a..73973dd4318 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -31,8 +31,8 @@ "LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -43,10 +43,10 @@ "@types/node": "20.16.9", "esbuild": "0.24.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0" + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3" }, "dependencies": { "@grafana/tsconfig": "^2.0.0", diff --git a/packages/grafana-e2e-selectors/rollup.config.ts b/packages/grafana-e2e-selectors/rollup.config.ts index bf8b4ccd1eb..0aeaab2d194 100644 --- a/packages/grafana-e2e-selectors/rollup.config.ts +++ b/packages/grafana-e2e-selectors/rollup.config.ts @@ -1,15 +1,24 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 0849c031faf..e8daf20dabd 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -31,8 +31,8 @@ "./LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -76,10 +76,10 @@ "esbuild": "0.24.0", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "ts-jest": "29.2.5", "ts-node": "10.9.2", "typescript": "5.5.4" diff --git a/packages/grafana-flamegraph/rollup.config.ts b/packages/grafana-flamegraph/rollup.config.ts index 0dd3c9f2f65..12a2bfd60b0 100644 --- a/packages/grafana-flamegraph/rollup.config.ts +++ b/packages/grafana-flamegraph/rollup.config.ts @@ -1,15 +1,24 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index 056688c86d7..98cb0c4bb0d 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -31,13 +31,13 @@ "typecheck": "yarn generate && tsc --emitDeclarationOnly false --noEmit", "lint": "eslint --ext .ts,.tsx ./src", "prettier:check": "prettier --check --list-different=false --log-level=warn \"**/*.{ts,tsx,scss,md,mdx,json}\"", - "build": "yarn generate && rollup -c rollup.config.ts" + "build": "yarn generate && rollup -c rollup.config.ts --configPlugin esbuild" }, "devDependencies": { "@babel/core": "7.25.2", "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.6", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-typescript": "^12.1.0", "@svgr/babel-plugin-remove-jsx-attribute": "^8.0.0", "@svgr/cli": "^8.1.0", "@svgr/core": "8.1.0", @@ -53,10 +53,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^6.1.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "7.1.3", "ts-node": "10.9.2", "typescript": "5.5.4" }, diff --git a/packages/grafana-icons/rollup.config.ts b/packages/grafana-icons/rollup.config.ts index cfbe94f8ac4..da80c2e17a8 100644 --- a/packages/grafana-icons/rollup.config.ts +++ b/packages/grafana-icons/rollup.config.ts @@ -1,15 +1,24 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import externals from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -import pkg from './package.json'; +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'esm', diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index ad5afa4225b..f57c7b17792 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -28,8 +28,8 @@ "access": "public" }, "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -127,10 +127,10 @@ "react-dom": "18.2.0", "react-select-event": "5.5.1", "react-test-renderer": "18.2.0", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "sass": "1.79.3", "sass-loader": "14.2.1", "style-loader": "4.0.0", diff --git a/packages/grafana-prometheus/rollup.config.ts b/packages/grafana-prometheus/rollup.config.ts index 080514c27d5..b25d1c770e5 100644 --- a/packages/grafana-prometheus/rollup.config.ts +++ b/packages/grafana-prometheus/rollup.config.ts @@ -1,16 +1,26 @@ import image from '@rollup/plugin-image'; import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild(), image()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + image(), + ], output: [ { format: 'cjs', diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index f773eb75175..a690d8d5395 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -29,8 +29,8 @@ "LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -66,10 +66,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "rollup-plugin-sourcemaps": "0.6.3", "typescript": "5.5.4" }, diff --git a/packages/grafana-runtime/rollup.config.ts b/packages/grafana-runtime/rollup.config.ts index 241d425641c..00d7077dad1 100644 --- a/packages/grafana-runtime/rollup.config.ts +++ b/packages/grafana-runtime/rollup.config.ts @@ -1,15 +1,24 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 39c8d34ab20..6b0ca630ad3 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -28,8 +28,8 @@ "LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -41,10 +41,10 @@ "esbuild": "0.24.0", "glob": "^11.0.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "typescript": "5.5.4" }, "dependencies": { diff --git a/packages/grafana-schema/rollup.config.ts b/packages/grafana-schema/rollup.config.ts index d75af54dbfc..158619ae9c6 100644 --- a/packages/grafana-schema/rollup.config.ts +++ b/packages/grafana-schema/rollup.config.ts @@ -1,17 +1,26 @@ import resolve from '@rollup/plugin-node-resolve'; -import glob from 'glob'; +import { glob } from 'glob'; +import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', @@ -45,7 +54,13 @@ export default [ fileURLToPath(new URL(file, import.meta.url)), ]) ), - plugins: [resolve(), esbuild()], + plugins: [ + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: { format: 'esm', dir: path.dirname(pkg.publishConfig.module), diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 3c82160a6fc..7499d50b06b 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -32,8 +32,8 @@ "./LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "storybook": "storybook dev -p 9001 -c .storybook --no-open", "storybook:build": "storybook build -o ./dist/storybook -c .storybook", @@ -174,12 +174,12 @@ "react-select-event": "^5.1.0", "react-test-renderer": "18.2.0", "rimraf": "6.0.1", - "rollup": "2.79.1", + "rollup": "^4.22.4", "rollup-plugin-copy": "3.5.0", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", - "rollup-plugin-svg-import": "1.6.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", + "rollup-plugin-svg-import": "3.0.0", "sass-loader": "14.2.1", "storybook": "^8.1.6", "storybook-dark-mode": "^4.0.1", diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts index 76891bf60c7..cba330e83ec 100644 --- a/packages/grafana-ui/rollup.config.ts +++ b/packages/grafana-ui/rollup.config.ts @@ -1,14 +1,15 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import copy from 'rollup-plugin-copy'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; import svg from 'rollup-plugin-svg-import'; -const icons = require('../../public/app/core/icons/cached.json'); - -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const icons = rq('../../public/app/core/icons/cached.json'); +const pkg = rq('./package.json'); const iconSrcPaths = icons.map((iconSubPath) => { return `../../public/img/icons/${iconSubPath}.svg`; @@ -18,14 +19,17 @@ export default [ { input: 'src/index.ts', plugins: [ - externals({ deps: true, packagePath: './package.json' }), + nodeExternals({ deps: true, packagePath: './package.json' }), svg({ stringify: true }), resolve(), copy({ targets: [{ src: iconSrcPaths, dest: './dist/public/' }], flatten: false, }), - esbuild(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), ], output: [ { diff --git a/yarn.lock b/yarn.lock index 7f212e21804..7d904aa60d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,7 +82,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.22.5, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.24.7": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.24.7": version: 7.24.7 resolution: "@babel/code-frame@npm:7.24.7" dependencies: @@ -3612,10 +3612,10 @@ __metadata: react-dom: "npm:18.2.0" react-use: "npm:17.5.1" rimraf: "npm:6.0.1" - rollup: "npm:2.79.1" - rollup-plugin-dts: "npm:^5.0.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:^5.0.0" + rollup: "npm:^4.22.4" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-node-externals: "npm:^7.1.3" rxjs: "npm:7.8.1" string-hash: "npm:^1.1.3" tinycolor2: "npm:1.6.0" @@ -3638,10 +3638,10 @@ __metadata: "@types/node": "npm:20.16.9" esbuild: "npm:0.24.0" rimraf: "npm:6.0.1" - rollup: "npm:2.79.1" - rollup-plugin-dts: "npm:^5.0.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:^5.0.0" + rollup: "npm:^4.22.4" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-node-externals: "npm:^7.1.3" tslib: "npm:2.7.0" typescript: "npm:5.5.4" languageName: unknown @@ -3817,10 +3817,10 @@ __metadata: react: "npm:18.2.0" react-use: "npm:17.5.1" react-virtualized-auto-sizer: "npm:1.0.24" - rollup: "npm:2.79.1" - rollup-plugin-dts: "npm:^5.0.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:^5.0.0" + rollup: "npm:^4.22.4" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-node-externals: "npm:^7.1.3" tinycolor2: "npm:1.6.0" ts-jest: "npm:29.2.5" ts-node: "npm:10.9.2" @@ -4028,10 +4028,10 @@ __metadata: react-test-renderer: "npm:18.2.0" react-use: "npm:17.5.1" react-window: "npm:1.8.10" - rollup: "npm:2.79.1" - rollup-plugin-dts: "npm:^5.0.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:^5.0.0" + rollup: "npm:^4.22.4" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-node-externals: "npm:^7.1.3" rxjs: "npm:7.8.1" sass: "npm:1.79.3" sass-loader: "npm:14.2.1" @@ -4079,10 +4079,10 @@ __metadata: react: "npm:18.2.0" react-dom: "npm:18.2.0" rimraf: "npm:6.0.1" - rollup: "npm:2.79.1" - rollup-plugin-dts: "npm:^5.0.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:^5.0.0" + rollup: "npm:^4.22.4" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-node-externals: "npm:^7.1.3" rollup-plugin-sourcemaps: "npm:0.6.3" rxjs: "npm:7.8.1" tslib: "npm:2.7.0" @@ -4099,8 +4099,8 @@ __metadata: dependencies: "@babel/core": "npm:7.25.2" "@grafana/tsconfig": "npm:^2.0.0" - "@rollup/plugin-node-resolve": "npm:^15.2.3" - "@rollup/plugin-typescript": "npm:^11.1.6" + "@rollup/plugin-node-resolve": "npm:^15.3.0" + "@rollup/plugin-typescript": "npm:^12.1.0" "@svgr/babel-plugin-remove-jsx-attribute": "npm:^8.0.0" "@svgr/cli": "npm:^8.1.0" "@svgr/core": "npm:8.1.0" @@ -4116,10 +4116,10 @@ __metadata: react: "npm:18.2.0" react-dom: "npm:18.2.0" rimraf: "npm:6.0.1" - rollup: "npm:2.79.1" - rollup-plugin-dts: "npm:^6.1.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:5.0.0" + rollup: "npm:^4.22.4" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-node-externals: "npm:7.1.3" ts-node: "npm:10.9.2" typescript: "npm:5.5.4" peerDependencies: @@ -4160,10 +4160,10 @@ __metadata: esbuild: "npm:0.24.0" glob: "npm:^11.0.0" rimraf: "npm:6.0.1" - rollup: "npm:2.79.1" - rollup-plugin-dts: "npm:^5.0.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:^5.0.0" + rollup: "npm:^4.22.4" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-node-externals: "npm:^7.1.3" tslib: "npm:2.7.0" typescript: "npm:5.5.4" languageName: unknown @@ -4350,12 +4350,12 @@ __metadata: react-use: "npm:17.5.1" react-window: "npm:1.8.10" rimraf: "npm:6.0.1" - rollup: "npm:2.79.1" + rollup: "npm:^4.22.4" rollup-plugin-copy: "npm:3.5.0" - rollup-plugin-dts: "npm:^5.0.0" - rollup-plugin-esbuild: "npm:5.0.0" - rollup-plugin-node-externals: "npm:^5.0.0" - rollup-plugin-svg-import: "npm:1.6.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-node-externals: "npm:^7.1.3" + rollup-plugin-svg-import: "npm:3.0.0" rxjs: "npm:7.8.1" sass-loader: "npm:14.2.1" slate: "npm:0.47.9" @@ -6944,7 +6944,7 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-node-resolve@npm:15.3.0, @rollup/plugin-node-resolve@npm:^15.2.3": +"@rollup/plugin-node-resolve@npm:15.3.0, @rollup/plugin-node-resolve@npm:^15.3.0": version: 15.3.0 resolution: "@rollup/plugin-node-resolve@npm:15.3.0" dependencies: @@ -6978,9 +6978,9 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-typescript@npm:^11.1.6": - version: 11.1.6 - resolution: "@rollup/plugin-typescript@npm:11.1.6" +"@rollup/plugin-typescript@npm:^12.1.0": + version: 12.1.0 + resolution: "@rollup/plugin-typescript@npm:12.1.0" dependencies: "@rollup/pluginutils": "npm:^5.1.0" resolve: "npm:^1.22.1" @@ -6993,7 +6993,7 @@ __metadata: optional: true tslib: optional: true - checksum: 10/4ae4d6cfc929393171288df2f18b5eb837fa53d8689118d9661b3064567341f6f6cf8389af55f1d5f015e3682abf30a64ab609fdf75ecb5a84224505e407eb69 + checksum: 10/93e67032377278be3658988423588f2941eb55ccb540312ab847c050ea62a57d056d3f80c292bf463e90cbc71795498805120a0f244040d8304ba57d9bb8c09e languageName: node linkType: hard @@ -7010,19 +7010,9 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^4.1.1": - version: 4.2.1 - resolution: "@rollup/pluginutils@npm:4.2.1" - dependencies: - estree-walker: "npm:^2.0.1" - picomatch: "npm:^2.2.2" - checksum: 10/503a6f0a449e11a2873ac66cfdfb9a3a0b77ffa84c5cad631f5e4bc1063c850710e8d5cd5dab52477c0d66cda2ec719865726dbe753318cd640bab3fff7ca476 - languageName: node - linkType: hard - -"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.1.0": - version: 5.1.0 - resolution: "@rollup/pluginutils@npm:5.1.0" +"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0": + version: 5.1.2 + resolution: "@rollup/pluginutils@npm:5.1.2" dependencies: "@types/estree": "npm:^1.0.0" estree-walker: "npm:^2.0.2" @@ -7032,7 +7022,119 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 10/abb15eaec5b36f159ec351b48578401bedcefdfa371d24a914cfdbb1e27d0ebfbf895299ec18ccc343d247e71f2502cba21202bc1362d7ef27d5ded699e5c2b2 + checksum: 10/cc1fe3285ab48915a6535ab2f0c90dc511bd3e63143f8e9994bb036c6c5071fd14d641cff6c89a7fde6a4faa85227d4e2cf46ee36b7d962099e0b9e4c9b8a4b0 + languageName: node + linkType: hard + +"@rollup/rollup-android-arm-eabi@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.22.4" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-android-arm64@npm:4.22.4" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-darwin-arm64@npm:4.22.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-darwin-x64@npm:4.22.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.22.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.22.4" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.22.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.22.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.22.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.22.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.22.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.22.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.22.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.22.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.22.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.22.4": + version: 4.22.4 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.22.4" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -9792,7 +9894,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": +"@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" checksum: 10/7de6d928dd4010b0e20c6919e1a6c27b61f8d4567befa89252055fad503d587ecb9a1e3eab1b1901f923964d7019796db810b7fd6430acb26c32866d126fd408 @@ -16311,7 +16413,7 @@ __metadata: languageName: node linkType: hard -"es-module-lexer@npm:^1.0.5, es-module-lexer@npm:^1.2.1, es-module-lexer@npm:^1.5.0, es-module-lexer@npm:^1.5.3": +"es-module-lexer@npm:^1.2.1, es-module-lexer@npm:^1.3.1, es-module-lexer@npm:^1.5.0, es-module-lexer@npm:^1.5.3": version: 1.5.4 resolution: "es-module-lexer@npm:1.5.4" checksum: 10/f29c7c97a58eb17640dcbd71bd6ef754ad4f58f95c3073894573d29dae2cad43ecd2060d97ed5b866dfb7804d5590fb7de1d2c5339a5fceae8bd60b580387fc5 @@ -17306,7 +17408,7 @@ __metadata: languageName: node linkType: hard -"estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2": +"estree-walker@npm:^2.0.2": version: 2.0.2 resolution: "estree-walker@npm:2.0.2" checksum: 10/b02109c5d46bc2ed47de4990eef770f7457b1159a229f0999a09224d2b85ffeed2d7679cffcff90aeb4448e94b0168feb5265b209cdec29aad50a3d6e93d21e2 @@ -18422,12 +18524,12 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.7.0": - version: 4.7.3 - resolution: "get-tsconfig@npm:4.7.3" +"get-tsconfig@npm:^4.7.0, get-tsconfig@npm:^4.7.2": + version: 4.8.1 + resolution: "get-tsconfig@npm:4.8.1" dependencies: resolve-pkg-maps: "npm:^1.0.0" - checksum: 10/7397bb4f8aef936df4d9016555b662dcf5279f3c46428b7c7c1ff5e94ab2b87d018b3dda0f4bc1a28b154d5affd0eac5d014511172c085fd8a9cdff9ea7fe043 + checksum: 10/3fb5a8ad57b9633eaea085d81661e9e5c9f78b35d8f8689eaf8b8b45a2a3ebf3b3422266d4d7df765e308cc1e6231648d114803ab3d018332e29916f2c1de036 languageName: node linkType: hard @@ -21564,13 +21666,6 @@ __metadata: languageName: node linkType: hard -"joycon@npm:^3.1.1": - version: 3.1.1 - resolution: "joycon@npm:3.1.1" - checksum: 10/4b36e3479144ec196425f46b3618f8a96ce7e1b658f091a309cd4906215f5b7a402d7df331a3e0a09681381a658d0c5f039cb3cf6907e0a1e17ed847f5d37775 - languageName: node - linkType: hard - "jquery@npm:3.7.1": version: 3.7.1 resolution: "jquery@npm:3.7.1" @@ -21853,7 +21948,7 @@ __metadata: languageName: node linkType: hard -"jsonc-parser@npm:3.2.0, jsonc-parser@npm:^3.2.0": +"jsonc-parser@npm:3.2.0": version: 3.2.0 resolution: "jsonc-parser@npm:3.2.0" checksum: 10/bd68b902e5f9394f01da97921f49c5084b2dc03a0c5b4fdb2a429f8d6f292686c1bf87badaeb0a8148d024192a88f5ad2e57b2918ba43fe25cf15f3371db64d4 @@ -22729,7 +22824,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.10, magic-string@npm:^0.30.2, magic-string@npm:^0.30.5": +"magic-string@npm:^0.30.10, magic-string@npm:^0.30.5": version: 0.30.10 resolution: "magic-string@npm:0.30.10" dependencies: @@ -28827,23 +28922,7 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-dts@npm:^5.0.0": - version: 5.3.1 - resolution: "rollup-plugin-dts@npm:5.3.1" - dependencies: - "@babel/code-frame": "npm:^7.22.5" - magic-string: "npm:^0.30.2" - peerDependencies: - rollup: ^3.0 - typescript: ^4.1 || ^5.0 - dependenciesMeta: - "@babel/code-frame": - optional: true - checksum: 10/490c39d88aa61448d3ca8200abceda59f6c6d79a75f31b8e8ecbd9c8528a0ab0724f115c9ce67fba4243a819e26841163fcc7c66cd429e55a1686a8e7029ee5e - languageName: node - linkType: hard - -"rollup-plugin-dts@npm:^6.1.0": +"rollup-plugin-dts@npm:^6.1.1": version: 6.1.1 resolution: "rollup-plugin-dts@npm:6.1.1" dependencies: @@ -28859,37 +28938,27 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-esbuild@npm:5.0.0": - version: 5.0.0 - resolution: "rollup-plugin-esbuild@npm:5.0.0" +"rollup-plugin-esbuild@npm:6.1.1": + version: 6.1.1 + resolution: "rollup-plugin-esbuild@npm:6.1.1" dependencies: - "@rollup/pluginutils": "npm:^5.0.1" + "@rollup/pluginutils": "npm:^5.0.5" debug: "npm:^4.3.4" - es-module-lexer: "npm:^1.0.5" - joycon: "npm:^3.1.1" - jsonc-parser: "npm:^3.2.0" + es-module-lexer: "npm:^1.3.1" + get-tsconfig: "npm:^4.7.2" peerDependencies: - esbuild: ">=0.10.1" - rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 - checksum: 10/94b8dcad27eada7a7422aa8dbee4784452d2598fccc60555a3639d86e8872e9d440b5debcd26883389695cf03d61891ba8b076a7a35c6bf4beddbdd0be404642 + esbuild: ">=0.18.0" + rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + checksum: 10/bba2d1dfb92a193823ac9dd1cdd44a8fd8cd9f25868e9a22ca077e1b7445feb4eaaf6df051148e367fc902d7d59c9f50efab49086c24c367972f05c86f3a656d languageName: node linkType: hard -"rollup-plugin-node-externals@npm:5.0.0": - version: 5.0.0 - resolution: "rollup-plugin-node-externals@npm:5.0.0" +"rollup-plugin-node-externals@npm:7.1.3, rollup-plugin-node-externals@npm:^7.1.3": + version: 7.1.3 + resolution: "rollup-plugin-node-externals@npm:7.1.3" peerDependencies: - rollup: ^2.60.0 - checksum: 10/745df84e554dc8b779ade79d88e1e21b30780653b9e454f8591163d87d55519bc71219a7f7a994ed32a3f2d5f9eb498be0649a78b42992806c272597bf49f207 - languageName: node - linkType: hard - -"rollup-plugin-node-externals@npm:^5.0.0": - version: 5.0.2 - resolution: "rollup-plugin-node-externals@npm:5.0.2" - peerDependencies: - rollup: ^2.60.0 || ^3.0.0 - checksum: 10/65d24e6f174d170b855912f230e2dbb9debee4c81d2819e88391f324cc190a84f2c1ab4b421688145786df822434e40b6203524be497832b2f1201dc3728ef77 + rollup: ^3.0.0 || ^4.0.0 + checksum: 10/4e8d38ebc3c8a29cb72d11a73f97ba810a4560b5b0f0be445ba32180875073ce4752a9ea4d0b658717ddae642d314738047677e5c818b1893f3d715d18fe413c languageName: node linkType: hard @@ -28909,28 +28978,77 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-svg-import@npm:1.6.0": - version: 1.6.0 - resolution: "rollup-plugin-svg-import@npm:1.6.0" +"rollup-plugin-svg-import@npm:3.0.0": + version: 3.0.0 + resolution: "rollup-plugin-svg-import@npm:3.0.0" dependencies: - "@rollup/pluginutils": "npm:^4.1.1" + "@rollup/pluginutils": "npm:^5.0.1" peerDependencies: - rollup: ">=1.29.0 <3.0.0" - checksum: 10/453862c39d2301563d9d07f6647c295377ff66cf3174d2a0612389fda4bfd9fa72718d90279feb782d3525f1d6f9710e1dc78641cd9d4044360a0179f88054b0 + rollup: ^3.0.0||^4.0.0 + checksum: 10/4489b8cd702eeb82b693cb4b11557cbb24c4bac68f8c5997ad7288a998b9db821c11d4682cd2892dc17ddb19228ee725de274b396b262d5fef4e05066cd07616 languageName: node linkType: hard -"rollup@npm:2.79.1": - version: 2.79.1 - resolution: "rollup@npm:2.79.1" +"rollup@npm:^4.22.4": + version: 4.22.4 + resolution: "rollup@npm:4.22.4" dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.22.4" + "@rollup/rollup-android-arm64": "npm:4.22.4" + "@rollup/rollup-darwin-arm64": "npm:4.22.4" + "@rollup/rollup-darwin-x64": "npm:4.22.4" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.22.4" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.22.4" + "@rollup/rollup-linux-arm64-gnu": "npm:4.22.4" + "@rollup/rollup-linux-arm64-musl": "npm:4.22.4" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.22.4" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.22.4" + "@rollup/rollup-linux-s390x-gnu": "npm:4.22.4" + "@rollup/rollup-linux-x64-gnu": "npm:4.22.4" + "@rollup/rollup-linux-x64-musl": "npm:4.22.4" + "@rollup/rollup-win32-arm64-msvc": "npm:4.22.4" + "@rollup/rollup-win32-ia32-msvc": "npm:4.22.4" + "@rollup/rollup-win32-x64-msvc": "npm:4.22.4" + "@types/estree": "npm:1.0.5" fsevents: "npm:~2.3.2" dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true fsevents: optional: true bin: rollup: dist/bin/rollup - checksum: 10/df087b701304432f30922bbee5f534ab189aa6938bd383b5686c03147e0d00cd1789ea10a462361326ce6b6ebe448ce272ad3f3cc40b82eeb3157df12f33663c + checksum: 10/0fbee8c14d9052624c76a09fe79ed4d46024832be3ceea86c69f1521ae84b581a64c6e6596fdd796030c206835987e1a0a3be85f4c0d35b71400be5dce799d12 languageName: node linkType: hard From 6137a755521baac56a36464a164064474f6fa578 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Fri, 27 Sep 2024 11:07:02 +0200 Subject: [PATCH 018/174] Docs: document the `managed_service_accounts_enabled` configuration option (#93883) * Config: Disclaimer single-org support for managed service accounts * Add docs update * Update docs/sources/setup-grafana/configure-grafana/_index.md --- conf/defaults.ini | 1 + conf/sample.ini | 1 + docs/sources/setup-grafana/configure-grafana/_index.md | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index fc2f2eeaa19..c29619e741e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -616,6 +616,7 @@ id_response_header_prefix = X-Grafana id_response_header_namespaces = user api-key service-account # Enables the use of managed service accounts for plugin authentication +# This feature currently **only supports single-organization deployments** managed_service_accounts_enabled = false #################################### SSO Settings ########################### diff --git a/conf/sample.ini b/conf/sample.ini index 236ad499da5..aa74245e2c1 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -620,6 +620,7 @@ ;id_response_header_namespaces = user api-key service-account # Enables the use of managed service accounts for plugin authentication +# This feature currently **only supports single-organization deployments** ; managed_service_accounts_enabled = false #################################### Anonymous Auth ###################### diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 8e74e21186f..a0089a233fe 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1090,6 +1090,16 @@ Set to `true` to enable verbose request signature logging when AWS Signature Ver
+### managed_service_accounts_enabled + +> Only available in Grafana 11.3+. + +Set to `true` to enable the use of managed service accounts for plugin authentication. Default is `false`. + +> **Limitations:** +> This feature currently **only supports single-organization deployments**. +> The plugin's service account is automatically created in the default organization. This means the plugin can only access data and resources within that specific organization. + ## [auth.anonymous] Refer to [Anonymous authentication]({{< relref "../configure-security/configure-authentication/grafana#anonymous-authentication" >}}) for detailed instructions. From 08dab3f81600f73e9a8756ac752f37cf1ec611a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:48:11 +0100 Subject: [PATCH 019/174] Update dependency eslint-plugin-react to v7.37.0 (#93891) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 3f61438cb59..f02237ac485 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "eslint-plugin-jsx-a11y": "6.10.0", "eslint-plugin-lodash": "7.4.0", "eslint-plugin-no-barrel-files": "^1.1.0", - "eslint-plugin-react": "7.36.1", + "eslint-plugin-react": "7.37.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-testing-library": "^6.2.2", "eslint-scope": "^8.0.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index f57c7b17792..573cf60102a 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -114,7 +114,7 @@ "eslint-plugin-jsdoc": "48.11.0", "eslint-plugin-jsx-a11y": "6.10.0", "eslint-plugin-lodash": "7.4.0", - "eslint-plugin-react": "7.36.1", + "eslint-plugin-react": "7.37.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", diff --git a/yarn.lock b/yarn.lock index 7d904aa60d1..cab80717a2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4001,7 +4001,7 @@ __metadata: eslint-plugin-jsdoc: "npm:48.11.0" eslint-plugin-jsx-a11y: "npm:6.10.0" eslint-plugin-lodash: "npm:7.4.0" - eslint-plugin-react: "npm:7.36.1" + eslint-plugin-react: "npm:7.37.0" eslint-plugin-react-hooks: "npm:4.6.0" eslint-webpack-plugin: "npm:4.2.0" eventemitter3: "npm:5.0.1" @@ -17133,9 +17133,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react@npm:7.36.1": - version: 7.36.1 - resolution: "eslint-plugin-react@npm:7.36.1" +"eslint-plugin-react@npm:7.37.0": + version: 7.37.0 + resolution: "eslint-plugin-react@npm:7.37.0" dependencies: array-includes: "npm:^3.1.8" array.prototype.findlast: "npm:^1.2.5" @@ -17157,7 +17157,7 @@ __metadata: string.prototype.repeat: "npm:^1.0.0" peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - checksum: 10/bca154b446c35af4859a92fd043dcfe5c74851eb27652234020548570bb81d37cc9f1eb1795b3c9e7514de6c9b48f42fcc00153062eca879dab45ab84e49d0b1 + checksum: 10/ae005a5e4bdcbf43cda0e5297f5ee8badbbcc18ce6c3f83ac4141173242b27c8a067372061af745ae490c18eef2c3257985bcd240cee85dec262ca875347e8fc languageName: node linkType: hard @@ -19085,7 +19085,7 @@ __metadata: eslint-plugin-jsx-a11y: "npm:6.10.0" eslint-plugin-lodash: "npm:7.4.0" eslint-plugin-no-barrel-files: "npm:^1.1.0" - eslint-plugin-react: "npm:7.36.1" + eslint-plugin-react: "npm:7.37.0" eslint-plugin-react-hooks: "npm:4.6.0" eslint-plugin-testing-library: "npm:^6.2.2" eslint-scope: "npm:^8.0.0" From f49b4d35f225b4f342edb3df6ce099cf97fb662f Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:11:27 +0200 Subject: [PATCH 020/174] OAuth: Add custom unauthorized message option in configuration (#93717) * read custom message from config * Read error key from bootdata * oopsie * Remove console.log * Update docs and sample/default inis * Add default key value to the config --- conf/defaults.ini | 3 +++ conf/sample.ini | 3 +++ .../setup-grafana/configure-grafana/_index.md | 4 ++++ pkg/api/login_oauth.go | 6 ++---- pkg/setting/setting.go | 3 +++ public/app/core/components/Login/LoginCtrl.tsx | 12 +++++++++++- public/locales/en-US/grafana.json | 3 +++ public/locales/pseudo-LOCALE/grafana.json | 3 +++ 8 files changed, 32 insertions(+), 5 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index c29619e741e..8baa6296110 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -582,6 +582,9 @@ oauth_auto_login = false # OAuth state max age cookie duration in seconds. Defaults to 600 seconds. oauth_state_cookie_max_age = 600 +# Sets a custom oAuth error message. This is useful if you need to point the users to a specific location for support. +oauth_login_error_message = oauth.login.error + # Minimum wait time in milliseconds for the server lock retry mechanism. # The server lock retry mechanism is used to prevent multiple Grafana instances from # simultaneously refreshing OAuth tokens. This mechanism waits at least this amount diff --git a/conf/sample.ini b/conf/sample.ini index aa74245e2c1..099de883121 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -583,6 +583,9 @@ # Deprecated, use auto_login option for specific provider instead. ;oauth_auto_login = false +# Sets a custom oAuth error message. This is useful if you need to point the users to a specific location for support. +;oauth_login_error_message = oauth.login.error + # OAuth state max age cookie duration in seconds. Defaults to 600 seconds. ;oauth_state_cookie_max_age = 600 diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index a0089a233fe..de2e43e06c3 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -964,6 +964,10 @@ This setting is ignored if multiple OAuth providers are configured. Default is ` How many seconds the OAuth state cookie lives before being deleted. Default is `600` (seconds) Administrators can increase this if they experience OAuth login state mismatch errors. +### oauth_login_error_message + +A custom error message for when users are unauthorized. Default is a key for an internationalized phrase in the frontend, `Login provider denied login request`. + ### oauth_refresh_token_server_lock_min_wait_ms Minimum wait time in milliseconds for the server lock retry mechanism. Default is `1000` (milliseconds). The server lock retry mechanism is used to prevent multiple Grafana instances from simultaneously refreshing OAuth tokens. This mechanism waits at least this amount of time before retrying to acquire the server lock. diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 1fb9aaa3ce3..f59b09045a3 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -1,8 +1,7 @@ package api import ( - "errors" - + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/services/authn" @@ -21,8 +20,7 @@ func (hs *HTTPServer) OAuthLogin(reqCtx *contextmodel.ReqContext) { if errorParam := reqCtx.Query("error"); errorParam != "" { errorDesc := reqCtx.Query("error_description") hs.log.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc) - - hs.redirectWithError(reqCtx, errors.New("login provider denied login request"), "error", errorParam, "errorDesc", errorDesc) + hs.redirectWithError(reqCtx, errutil.Unauthorized("oauth.login", errutil.WithPublicMessage(hs.Cfg.OAuthLoginErrorMessage)).Errorf("Login provider denied login request")) return } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 306ccff0392..935254f907d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -263,6 +263,7 @@ type Cfg struct { // OAuth OAuthAutoLogin bool + OAuthLoginErrorMessage string OAuthCookieMaxAge int OAuthAllowInsecureEmailLookup bool OAuthRefreshTokenServerLockMinWaitMs int64 @@ -1621,6 +1622,8 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { cfg.Logger.Warn("[Deprecated] The oauth_auto_login configuration setting is deprecated. Please use auto_login inside auth provider section instead.") } + // Default to the translation key used in the frontend + cfg.OAuthLoginErrorMessage = valueAsString(auth, "oauth_login_error_message", "oauth.login.error") cfg.OAuthCookieMaxAge = auth.Key("oauth_state_cookie_max_age").MustInt(600) cfg.OAuthRefreshTokenServerLockMinWaitMs = auth.Key("oauth_refresh_token_server_lock_min_wait_ms").MustInt64(1000) cfg.SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "") diff --git a/public/app/core/components/Login/LoginCtrl.tsx b/public/app/core/components/Login/LoginCtrl.tsx index a8612613173..1a8a1abfd5b 100644 --- a/public/app/core/components/Login/LoginCtrl.tsx +++ b/public/app/core/components/Login/LoginCtrl.tsx @@ -51,7 +51,8 @@ export class LoginCtrl extends PureComponent { isLoggingIn: false, isChangingPassword: false, showDefaultPasswordWarning: false, - loginErrorMessage: config.loginError, + // oAuth unauthorized sets the redirect error message in the bootdata, hence we need to check the key here + loginErrorMessage: getBootDataErrMessage(config.loginError), }; } @@ -178,3 +179,12 @@ function getErrorMessage(err: FetchError Date: Fri, 27 Sep 2024 14:43:02 +0400 Subject: [PATCH 021/174] Chore: update ownership of grafana live FE code (#93823) update ownership of live FE --- .github/CODEOWNERS | 4 ++-- pkg/services/featuremgmt/registry.go | 4 ++-- pkg/services/featuremgmt/toggles_gen.csv | 4 ++-- pkg/services/featuremgmt/toggles_gen.json | 18 ++++++++++++------ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f00c143ce13..6f45dea46d8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -439,7 +439,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/invites/ @grafana/grafana-frontend-platform /public/app/features/library-panels/ @grafana/dashboards-squad /public/app/features/logs/ @grafana/observability-logs -/public/app/features/live/ @grafana/grafana-app-platform-squad +/public/app/features/live/ @grafana/dashboards-squad /public/app/features/apiserver/ @grafana/grafana-app-platform-squad /public/app/features/manage-dashboards/ @grafana/dashboards-squad /public/app/features/notifications/ @grafana/grafana-frontend-platform @@ -491,7 +491,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/plugins/panel/geomap/ @grafana/dataviz-squad /public/app/plugins/panel/canvas/ @grafana/dataviz-squad /public/app/plugins/panel/candlestick/ @grafana/dataviz-squad -/public/app/plugins/panel/live/ @grafana/grafana-app-platform-squad +/public/app/plugins/panel/live/ @grafana/dashboards-squad /public/app/plugins/panel/news/ @grafana/grafana-frontend-platform /public/app/plugins/panel/stat/ @grafana/dataviz-squad /public/app/plugins/panel/text/ @grafana/grafana-frontend-platform diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c87b872e64f..d42b8ac1ebd 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -32,14 +32,14 @@ var ( Description: "This will use a webworker thread to processes events rather than the main thread", Stage: FeatureStageExperimental, FrontendOnly: true, - Owner: grafanaAppPlatformSquad, + Owner: grafanaDashboardsSquad, }, { Name: "queryOverLive", Description: "Use Grafana Live WebSocket to execute backend queries", Stage: FeatureStageExperimental, FrontendOnly: true, - Owner: grafanaAppPlatformSquad, + Owner: grafanaDashboardsSquad, }, { Name: "panelTitleSearch", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ae8b748ca22..1aea6fced26 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -1,7 +1,7 @@ Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly disableEnvelopeEncryption,GA,@grafana/grafana-as-code,false,false,false -live-service-web-worker,experimental,@grafana/grafana-app-platform-squad,false,false,true -queryOverLive,experimental,@grafana/grafana-app-platform-squad,false,false,true +live-service-web-worker,experimental,@grafana/dashboards-squad,false,false,true +queryOverLive,experimental,@grafana/dashboards-squad,false,false,true panelTitleSearch,preview,@grafana/search-and-storage,false,false,false publicDashboards,GA,@grafana/sharing-squad,false,false,false publicDashboardsEmailSharing,preview,@grafana/sharing-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 24e8ef6522f..3cd366933c2 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1662,13 +1662,16 @@ { "metadata": { "name": "live-service-web-worker", - "resourceVersion": "1718727528075", - "creationTimestamp": "2022-01-26T17:44:20Z" + "resourceVersion": "1727432782383", + "creationTimestamp": "2022-01-26T17:44:20Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-27 10:26:22.38366 +0000 UTC" + } }, "spec": { "description": "This will use a webworker thread to processes events rather than the main thread", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", + "codeowner": "@grafana/dashboards-squad", "frontend": true } }, @@ -2523,13 +2526,16 @@ { "metadata": { "name": "queryOverLive", - "resourceVersion": "1718727528075", - "creationTimestamp": "2022-01-26T17:44:20Z" + "resourceVersion": "1727432782383", + "creationTimestamp": "2022-01-26T17:44:20Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-27 10:26:22.38366 +0000 UTC" + } }, "spec": { "description": "Use Grafana Live WebSocket to execute backend queries", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", + "codeowner": "@grafana/dashboards-squad", "frontend": true } }, From 012d62782cf7e16082765aac7b47f0896c48d8f2 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Fri, 27 Sep 2024 13:18:52 +0200 Subject: [PATCH 022/174] Fix: Prevent import errors caused by Rollup 4 (#93903) fix(packages): prevent import errors by injecting exports.esmodule and check for default prop --- packages/grafana-data/rollup.config.ts | 7 +++++++ packages/grafana-e2e-selectors/rollup.config.ts | 7 +++++++ packages/grafana-flamegraph/rollup.config.ts | 7 +++++++ packages/grafana-icons/rollup.config.ts | 6 ++++++ packages/grafana-prometheus/rollup.config.ts | 7 +++++++ packages/grafana-runtime/rollup.config.ts | 7 +++++++ packages/grafana-schema/rollup.config.ts | 7 +++++++ packages/grafana-ui/rollup.config.ts | 7 +++++++ 8 files changed, 55 insertions(+) diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts index 4c115636a7c..9894024f99c 100644 --- a/packages/grafana-data/rollup.config.ts +++ b/packages/grafana-data/rollup.config.ts @@ -8,6 +8,11 @@ import { nodeExternals } from 'rollup-plugin-node-externals'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', @@ -24,6 +29,7 @@ export default [ format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -32,6 +38,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-data/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-e2e-selectors/rollup.config.ts b/packages/grafana-e2e-selectors/rollup.config.ts index 0aeaab2d194..b70662c33a8 100644 --- a/packages/grafana-e2e-selectors/rollup.config.ts +++ b/packages/grafana-e2e-selectors/rollup.config.ts @@ -8,6 +8,11 @@ import { nodeExternals } from 'rollup-plugin-node-externals'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', @@ -24,6 +29,7 @@ export default [ format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -32,6 +38,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-e2e-selectors/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-flamegraph/rollup.config.ts b/packages/grafana-flamegraph/rollup.config.ts index 12a2bfd60b0..86d0ad86cfa 100644 --- a/packages/grafana-flamegraph/rollup.config.ts +++ b/packages/grafana-flamegraph/rollup.config.ts @@ -8,6 +8,11 @@ import { nodeExternals } from 'rollup-plugin-node-externals'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', @@ -24,6 +29,7 @@ export default [ format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -32,6 +38,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-ui/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-icons/rollup.config.ts b/packages/grafana-icons/rollup.config.ts index da80c2e17a8..6bf3c3b536e 100644 --- a/packages/grafana-icons/rollup.config.ts +++ b/packages/grafana-icons/rollup.config.ts @@ -8,6 +8,11 @@ import { nodeExternals } from 'rollup-plugin-node-externals'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', @@ -25,6 +30,7 @@ export default [ sourcemap: true, dir: path.dirname(pkg.publishConfig.main), preserveModules: true, + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-prometheus/rollup.config.ts b/packages/grafana-prometheus/rollup.config.ts index b25d1c770e5..7e630257c36 100644 --- a/packages/grafana-prometheus/rollup.config.ts +++ b/packages/grafana-prometheus/rollup.config.ts @@ -9,6 +9,11 @@ import { nodeExternals } from 'rollup-plugin-node-externals'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', @@ -26,6 +31,7 @@ export default [ format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -34,6 +40,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-prometheus/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-runtime/rollup.config.ts b/packages/grafana-runtime/rollup.config.ts index 00d7077dad1..25992a93441 100644 --- a/packages/grafana-runtime/rollup.config.ts +++ b/packages/grafana-runtime/rollup.config.ts @@ -8,6 +8,11 @@ import { nodeExternals } from 'rollup-plugin-node-externals'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', @@ -24,6 +29,7 @@ export default [ format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -32,6 +38,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-runtime/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-schema/rollup.config.ts b/packages/grafana-schema/rollup.config.ts index 158619ae9c6..6a2f736c944 100644 --- a/packages/grafana-schema/rollup.config.ts +++ b/packages/grafana-schema/rollup.config.ts @@ -10,6 +10,11 @@ import { nodeExternals } from 'rollup-plugin-node-externals'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', @@ -26,6 +31,7 @@ export default [ format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -34,6 +40,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-schema/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts index cba330e83ec..b18618dd7ea 100644 --- a/packages/grafana-ui/rollup.config.ts +++ b/packages/grafana-ui/rollup.config.ts @@ -15,6 +15,11 @@ const iconSrcPaths = icons.map((iconSubPath) => { return `../../public/img/icons/${iconSubPath}.svg`; }); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', @@ -36,6 +41,7 @@ export default [ format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -44,6 +50,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-ui/src`), + ...legacyOutputDefaults, }, ], }, From 51d73249b22b7f72c570752ec68fa2539b0e9a82 Mon Sep 17 00:00:00 2001 From: Sam Jewell <2903904+samjewell@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:33:24 +0100 Subject: [PATCH 023/174] Docs: Update Usage insights logs docs- Scope (#93425) Update Usage insights logs docs: Scope As far as I can tell, in https://github.com/grafana/grafana/pull/59931 we started to record Usage Insights events for Explore queries. And in https://github.com/grafana/grafana/pull/78097 we further improved our implementation of that logging. This documentation should have been updated back then to match. So I'm updating it now. --- .../sources/setup-grafana/configure-security/export-logs.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-security/export-logs.md b/docs/sources/setup-grafana/configure-security/export-logs.md index 907c980a4f9..ba942393530 100644 --- a/docs/sources/setup-grafana/configure-security/export-logs.md +++ b/docs/sources/setup-grafana/configure-security/export-logs.md @@ -32,7 +32,11 @@ Usage insights logs are JSON objects that represent certain user activities, suc ### Scope -A log is created every time a user opens a dashboard or when a query is sent to a data source in the dashboard view. A query that is performed via Explore does not generate a log. +A log is created every time: + +- A user opens a dashboard. +- A query is sent to a data source in the dashboard view. +- A query is performed via Explore. ### Format From 3437c8be7f88fc17f1f540c51c0a05fef0ce5e17 Mon Sep 17 00:00:00 2001 From: Roman Pertl <533172+roock@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:00:57 +0200 Subject: [PATCH 024/174] Docs: grafana image renderer instructions custom cert in container (#93646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com> --- .../image-rendering/troubleshooting/index.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md index 34d220c51b2..ece628061d3 100644 --- a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md +++ b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md @@ -125,6 +125,22 @@ If this happens, then you have to add the certificate to the trust store. If you certutil –addstore "Root" /internal-root-ca.crt.pem ``` +**Container:** + +```Dockerfile +FROM grafana/grafana-image-renderer:latest + +USER root + +RUN apk add --no-cache nss-tools + +USER grafana + +COPY internal-root-ca.crt.pem /etc/pki/tls/certs/internal-root-ca.crt.pem +RUN mkdir -p /home/grafana/.pki/nssdb +RUN certutil -d sql:/home/grafana/.pki/nssdb -A -n internal-root-ca -t C -i /etc/pki/tls/certs/internal-root-ca.crt.pem +``` + ## Custom Chrome/Chromium As a last resort, if you already have [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/) From fcb17379ea03d5606a9fa58f89fd07a65adcaa1a Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 27 Sep 2024 14:22:29 +0200 Subject: [PATCH 025/174] LibraryElements: add fake service implementation and replace its usage in Dashboard API (#93783) * LibraryElements: add fake service implementation * Dashboards: replace fake LibraryElements implementation --- pkg/api/dashboard_test.go | 50 +------ .../fake/libraryelements_service.go | 125 +++++++++++++++++ .../fake/libraryelements_service_test.go | 132 ++++++++++++++++++ 3 files changed, 264 insertions(+), 43 deletions(-) create mode 100644 pkg/services/libraryelements/fake/libraryelements_service.go create mode 100644 pkg/services/libraryelements/fake/libraryelements_service_test.go diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 7f3b8dce121..ad246fec912 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -42,7 +42,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" - "github.com/grafana/grafana/pkg/services/libraryelements/model" + libraryelementsfake "github.com/grafana/grafana/pkg/services/libraryelements/fake" "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/live" @@ -271,7 +271,7 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { hs.starService = startest.NewStarServiceFake() hs.LibraryPanelService = &mockLibraryPanelService{} - hs.LibraryElementService = &mockLibraryElementService{} + hs.LibraryElementService = &libraryelementsfake.LibraryElementService{} pubDashService := publicdashboards.NewFakePublicDashboardService(t) pubDashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil).Maybe() @@ -677,7 +677,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { Cfg: setting.NewCfg(), ProvisioningService: fakeProvisioningService, LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: mockDashboardProvisioningService{}, SQLStore: mockSQLStore, AccessControl: accesscontrolmock.New(), @@ -827,7 +827,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr } libraryPanelsService := mockLibraryPanelService{} - libraryElementsService := mockLibraryElementService{} + libraryElementsService := libraryelementsfake.LibraryElementService{} cfg := setting.NewCfg() ac := accesscontrolmock.New() folderPermissions := accesscontrolmock.NewMockedPermissionsService() @@ -909,7 +909,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s QuotaService: quotatest.New(false, nil), pluginStore: &pluginstore.FakePluginStore{}, LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: dashboardService, folderService: folderService, Features: featuremgmt.WithFeatures(), @@ -947,7 +947,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, SQLStore: sqlmock, dashboardVersionService: fakeDashboardVersionService, Features: featuremgmt.WithFeatures(), @@ -990,7 +990,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: mock, SQLStore: sqlStore, Features: featuremgmt.WithFeatures(), @@ -1050,39 +1050,3 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Con func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { return nil } - -type mockLibraryElementService struct{} - -func (l *mockLibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { - return model.LibraryElementDTO{}, nil -} - -// GetElement gets an element from a UID. -func (l *mockLibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { - return model.LibraryElementDTO{}, nil -} - -// GetElementsForDashboard gets all connected elements for a specific dashboard. -func (l *mockLibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) { - return map[string]model.LibraryElementDTO{}, nil -} - -// ConnectElementsToDashboard connects elements to a specific dashboard. -func (l *mockLibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error { - return nil -} - -// DisconnectElementsFromDashboard disconnects elements from a specific dashboard. -func (l *mockLibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error { - return nil -} - -// DeleteLibraryElementsInFolder deletes all elements for a specific folder. -func (l *mockLibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error { - return nil -} - -// GetAll gets all library elements with support to query filters. -func (l *mockLibraryElementService) GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) { - return model.LibraryElementSearchResult{}, nil -} diff --git a/pkg/services/libraryelements/fake/libraryelements_service.go b/pkg/services/libraryelements/fake/libraryelements_service.go new file mode 100644 index 00000000000..e8f0b3615d5 --- /dev/null +++ b/pkg/services/libraryelements/fake/libraryelements_service.go @@ -0,0 +1,125 @@ +package fake + +import ( + "context" + "sync" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/libraryelements/model" + "github.com/grafana/grafana/pkg/util" +) + +// LibraryElementService is a fake with only the required methods implemented while the others are stubbed. +type LibraryElementService struct { + elements map[string]model.LibraryElementDTO + mx sync.RWMutex + idCounter int64 +} + +var _ libraryelements.Service = (*LibraryElementService)(nil) + +func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { + l.mx.Lock() + defer l.mx.Unlock() + + if len(l.elements) == 0 { + l.elements = make(map[string]model.LibraryElementDTO, 0) + } + + var orgID int64 = 1 + if signedInOrgID := signedInUser.GetOrgID(); signedInOrgID != 0 { + orgID = signedInOrgID + } + + var folderUID string + if cmd.FolderUID != nil { + folderUID = *cmd.FolderUID + } + + createUID := cmd.UID + if len(createUID) == 0 { + createUID = util.GenerateShortUID() + } + + if _, exists := l.elements[createUID]; exists { + return model.LibraryElementDTO{}, model.ErrLibraryElementAlreadyExists + } + + l.idCounter++ + + dto := model.LibraryElementDTO{ + ID: l.idCounter, + OrgID: orgID, + FolderID: cmd.FolderID, //nolint: staticcheck + FolderUID: folderUID, + UID: createUID, + Name: cmd.Name, + Kind: cmd.Kind, + Type: "text", + Description: "A description", + Model: cmd.Model, + Version: 1, + Meta: model.LibraryElementDTOMeta{}, + } + + l.elements[createUID] = dto + + return dto, nil +} + +func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { + l.mx.RLock() + defer l.mx.RUnlock() + + libraryElement, exists := l.elements[cmd.UID] + if !exists { + return model.LibraryElementDTO{}, model.ErrLibraryElementNotFound + } + + return libraryElement, nil +} + +func (l *LibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) { + return map[string]model.LibraryElementDTO{}, nil +} + +func (l *LibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error { + return nil +} + +func (l *LibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error { + return nil +} + +func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error { + return nil +} + +func (l *LibraryElementService) GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) { + elements := make([]model.LibraryElementDTO, 0, len(l.elements)) + + var orgID int64 = 1 + if signedInOrgID := signedInUser.GetOrgID(); signedInOrgID != 0 { + orgID = signedInOrgID + } + + l.mx.RLock() + defer l.mx.RUnlock() + + for _, element := range l.elements { + if element.OrgID != orgID { + continue + } + + elements = append(elements, element) + } + + // For this fake ignore pagination to make it simpler. + return model.LibraryElementSearchResult{ + TotalCount: int64(len(elements)), + Elements: elements, + Page: 1, + PerPage: len(elements), + }, nil +} diff --git a/pkg/services/libraryelements/fake/libraryelements_service_test.go b/pkg/services/libraryelements/fake/libraryelements_service_test.go new file mode 100644 index 00000000000..a246e4c6e2b --- /dev/null +++ b/pkg/services/libraryelements/fake/libraryelements_service_test.go @@ -0,0 +1,132 @@ +package fake_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/libraryelements/fake" + "github.com/grafana/grafana/pkg/services/libraryelements/model" +) + +func TestLibraryElementService(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("GetElement", func(t *testing.T) { + t.Parallel() + + user := &identity.StaticRequester{} + + t.Run("when the element does not exist, it returns a `model.ErrLibraryElementNotFound` error", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + element, err := svc.GetElement(ctx, user, model.GetLibraryElementCommand{UID: "does-not-exist"}) + require.ErrorIs(t, err, model.ErrLibraryElementNotFound) + require.Empty(t, element) + }) + + t.Run("when the element exists, it returns it", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + uid := "uid-1" + + createdElement, err := svc.CreateElement(ctx, user, model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + UID: uid, + }) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + + element, err := svc.GetElement(ctx, user, model.GetLibraryElementCommand{UID: uid}) + require.NoError(t, err) + require.EqualValues(t, element, createdElement) + }) + }) + + t.Run("CreateElement", func(t *testing.T) { + t.Parallel() + + user := &identity.StaticRequester{} + + t.Run("when the uid already exists, it returns a `model.ErrLibraryElementAlreadyExists` error", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + UID: "uid-1", + } + + createdElement, err := svc.CreateElement(ctx, user, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + + createdElement, err = svc.CreateElement(ctx, user, cmd) + require.ErrorIs(t, err, model.ErrLibraryElementAlreadyExists) + require.Empty(t, createdElement) + }) + + t.Run("when the uid is not passed in, it generates a new one", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + } + + createdElement, err := svc.CreateElement(ctx, user, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + require.NotEmpty(t, createdElement.UID) + }) + }) + + t.Run("GetAllElements", func(t *testing.T) { + t.Parallel() + + t.Run("only returns the elements belonging to the requestor org", func(t *testing.T) { + t.Parallel() + + user1 := &identity.StaticRequester{OrgID: 1} + user2 := &identity.StaticRequester{OrgID: 2} + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + } + + createdElement1, err := svc.CreateElement(ctx, user1, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement1) + + createdElement2, err := svc.CreateElement(ctx, user2, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement2) + + result, err := svc.GetAllElements(ctx, user2, model.SearchLibraryElementsQuery{}) + require.NoError(t, err) + require.Len(t, result.Elements, 1) + require.Equal(t, createdElement2.UID, result.Elements[0].UID) + }) + }) +} From db42af20ca8875fbe3410ddd49e0effba6d8594a Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 27 Sep 2024 14:27:16 +0200 Subject: [PATCH 026/174] Alerting: Prometheus primary mode for the alert list page (#92975) * Lazy loading of mimir groups * Refactor rule statuses * Use prometheus endpoint to populate namespace and group dropdowns * Add a feature toggle * Use lazy loading ruler rules if the feature toggle enabled * Remove unnecessary props form dynamic table * Remove query from hash calculation * Conditionally load ns and group autocompletions from Prom or Ruler APIs * Fix prometheus dto labels property type * Add a new suggestions hook which provides autocomplete options for the alert rule form * Improve delete status handling * Add waiting for Prometheus endpoint consistency after update submission * Get rule definition from ruler or prometheus endpoint in useCombinedRule * Add Prometheus consistency check. Fix view page redirects * Remove rules reload after rule creation, remove statuses from Prom primary mode * Add waiting for Prometheus consistency on delete rule action * Add groups list rendering improvements * Add memo to useAbilities * Fix GMA consistency check, fix GMA statuses * defer filered rules rendering * Update failing tests * Update locales * Add rule-id tests * Remove unused action * update loading styles * Fix unrelated test * Add a new object for reading alerting feature toggles, address minor review issues * Improve consistency check * update i18n * Improve rule form redirects * Refactor feature toggle handling * Update docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Fix prettier issues * Fix i18n * Fix the feature toggle description * Fix rule updates, fix ruler-based suggestions, wait for deletion for GMA rules * Fix rename * Remove unused code, improve copy * Update i18n * Fix url redirect when serving from subpath --------- Co-authored-by: Tom Ratcliffe Co-authored-by: Gilles De Mey Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 16 ++ .../RuleEditorCloudOnlyAllowed.test.tsx | 12 +- .../alerting/unified/RuleList.test.tsx | 3 +- .../RuleEditorCloudRules.test.tsx.snap | 11 -- .../RuleEditorRecordingRule.test.tsx.snap | 11 -- .../unified/components/DynamicTable.tsx | 1 - .../rule-editor/GroupAndNamespaceFields.tsx | 38 ++-- .../alert-rule-form/AlertRuleForm.tsx | 63 ++++-- .../rule-editor/labels/LabelsField.tsx | 63 +----- .../rule-editor/useAlertRuleSuggestions.tsx | 134 +++++++++++++ .../components/rule-list/RuleList.v1.tsx | 166 ++++++++-------- .../components/rule-viewer/DeleteModal.tsx | 18 +- .../components/rule-viewer/RuleViewer.tsx | 51 ++++- .../components/rules/RuleActionsButtons.tsx | 10 +- .../unified/components/rules/RuleStats.tsx | 14 +- .../components/rules/RulesGroup.test.tsx | 4 +- .../unified/components/rules/RulesGroup.tsx | 59 ++++-- .../unified/components/rules/RulesTable.tsx | 182 +++++++++++++----- .../alerting/unified/featureToggles.ts | 3 + .../ruleGroup/useUpsertRuleFromRuleGroup.ts | 7 +- .../alerting/unified/hooks/useAbilities.ts | 67 ++++--- .../alerting/unified/hooks/useCombinedRule.ts | 38 ++-- .../hooks/useCombinedRuleNamespaces.ts | 27 +++ .../unified/hooks/useFilteredRules.ts | 9 +- .../alerting/unified/hooks/useHasRuler.ts | 31 +-- .../hooks/usePrometheusConsistencyCheck.ts | 162 ++++++++++++++++ .../alerting/unified/state/actions.ts | 21 +- .../alerting/unified/utils/constants.ts | 2 +- .../alerting/unified/utils/rule-id.test.ts | 58 +++++- .../alerting/unified/utils/rule-id.ts | 38 ++-- .../PanelDataAlertingTab.test.tsx | 15 +- public/app/types/unified-alerting-dto.ts | 2 +- public/app/types/unified-alerting.ts | 2 +- public/locales/en-US/grafana.json | 6 + public/locales/pseudo-LOCALE/grafana.json | 6 + 40 files changed, 945 insertions(+), 419 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rule-editor/useAlertRuleSuggestions.tsx create mode 100644 public/app/features/alerting/unified/featureToggles.ts create mode 100644 public/app/features/alerting/unified/hooks/usePrometheusConsistencyCheck.ts diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 39ab938fb28..3156876f8cf 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -196,6 +196,7 @@ Experimental features might be changed or removed without prior notice. | `dataplaneAggregator` | Enable grafana dataplane aggregator | | `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | | `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | +| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | | `singleTopNav` | Unifies the top search bar and breadcrumb bar into one | | `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | | `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7104c77c50e..55014a72992 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -204,6 +204,7 @@ export interface FeatureToggles { dataplaneAggregator?: boolean; newFiltersUI?: boolean; lokiSendDashboardPanelNames?: boolean; + alertingPrometheusRulesPrimary?: boolean; singleTopNav?: boolean; exploreLogsShardSplitting?: boolean; exploreLogsAggregatedMetrics?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d42b8ac1ebd..d37138665a3 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1403,6 +1403,13 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaObservabilityLogsSquad, }, + { + Name: "alertingPrometheusRulesPrimary", + Description: "Uses Prometheus rules as the primary source of truth for ruler-enabled data sources", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + FrontendOnly: true, + }, { Name: "singleTopNav", Description: "Unifies the top search bar and breadcrumb bar into one", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 1aea6fced26..d00de03a658 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -185,6 +185,7 @@ alertingFilterV2,experimental,@grafana/alerting-squad,false,false,false dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false newFiltersUI,experimental,@grafana/dashboards-squad,false,false,false lokiSendDashboardPanelNames,experimental,@grafana/observability-logs,false,false,false +alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true singleTopNav,experimental,@grafana/grafana-frontend-platform,false,false,true exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 2be496880cb..c89153d1748 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -751,6 +751,10 @@ const ( // Send dashboard and panel names to Loki when querying FlagLokiSendDashboardPanelNames = "lokiSendDashboardPanelNames" + // FlagAlertingPrometheusRulesPrimary + // Uses Prometheus rules as the primary source of truth for ruler-enabled data sources + FlagAlertingPrometheusRulesPrimary = "alertingPrometheusRulesPrimary" + // FlagSingleTopNav // Unifies the top search bar and breadcrumb bar into one FlagSingleTopNav = "singleTopNav" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 3cd366933c2..14973b66edb 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -240,6 +240,22 @@ "hideFromAdminPage": true } }, + { + "metadata": { + "name": "alertingPrometheusRulesPrimary", + "resourceVersion": "1727332930692", + "creationTimestamp": "2024-09-09T13:56:47Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 06:42:10.692959 +0000 UTC" + } + }, + "spec": { + "description": "Uses Prometheus rules as the primary source of truth for ruler-enabled data sources", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "frontend": true + } + }, { "metadata": { "name": "alertingQueryAndExpressionsStepMode", diff --git a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx index 724ce2bed77..c76b8408f2c 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx @@ -12,8 +12,8 @@ import { searchFolders } from '../../manage-dashboards/state/actions'; import { discoverFeatures } from './api/buildInfo'; import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace } from './api/ruler'; import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; +import { setupMswServer } from './mockApi'; import { grantUserPermissions, mockDataSource, MockDataSourceSrv } from './mocks'; -import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; import * as config from './utils/config'; import { DataSourceType } from './utils/datasource'; @@ -25,7 +25,12 @@ jest.mock('./components/rule-editor/ExpressionEditor', () => ({ })); jest.mock('./api/buildInfo'); -jest.mock('./api/ruler'); +jest.mock('./api/ruler', () => ({ + rulerUrlBuilder: jest.requireActual('./api/ruler').rulerUrlBuilder, + fetchRulerRules: jest.fn(), + fetchRulerRulesGroup: jest.fn(), + fetchRulerRulesNamespace: jest.fn(), +})); jest.mock('../../../../app/features/manage-dashboards/state/actions'); // there's no angular scope in test and things go terribly wrong when trying to render the query editor row. @@ -116,7 +121,6 @@ const mocks = { fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace), fetchRulerRules: jest.mocked(fetchRulerRules), - fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet), }, }; @@ -133,6 +137,8 @@ function getDiscoverFeaturesMock(application: PromApplication, features?: Partia }; } +setupMswServer(); + describe('RuleEditor cloud: checking editable data sources', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 5ed14c39266..ef3ec39be48 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -807,8 +807,9 @@ describe('RuleList', () => { renderRuleList(); - await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); + const groupRows = await ui.ruleGroup.findAll(); + expect(groupRows).toHaveLength(1); expect(ui.exportButton.get()).toBeInTheDocument(); }); }); diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap index ee06f142f10..61ee13594c8 100644 --- a/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap @@ -74,17 +74,6 @@ exports[`RuleEditor cloud can create a new cloud alert 1`] = ` "method": "POST", "url": "http://localhost/api/ruler/mimir/api/v1/rules/namespace-2?subtype=mimir", }, - { - "body": "", - "headers": [ - [ - "accept", - "application/json, text/plain, */*", - ], - ], - "method": "GET", - "url": "http://localhost/api/ruler/mimir/api/v1/rules?subtype=mimir", - }, { "body": "", "headers": [ diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap index 9e7216c81e0..d95652d615c 100644 --- a/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap @@ -69,17 +69,6 @@ exports[`RuleEditor recording rules can create a new cloud recording rule 1`] = "method": "POST", "url": "http://localhost/api/ruler/mimir/api/v1/rules/namespace-2?subtype=mimir", }, - { - "body": "", - "headers": [ - [ - "accept", - "application/json, text/plain, */*", - ], - ], - "method": "GET", - "url": "http://localhost/api/ruler/mimir/api/v1/rules?subtype=mimir", - }, { "body": "", "headers": [ diff --git a/public/app/features/alerting/unified/components/DynamicTable.tsx b/public/app/features/alerting/unified/components/DynamicTable.tsx index ae405474a82..f10b5ecc7a4 100644 --- a/public/app/features/alerting/unified/components/DynamicTable.tsx +++ b/public/app/features/alerting/unified/components/DynamicTable.tsx @@ -43,7 +43,6 @@ export interface DynamicTableProps { onCollapse?: (item: DynamicTableItemProps) => void; onExpand?: (item: DynamicTableItemProps) => void; isExpanded?: (item: DynamicTableItemProps) => boolean; - renderExpandedContent?: ( item: DynamicTableItemProps, index: number, diff --git a/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx b/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx index eb3532f954a..d95fc0a6897 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx @@ -1,15 +1,14 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Field, useStyles2, VirtualizedSelect } from '@grafana/ui'; -import { useDispatch } from 'app/types'; -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; -import { fetchRulerRulesAction } from '../../state/actions'; import { RuleFormValues } from '../../types/rule-form'; +import { useAlertRuleSuggestions } from './useAlertRuleSuggestions'; + interface Props { rulesSourceName: string; } @@ -23,27 +22,22 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => { } = useFormContext(); const style = useStyles2(getStyle); - - const rulerRequests = useUnifiedAlertingSelector((state) => state.rulerRules); - const dispatch = useDispatch(); - useEffect(() => { - dispatch(fetchRulerRulesAction({ rulesSourceName })); - }, [rulesSourceName, dispatch]); - - const rulesConfig = rulerRequests[rulesSourceName]?.result; + const { namespaceGroups, isLoading } = useAlertRuleSuggestions(rulesSourceName); const namespace = watch('namespace'); - const namespaceOptions = useMemo( - (): Array> => - rulesConfig ? Object.keys(rulesConfig).map((namespace) => ({ label: namespace, value: namespace })) : [], - [rulesConfig] + const namespaceOptions: Array> = useMemo( + () => + Array.from(namespaceGroups.keys()).map((namespace) => ({ + label: namespace, + value: namespace, + })), + [namespaceGroups] ); - const groupOptions = useMemo( - (): Array> => - (namespace && rulesConfig?.[namespace]?.map((group) => ({ label: group.name, value: group.name }))) || [], - [namespace, rulesConfig] + const groupOptions: Array> = useMemo( + () => (namespace && namespaceGroups.get(namespace)?.map((group) => ({ label: group, value: group }))) || [], + [namespace, namespaceGroups] ); return ( @@ -66,6 +60,8 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => { }} options={namespaceOptions} width={42} + isLoading={isLoading} + disabled={isLoading} /> )} name="namespace" @@ -87,6 +83,8 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => { setValue('group', value.value ?? ''); }} className={style.input} + isLoading={isLoading} + disabled={isLoading} /> )} name="group" diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 7c010fcfc5c..d85563db127 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { useEffect, useMemo, useState } from 'react'; import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form'; -import { Link, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; @@ -9,7 +9,6 @@ import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } fro import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; -import { useQueryParams } from 'app/core/hooks/useQueryParams'; import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule'; import { getRuleGroupLocationFromFormValues, @@ -20,7 +19,8 @@ import { isGrafanaRulerRulePaused, isRecordingRuleByType, } from 'app/features/alerting/unified/utils/rules'; -import { RuleWithLocation } from 'app/types/unified-alerting'; +import { RuleGroupIdentifier, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting'; +import { PostableRuleGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { LogMessages, @@ -29,8 +29,10 @@ import { trackAlertRuleFormError, trackAlertRuleFormSaved, } from '../../../Analytics'; +import { shouldUsePrometheusRulesPrimary } from '../../../featureToggles'; import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup'; import { useAddRuleToRuleGroup, useUpdateRuleInRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup'; +import { useURLSearchParams } from '../../../hooks/useURLSearchParams'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { DEFAULT_GROUP_EVALUATION_INTERVAL, @@ -45,6 +47,8 @@ import { normalizeDefaultAnnotations, } from '../../../utils/rule-form'; import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id'; +import * as ruleId from '../../../utils/rule-id'; +import { createRelativeUrl } from '../../../utils/url'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; import { AlertRuleNameAndMetric } from '../AlertRuleNameInput'; import AnnotationsStep from '../AnnotationsStep'; @@ -61,10 +65,12 @@ type Props = { prefill?: Partial; // Existing implies we modify existing rule. Prefill only provides default form values }; +const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); + export const AlertRuleForm = ({ existing, prefill }: Props) => { const styles = useStyles2(getStyles); const notifyApp = useAppNotification(); - const [queryParams] = useQueryParams(); + const [queryParams] = useURLSearchParams(); const [showEditYaml, setShowEditYaml] = useState(false); const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); @@ -77,7 +83,6 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { const uidFromParams = routeParams.id; - const returnTo = !queryParams.returnTo ? '/alerting/list' : String(queryParams.returnTo); const [showDeleteModal, setShowDeleteModal] = useState(false); const defaultValues: RuleFormValues = useMemo(() => { @@ -89,8 +94,8 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { return formValuesFromPrefill(prefill); } - if (typeof queryParams.defaults === 'string') { - return formValuesFromQueryParams(queryParams.defaults, ruleType); + if (queryParams.has('defaults')) { + return formValuesFromQueryParams(queryParams.get('defaults') ?? '', ruleType); } return { @@ -160,10 +165,17 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { ); } - if (exitOnSave && returnTo) { + const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier; + if (exitOnSave) { + const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition); + locationService.push(returnTo); - } else if (isCloudRulerRule(ruleDefinition)) { - const { dataSourceName, namespaceName, groupName } = getRuleGroupLocationFromFormValues(values); + return; + } + + // Cloud Ruler rules identifier changes on update due to containing rule name and hash components + // After successful update we need to update the URL to avoid displaying 404 errors + if (isCloudRulerRule(ruleDefinition)) { const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition); locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`); } @@ -171,6 +183,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { const deleteRule = async () => { if (existing) { + const returnTo = queryParams.get('returnTo') || '/alerting/list'; const ruleGroupIdentifier = getRuleGroupLocationFromRuleWithLocation(existing); const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); @@ -193,6 +206,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { const cancelRuleCreation = () => { logInfo(LogMessages.cancelSavingAlertRule); trackAlertRuleFormCancelled({ formAction: existing ? 'update' : 'create' }); + locationService.getHistory().goBack(); }; const evaluateEveryInForm = watch('evaluateEvery'); @@ -222,11 +236,9 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { {isSubmitting && } Save rule and exit - - - + {existing ? ( - )} - -
- )} - {hasNoAlertRulesCreatedYet && } - {hasAlertRulesCreated && } - - ); - }, - { style: 'page' } -); + const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces(); + const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState); + return ( + // We don't want to show the Loading... indicator for the whole page. + // We show separate indicators for Grafana-managed and Cloud rules + }> + + + {hasAlertRulesCreated && ( + + {view === 'groups' && hasActiveFilters && ( + + )} + + + )} + {hasNoAlertRulesCreatedYet && } + {hasAlertRulesCreated && } + + ); +}; -export default RuleList; +export default withErrorBoundary(RuleListV1, { style: 'page' }); export function CreateAlertButton() { const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule); diff --git a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx index 3e2e87a4438..2aca5f867f7 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx @@ -5,16 +5,21 @@ import { ConfirmModal } from '@grafana/ui'; import { dispatch } from 'app/store/store'; import { CombinedRule } from 'app/types/unified-alerting'; +import { shouldUsePrometheusRulesPrimary } from '../../featureToggles'; import { useDeleteRuleFromGroup } from '../../hooks/ruleGroup/useDeleteRuleFromGroup'; -import { fetchPromAndRulerRulesAction } from '../../state/actions'; +import { usePrometheusConsistencyCheck } from '../../hooks/usePrometheusConsistencyCheck'; +import { fetchPromAndRulerRulesAction, fetchRulerRulesAction } from '../../state/actions'; import { fromRulerRuleAndRuleGroupIdentifier } from '../../utils/rule-id'; -import { getRuleGroupLocationFromCombinedRule } from '../../utils/rules'; +import { getRuleGroupLocationFromCombinedRule, isCloudRuleIdentifier } from '../../utils/rules'; type DeleteModalHook = [JSX.Element, (rule: CombinedRule) => void, () => void]; +const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); + export const useDeleteModal = (redirectToListView = false): DeleteModalHook => { const [ruleToDelete, setRuleToDelete] = useState(); const [deleteRuleFromGroup] = useDeleteRuleFromGroup(); + const { waitForRemoval } = usePrometheusConsistencyCheck(); const dismissModal = useCallback(() => { setRuleToDelete(undefined); @@ -39,13 +44,20 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => { // @TODO remove this when we moved everything to RTKQ – then the endpoint will simply invalidate the tags dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: ruleGroupIdentifier.dataSourceName })); + if (prometheusRulesPrimary && isCloudRuleIdentifier(ruleIdentifier)) { + await waitForRemoval(ruleIdentifier); + } else { + // Without this the delete popup will close and the user will still see the deleted rule + await dispatch(fetchRulerRulesAction({ rulesSourceName: ruleGroupIdentifier.dataSourceName })); + } + dismissModal(); if (redirectToListView) { locationService.replace('/alerting/list'); } }, - [deleteRuleFromGroup, dismissModal, redirectToListView] + [deleteRuleFromGroup, dismissModal, redirectToListView, waitForRemoval] ); const modal = useMemo( diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index 3f71b0b9e95..4c2efd64e58 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -1,9 +1,11 @@ import { css } from '@emotion/css'; import { chain, isEmpty, truncate } from 'lodash'; import { useState } from 'react'; +import { useMeasure } from 'react-use'; import { NavModelItem, UrlQueryValue } from '@grafana/data'; -import { Alert, LinkButton, Stack, TabContent, Text, TextLink, useStyles2 } from '@grafana/ui'; +import { Alert, LinkButton, LoadingBar, Stack, TabContent, Text, TextLink, useStyles2 } from '@grafana/ui'; +import { t, Trans } from '@grafana/ui/src/utils/i18n'; import { PageInfoItem } from 'app/core/components/Page/types'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule'; @@ -12,11 +14,12 @@ import { AlertInstanceTotalState, CombinedRule, RuleHealth, RuleIdentifier } fro import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; import { defaultPageNav } from '../../RuleViewer'; +import { shouldUsePrometheusRulesPrimary } from '../../featureToggles'; +import { usePrometheusCreationConsistencyCheck } from '../../hooks/usePrometheusConsistencyCheck'; import { PluginOriginBadge } from '../../plugins/PluginOriginBadge'; import { Annotation } from '../../utils/constants'; -import { makeDashboardLink, makePanelLink } from '../../utils/misc'; +import { makeDashboardLink, makePanelLink, stringifyErrorLike } from '../../utils/misc'; import { - RulePluginOrigin, getRulePluginOrigin, isAlertingRule, isFederatedRuleGroup, @@ -24,6 +27,7 @@ import { isGrafanaRulerRule, isGrafanaRulerRulePaused, isRecordingRule, + RulePluginOrigin, } from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; import { AlertLabels } from '../AlertLabels'; @@ -51,8 +55,10 @@ export enum ActiveTab { Details = 'details', } +const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); + const RuleViewer = () => { - const { rule } = useAlertRule(); + const { rule, identifier } = useAlertRule(); const { pageNav, activeTab } = usePageNav(rule); // this will be used to track if we are in the process of cloning a rule @@ -112,6 +118,7 @@ const RuleViewer = () => { } > + {prometheusRulesPrimary && } {/* tabs and tab content */} @@ -261,6 +268,42 @@ export const Title = ({ name, paused = false, state, health, ruleType, ruleOrigi ); }; +/** + * This component displays an Alert warning component if discovers inconsistencies between Prometheus and Ruler rules + * It will show loading indicator until the Prometheus and Ruler rule is consistent + * It will not show the warning if the rule is Grafana managed + */ +function PrometheusConsistencyCheck({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }) { + const [ref, { width }] = useMeasure(); + const { isConsistent, error } = usePrometheusCreationConsistencyCheck(ruleIdentifier); + + if (isConsistent) { + return null; + } + + if (error) { + return ( + + {stringifyErrorLike(error)} + + ); + } + + return ( + + + + + Alert rule has been updated. Changes may take up to a minute to appear on the Alert rules list view. + + + + ); +} + export const isErrorHealth = (health?: RuleHealth) => health === 'error' || health === 'err'; export function useActiveTab(): [ActiveTab, (tab: ActiveTab) => void] { diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index b747de47c3c..3658ce385c9 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -1,6 +1,5 @@ import { css, cx } from '@emotion/css'; import { useState } from 'react'; -import { useLocation } from 'react-router-dom-v5-compat'; import { GrafanaTheme2 } from '@grafana/data'; import { LinkButton, Stack, useStyles2 } from '@grafana/ui'; @@ -42,7 +41,6 @@ interface Props { */ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton, rule, rulesSource }: Props) => { const dispatch = useDispatch(); - const location = useLocation(); const style = useStyles2(getStyles); const redirectToListView = compact ? false : true; @@ -57,8 +55,6 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton const { namespace, group, rulerRule } = rule; const { hasActiveFilters } = useRulesFilter(); - const returnTo = location.pathname + location.search; - const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance); const [editRuleSupported, editRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update); @@ -85,7 +81,7 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton key="view" variant="secondary" icon="eye" - href={createViewLink(rulesSource, rule, returnTo)} + href={createViewLink(rulesSource, rule)} > {!compact && 'View'} @@ -95,9 +91,7 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton if (rulerRule && canEditRule) { const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule); - const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, { - returnTo, - }); + const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`); buttons.push( = { nodata: 0, }; -export const RuleStats = ({ namespaces }: Props) => { - const stats = statsFromNamespaces(namespaces); +// Stats calculation is an expensive operation +// Make sure we repeat that as few times as possible +export const RuleStats = React.memo(({ namespaces }: Props) => { + const deferredNamespaces = useDeferredValue(namespaces); + + const stats = useMemo(() => statsFromNamespaces(deferredNamespaces), [deferredNamespaces]); const total = totalFromStats(stats); const statsComponents = getComponentsFromStats(stats); @@ -49,7 +53,9 @@ export const RuleStats = ({ namespaces }: Props) => { )} ); -}; +}); + +RuleStats.displayName = 'RuleStats'; interface RuleGroupStatsProps { group: CombinedRuleGroup; diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx index 097ecefce1f..a868bc87fbd 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx @@ -40,8 +40,8 @@ const mocks = { function mockUseHasRuler(hasRuler: boolean, rulerRulesLoaded: boolean) { mocks.useHasRuler.mockReturnValue({ - hasRuler: () => hasRuler, - rulerRulesLoaded: () => rulerRulesLoaded, + hasRuler, + rulerRulesLoaded, }); } diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index 5ae024d814c..3cee6fead7d 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -1,21 +1,20 @@ import { css } from '@emotion/css'; import pluralize from 'pluralize'; -import { useEffect, useState } from 'react'; -import * as React from 'react'; +import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Badge, ConfirmModal, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui'; -import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier } from 'app/types/unified-alerting'; +import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier, RulesSource } from 'app/types/unified-alerting'; import { LogMessages, logInfo } from '../../Analytics'; import { useDeleteRuleGroup } from '../../hooks/ruleGroup/useDeleteRuleGroup'; import { useFolder } from '../../hooks/useFolder'; import { useHasRuler } from '../../hooks/useHasRuler'; import { useRulesAccess } from '../../utils/accessControlHooks'; -import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource'; +import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource'; import { makeFolderLink, makeFolderSettingsLink } from '../../utils/misc'; -import { isFederatedRuleGroup, isGrafanaRulerRule, rulesSourceToDataSourceName } from '../../utils/rules'; +import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules'; import { CollapseToggle } from '../CollapseToggle'; import { RuleLocation } from '../RuleLocation'; import { GrafanaRuleFolderExporter } from '../export/GrafanaRuleFolderExporter'; @@ -39,8 +38,8 @@ interface Props { export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: Props) => { const { rulesSource } = namespace; - const styles = useStyles2(getStyles); const [deleteRuleGroup] = useDeleteRuleGroup(); + const styles = useStyles2(getStyles); const [isEditingGroup, setIsEditingGroup] = useState(false); const [isDeletingGroup, setIsDeletingGroup] = useState(false); @@ -54,14 +53,13 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: setIsCollapsed(!expandAll); }, [expandAll]); - const { hasRuler, rulerRulesLoaded } = useHasRuler(); + const { hasRuler, rulerRulesLoaded } = useHasRuler(namespace.rulesSource); const rulerRule = group.rules[0]?.rulerRule; const folderUID = (rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid) || undefined; const { folder } = useFolder(folderUID); // group "is deleting" if rules source has ruler, but this group has no rules that are in ruler - const isDeleting = - hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && !group.rules.find((rule) => !!rule.rulerRule); + const isDeleting = hasRuler && rulerRulesLoaded && !group.rules.find((rule) => !!rule.rulerRule); const isFederated = isFederatedRuleGroup(group); // check if group has provisioned items @@ -76,7 +74,7 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: const deleteGroup = async () => { const namespaceName = decodeGrafanaNamespace(namespace).name; const groupName = group.name; - const dataSourceName = rulesSourceToDataSourceName(namespace.rulesSource); + const dataSourceName = getRulesSourceName(namespace.rulesSource); const ruleGroupIdentifier: RuleGroupIdentifier = { namespaceName, groupName, dataSourceName }; await deleteRuleGroup.execute(ruleGroupIdentifier); @@ -171,7 +169,7 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: } } } - } else if (canEditRules(rulesSource.name) && hasRuler(rulesSource)) { + } else if (canEditRules(rulesSource.name) && hasRuler) { if (!isFederated) { actionIcons.push( - - {isCloudRulesSource(rulesSource) && ( - - {rulesSource.meta.name} - - )} + + { // eslint-disable-next-line
setIsCollapsed(!isCollapsed)}> @@ -326,6 +316,33 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: RulesGroup.displayName = 'RulesGroup'; +// It's a simple component but we render 80 of them on the list page it needs to be fast +// The Tooltip component is expensive to render and the rulesSource doesn't change often +// so memoization seems to bring a lot of benefit here +const CloudSourceLogo = React.memo(({ rulesSource }: { rulesSource: RulesSource | string }) => { + const styles = useStyles2(getStyles); + + if (isCloudRulesSource(rulesSource)) { + return ( + + {rulesSource.meta.name} + + ); + } + + return null; +}); + +CloudSourceLogo.displayName = 'CloudSourceLogo'; + +// We render a lot of these on the list page, and the Icon component does quite a bit of work +// to render its contents +const FolderIcon = React.memo(({ isCollapsed }: { isCollapsed: boolean }) => { + return ; +}); + +FolderIcon.displayName = 'FolderIcon'; + export const getStyles = (theme: GrafanaTheme2) => { return { wrapper: css({}), diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index dfadb75dd8b..eb6f2b68ba0 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -1,14 +1,22 @@ import { css, cx } from '@emotion/css'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; +import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, Tooltip } from '@grafana/ui'; +import { useStyles2, Tooltip, Pagination } from '@grafana/ui'; import { CombinedRule } from 'app/types/unified-alerting'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; +import { alertRuleApi } from '../../api/alertRuleApi'; +import { featureDiscoveryApi } from '../../api/featureDiscoveryApi'; +import { shouldUsePrometheusRulesPrimary } from '../../featureToggles'; +import { useAsync } from '../../hooks/useAsync'; +import { attachRulerRuleToCombinedRule } from '../../hooks/useCombinedRuleNamespaces'; import { useHasRuler } from '../../hooks/useHasRuler'; +import { usePagination } from '../../hooks/usePagination'; import { PluginOriginBadge } from '../../plugins/PluginOriginBadge'; import { Annotation } from '../../utils/constants'; +import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { getRulePluginOrigin, isGrafanaRulerRule, isGrafanaRulerRulePaused } from '../../utils/rules'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines'; @@ -36,6 +44,11 @@ interface Props { className?: string; } +const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); + +const { useLazyGetRuleGroupForNamespaceQuery } = alertRuleApi; +const { useLazyDiscoverDsFeaturesQuery } = featureDiscoveryApi; + export const RulesTable = ({ rules, className, @@ -46,21 +59,26 @@ export const RulesTable = ({ showNextEvaluationColumn = false, }: Props) => { const styles = useStyles2(getStyles); - const wrapperClass = cx(styles.wrapper, className, { [styles.wrapperMargin]: showGuidelines }); + const { pageItems, page, numberOfPages, onPageChange } = usePagination(rules, 1, DEFAULT_PER_PAGE_PAGINATION); + + const { result: rulesWithRulerDefinitions, status: rulerRulesLoadingStatus } = useLazyLoadRulerRules(pageItems); + + const isLoadingRulerGroup = rulerRulesLoadingStatus === 'loading'; + const items = useMemo((): RuleTableItemProps[] => { - return rules.map((rule, ruleIdx) => { + return rulesWithRulerDefinitions.map((rule, ruleIdx) => { return { id: `${rule.namespace.name}-${rule.group.name}-${rule.name}-${ruleIdx}`, data: rule, }; }); - }, [rules]); + }, [rulesWithRulerDefinitions]); - const columns = useColumns(showSummaryColumn, showGroupColumn, showNextEvaluationColumn); + const columns = useColumns(showSummaryColumn, showGroupColumn, showNextEvaluationColumn, isLoadingRulerGroup); - if (!rules.length) { + if (!pageItems.length) { return
{emptyMessage}
; } @@ -73,13 +91,71 @@ export const RulesTable = ({ isExpandable={true} items={items} renderExpandedContent={({ data: rule }) => } - pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }} - paginationStyles={styles.pagination} + /> +
); }; +/** + * This hook is used to lazy load the Ruler rule for each rule. + * If the `prometheusRulesPrimary` feature flag is enabled, the hook will fetch the Ruler rule counterpart for each Prometheus rule. + * If the `prometheusRulesPrimary` feature flag is disabled, the hook will return the rules as is. + * @param rules Combined rules with or without Ruler rule property + * @returns Combined rules enriched with Ruler rule property + */ +function useLazyLoadRulerRules(rules: CombinedRule[]) { + const [fetchRulerRuleGroup] = useLazyGetRuleGroupForNamespaceQuery(); + const [fetchDsFeatures] = useLazyDiscoverDsFeaturesQuery(); + + const [actions, state] = useAsync(async () => { + const result = Promise.all( + rules.map(async (rule) => { + const dsFeatures = await fetchDsFeatures( + { rulesSourceName: getRulesSourceName(rule.namespace.rulesSource) }, + true + ).unwrap(); + + // Due to lack of ruleUid and folderUid in Prometheus rules we cannot do the lazy load for GMA + if (dsFeatures.rulerConfig && rule.namespace.rulesSource !== GRAFANA_RULES_SOURCE_NAME) { + // RTK Query should handle caching and deduplication for us + const rulerRuleGroup = await fetchRulerRuleGroup( + { + namespace: rule.namespace.name, + group: rule.group.name, + rulerConfig: dsFeatures.rulerConfig, + }, + true + ).unwrap(); + + attachRulerRuleToCombinedRule(rule, rulerRuleGroup); + } + + return rule; + }) + ); + return result; + }, rules); + + useEffect(() => { + if (prometheusRulesPrimary) { + actions.execute(); + } else { + // We need to reset the actions to update the rules if they changed + // Otherwise useAsync acts like a cache and always return the first rules passed to it + actions.reset(); + } + }, [rules, actions]); + + return state; +} + export const getStyles = (theme: GrafanaTheme2) => ({ wrapperMargin: css({ [theme.breakpoints.up('md')]: { @@ -93,6 +169,9 @@ export const getStyles = (theme: GrafanaTheme2) => ({ width: 'auto', borderRadius: theme.shape.radius.default, }), + skeletonWrapper: css({ + flex: 1, + }), pagination: css({ display: 'flex', margin: 0, @@ -102,36 +181,22 @@ export const getStyles = (theme: GrafanaTheme2) => ({ borderLeft: `1px solid ${theme.colors.border.medium}`, borderRight: `1px solid ${theme.colors.border.medium}`, borderBottom: `1px solid ${theme.colors.border.medium}`, + float: 'none', }), }); -function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNextEvaluationColumn: boolean) { - const { hasRuler, rulerRulesLoaded } = useHasRuler(); - +function useColumns( + showSummaryColumn: boolean, + showGroupColumn: boolean, + showNextEvaluationColumn: boolean, + isRulerLoading: boolean +) { return useMemo((): RuleTableColumnProps[] => { - const ruleIsDeleting = (rule: CombinedRule) => { - const { namespace, promRule, rulerRule } = rule; - const { rulesSource } = namespace; - return Boolean(hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && promRule && !rulerRule); - }; - - const ruleIsCreating = (rule: CombinedRule) => { - const { namespace, promRule, rulerRule } = rule; - const { rulesSource } = namespace; - return Boolean(hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && rulerRule && !promRule); - }; - const columns: RuleTableColumnProps[] = [ { id: 'state', label: 'State', - renderCell: ({ data: rule }) => { - const isDeleting = ruleIsDeleting(rule); - const isCreating = ruleIsCreating(rule); - const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule); - - return ; - }, + renderCell: ({ data: rule }) => , size: '165px', }, { @@ -232,21 +297,50 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNe id: 'actions', label: 'Actions', // eslint-disable-next-line react/display-name - renderCell: ({ data: rule }) => { - const isDeleting = ruleIsDeleting(rule); - const isCreating = ruleIsCreating(rule); - return ( - - ); - }, + renderCell: ({ data: rule }) => , size: '200px', }); return columns; - }, [showSummaryColumn, showGroupColumn, showNextEvaluationColumn, hasRuler, rulerRulesLoaded]); + }, [showSummaryColumn, showGroupColumn, showNextEvaluationColumn, isRulerLoading]); +} + +function RuleStateCell({ rule }: { rule: CombinedRule }) { + const { isDeleting, isCreating, isPaused } = useRuleStatus(rule); + return ; +} + +function RuleActionsCell({ rule, isLoadingRuler }: { rule: CombinedRule; isLoadingRuler: boolean }) { + const styles = useStyles2(getStyles); + const { isDeleting, isCreating } = useRuleStatus(rule); + + if (isLoadingRuler) { + return ; + } + + return ( + + ); +} + +function useRuleStatus(rule: CombinedRule) { + const { hasRuler, rulerRulesLoaded } = useHasRuler(rule.namespace.rulesSource); + const { promRule, rulerRule } = rule; + + // If prometheusRulesPrimary is enabled, we don't fetch rules from the Ruler API (except for Grafana managed rules) + // so there is no way to detect statuses + if (prometheusRulesPrimary && !isGrafanaRulerRule(rulerRule)) { + return { isDeleting: false, isCreating: false, isPaused: false }; + } + + const isDeleting = Boolean(hasRuler && rulerRulesLoaded && promRule && !rulerRule); + const isCreating = Boolean(hasRuler && rulerRulesLoaded && rulerRule && !promRule); + const isPaused = isGrafanaRulerRule(rulerRule) && isGrafanaRulerRulePaused(rulerRule); + + return { isDeleting, isCreating, isPaused }; } diff --git a/public/app/features/alerting/unified/featureToggles.ts b/public/app/features/alerting/unified/featureToggles.ts new file mode 100644 index 00000000000..8e7ebc60c0e --- /dev/null +++ b/public/app/features/alerting/unified/featureToggles.ts @@ -0,0 +1,3 @@ +import { config } from '@grafana/runtime'; + +export const shouldUsePrometheusRulesPrimary = () => config.featureToggles.alertingPrometheusRulesPrimary ?? false; diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useUpsertRuleFromRuleGroup.ts b/public/app/features/alerting/unified/hooks/ruleGroup/useUpsertRuleFromRuleGroup.ts index 41a4664c54a..e67a2558387 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useUpsertRuleFromRuleGroup.ts +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useUpsertRuleFromRuleGroup.ts @@ -2,13 +2,11 @@ import { produce } from 'immer'; import { isEqual } from 'lodash'; import { t } from 'app/core/internationalization'; -import { dispatch } from 'app/store/store'; import { RuleGroupIdentifier, EditableRuleIdentifier } from 'app/types/unified-alerting'; import { PostableRuleDTO } from 'app/types/unified-alerting-dto'; import { alertRuleApi } from '../../api/alertRuleApi'; import { addRuleAction, updateRuleAction } from '../../reducers/ruler/ruleGroups'; -import { fetchRulerRulesAction } from '../../state/actions'; import { isGrafanaRuleIdentifier, isGrafanaRulerRule } from '../../utils/rules'; import { useAsync } from '../useAsync'; @@ -25,7 +23,7 @@ export function useAddRuleToRuleGroup() { const successMessage = t('alerting.rules.add-rule.success', 'Rule added successfully'); return useAsync(async (ruleGroup: RuleGroupIdentifier, rule: PostableRuleDTO, interval?: string) => { - const { namespaceName, dataSourceName } = ruleGroup; + const { namespaceName } = ruleGroup; // the new rule might have to be created in a new group, pass name and interval (optional) to the action const action = addRuleAction({ rule, interval, groupName: ruleGroup.groupName }); @@ -38,9 +36,6 @@ export function useAddRuleToRuleGroup() { notificationOptions: { successMessage }, }).unwrap(); - // @TODO remove - await dispatch(fetchRulerRulesAction({ rulesSourceName: dataSourceName })); - return result; }); } diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 12a10278774..f449bada4e4 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -9,7 +9,7 @@ import { CombinedRule } from 'app/types/unified-alerting'; import { alertmanagerApi } from '../api/alertmanagerApi'; import { useAlertmanager } from '../state/AlertmanagerContext'; import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control'; -import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { isAdmin } from '../utils/misc'; import { isFederatedRuleGroup, isGrafanaRecordingRule, isGrafanaRulerRule, isPluginProvidedRule } from '../utils/rules'; @@ -150,17 +150,12 @@ export function useAlertRuleAbilities(rule: CombinedRule, actions: AlertRuleActi }, [abilities, actions]); } +// This hook is being called a lot in different places +// In some cases multiple times for ~80 rules (e.g. on the list page) +// We need to investigate further if some of these calls are redundant +// In the meantime, memoizing the result helps export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities { - const rulesSource = rule.namespace.rulesSource; - const rulesSourceName = typeof rulesSource === 'string' ? rulesSource : rulesSource.name; - - const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance); - const isFederated = isFederatedRuleGroup(rule.group); - const isGrafanaManagedAlertRule = isGrafanaRulerRule(rule.rulerRule); - const isPluginProvided = isPluginProvidedRule(rule); - - // if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited - const immutableRule = isProvisioned || isFederated || isPluginProvided; + const rulesSourceName = getRulesSourceName(rule.namespace.rulesSource); const { isEditable, @@ -169,27 +164,39 @@ export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities = { - [AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create), - [AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read), - [AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false], - [AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false], - [AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore), - [AlertRuleAction.Silence]: canSilence, - [AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed], - [AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false], - }; + const abilities = useMemo>(() => { + const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance); + const isFederated = isFederatedRuleGroup(rule.group); + const isGrafanaManagedAlertRule = isGrafanaRulerRule(rule.rulerRule); + const isPluginProvided = isPluginProvidedRule(rule); + + // if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited + const immutableRule = isProvisioned || isFederated || isPluginProvided; + + // while we gather info, pretend it's not supported + const MaybeSupported = loading ? NotSupported : isRulerAvailable; + const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported; + + // Creating duplicates of plugin-provided rules does not seem to make a lot of sense + const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported; + + const rulesPermissions = getRulesPermissions(rulesSourceName); + + const abilities: Abilities = { + [AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create), + [AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read), + [AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false], + [AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false], + [AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore), + [AlertRuleAction.Silence]: canSilence, + [AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed], + [AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false], + }; + + return abilities; + }, [rule, loading, isRulerAvailable, isEditable, isRemovable, rulesSourceName, exportAllowed, canSilence]); return abilities; } diff --git a/public/app/features/alerting/unified/hooks/useCombinedRule.ts b/public/app/features/alerting/unified/hooks/useCombinedRule.ts index af5e843ad9d..7f0239142fc 100644 --- a/public/app/features/alerting/unified/hooks/useCombinedRule.ts +++ b/public/app/features/alerting/unified/hooks/useCombinedRule.ts @@ -10,7 +10,7 @@ import { getDataSourceByName } from '../utils/datasource'; import * as ruleId from '../utils/rule-id'; import { isCloudRuleIdentifier, isGrafanaRuleIdentifier, isPrometheusRuleIdentifier } from '../utils/rules'; -import { attachRulerRulesToCombinedRules } from './useCombinedRuleNamespaces'; +import { attachRulerRulesToCombinedRules, combineRulesNamespaces } from './useCombinedRuleNamespaces'; export function useCloudCombinedRulesMatching( ruleName: string, @@ -100,7 +100,7 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request } = useRuleLocation(ruleIdentifier); const { - currentData: promRuleNs, + currentData: promRuleNs = [], isLoading: isLoadingPromRules, error: promRuleNsError, } = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery( @@ -135,30 +135,21 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request }, [dsFeatures, fetchRulerRuleGroup, ruleLocation]); const rule = useMemo(() => { - if (!promRuleNs || !ruleSource) { + if (!ruleSource || !ruleLocation) { return; } - if (promRuleNs.length > 0) { - const namespaces = promRuleNs.map((ns) => - attachRulerRulesToCombinedRules(ruleSource, ns, rulerRuleGroup ? [rulerRuleGroup] : []) - ); + const rulerConfig = rulerRuleGroup ? { [ruleLocation.namespace]: [rulerRuleGroup] } : {}; - for (const namespace of namespaces) { - for (const group of namespace.groups) { - for (const rule of group.rules) { - const id = ruleId.fromCombinedRule(ruleSourceName, rule); + const combinedNamespaces = combineRulesNamespaces(ruleSource, promRuleNs, rulerConfig); + const combinedRules = combinedNamespaces.flatMap((ns) => ns.groups).flatMap((group) => group.rules); - if (ruleId.equal(id, ruleIdentifier)) { - return rule; - } - } - } - } - } + const matchingRule = combinedRules.find((rule) => + ruleId.equal(ruleId.fromCombinedRule(ruleSourceName, rule), ruleIdentifier) + ); - return; - }, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, ruleSource]); + return matchingRule; + }, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, ruleSource, ruleLocation]); return { loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup, @@ -167,13 +158,14 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request }; } -interface RuleLocation { +export interface RuleLocation { + datasource: string; namespace: string; group: string; ruleName: string; } -function useRuleLocation(ruleIdentifier: RuleIdentifier): RequestState { +export function useRuleLocation(ruleIdentifier: RuleIdentifier): RequestState { const { isLoading, currentData, error, isUninitialized } = alertRuleApi.endpoints.getAlertRule.useQuery( { uid: isGrafanaRuleIdentifier(ruleIdentifier) ? ruleIdentifier.uid : '' }, { skip: !isGrafanaRuleIdentifier(ruleIdentifier), refetchOnMountOrArgChange: true } @@ -183,6 +175,7 @@ function useRuleLocation(ruleIdentifier: RuleIdentifier): RequestState + rulerRuleToCombinedRule(rulerRule, rule.namespace, rule.group) + ); + const existingRulerRulesByName = combinedRulesFromRuler.reduce((acc, rule) => { + const sameNameRules = acc.get(rule.name); + if (sameNameRules) { + sameNameRules.push(rule); + } else { + acc.set(rule.name, [rule]); + } + return acc; + }, new Map()); + + const matchingRulerRule = getExistingRuleInGroup(rule.promRule, existingRulerRulesByName, rule.namespace.rulesSource); + if (matchingRulerRule) { + rule.rulerRule = matchingRulerRule.rulerRule; + rule.query = matchingRulerRule.query; + rule.labels = matchingRulerRule.labels; + rule.annotations = matchingRulerRule.annotations; + } +} + export function addCombinedPromAndRulerGroups( ns: CombinedRuleNamespace, promGroups: RuleGroup[], diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index cf5e9cc1061..be82f26a3b3 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -1,7 +1,7 @@ import uFuzzy from '@leeoniya/ufuzzy'; import { produce } from 'immer'; import { chain, compact, isEmpty } from 'lodash'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useDeferredValue, useEffect, useMemo } from 'react'; import { getDataSourceSrv } from '@grafana/runtime'; import { Matcher } from 'app/plugins/datasource/alertmanager/types'; @@ -105,8 +105,11 @@ export function useRulesFilter() { } export const useFilteredRules = (namespaces: CombinedRuleNamespace[], filterState: RulesFilter) => { + const deferredNamespaces = useDeferredValue(namespaces); + const deferredFilterState = useDeferredValue(filterState); + return useMemo(() => { - const filteredRules = filterRules(namespaces, filterState); + const filteredRules = filterRules(deferredNamespaces, deferredFilterState); // Totals recalculation is a workaround for the lack of server-side filtering filteredRules.forEach((namespace) => { @@ -125,7 +128,7 @@ export const useFilteredRules = (namespaces: CombinedRuleNamespace[], filterStat }); return filteredRules; - }, [namespaces, filterState]); + }, [deferredNamespaces, deferredFilterState]); }; export const filterRules = ( diff --git a/public/app/features/alerting/unified/hooks/useHasRuler.ts b/public/app/features/alerting/unified/hooks/useHasRuler.ts index d9451d4bb86..db2e85f09ee 100644 --- a/public/app/features/alerting/unified/hooks/useHasRuler.ts +++ b/public/app/features/alerting/unified/hooks/useHasRuler.ts @@ -1,32 +1,21 @@ -import { useCallback } from 'react'; - import { RulesSource } from 'app/types/unified-alerting'; -import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { featureDiscoveryApi } from '../api/featureDiscoveryApi'; +import { getRulesSourceName } from '../utils/datasource'; import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector'; -// datasource has ruler if it's grafana managed or if we're able to load rules from it -export function useHasRuler() { +const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi; + +// datasource has ruler if the discovery api returns a rulerConfig +export function useHasRuler(rulesSource: RulesSource) { const rulerRules = useUnifiedAlertingSelector((state) => state.rulerRules); + const rulesSourceName = getRulesSourceName(rulesSource); - const hasRuler = useCallback( - (rulesSource: string | RulesSource) => { - const rulesSourceName = typeof rulesSource === 'string' ? rulesSource : rulesSource.name; - return rulesSourceName === GRAFANA_RULES_SOURCE_NAME || !!rulerRules[rulesSourceName]?.result; - }, - [rulerRules] - ); + const { currentData: dsFeatures } = useDiscoverDsFeaturesQuery({ rulesSourceName }); - const rulerRulesLoaded = useCallback( - (rulesSource: RulesSource) => { - const rulesSourceName = getRulesSourceName(rulesSource); - const result = rulerRules[rulesSourceName]?.result; - - return Boolean(result); - }, - [rulerRules] - ); + const hasRuler = Boolean(dsFeatures?.rulerConfig); + const rulerRulesLoaded = Boolean(rulerRules[rulesSourceName]?.result); return { hasRuler, rulerRulesLoaded }; } diff --git a/public/app/features/alerting/unified/hooks/usePrometheusConsistencyCheck.ts b/public/app/features/alerting/unified/hooks/usePrometheusConsistencyCheck.ts new file mode 100644 index 00000000000..9d2ffff3dd3 --- /dev/null +++ b/public/app/features/alerting/unified/hooks/usePrometheusConsistencyCheck.ts @@ -0,0 +1,162 @@ +import { useCallback, useEffect, useRef } from 'react'; + +import { CloudRuleIdentifier, RuleIdentifier } from 'app/types/unified-alerting'; + +import { alertRuleApi } from '../api/alertRuleApi'; +import * as ruleId from '../utils/rule-id'; +import { isCloudRuleIdentifier } from '../utils/rules'; + +import { useAsync } from './useAsync'; + +const { useLazyPrometheusRuleNamespacesQuery } = alertRuleApi; + +const CONSISTENCY_CHECK_POOL_INTERVAL = 3 * 1000; // 3 seconds; +const CONSISTENCY_CHECK_TIMEOUT = 90 * 1000; // 90 seconds + +const { setInterval, clearInterval } = window; + +function useMatchingPromRuleExists() { + const [fetchPrometheusNamespaces] = useLazyPrometheusRuleNamespacesQuery(); + + const matchingPromRuleExists = useCallback( + async (ruleIdentifier: CloudRuleIdentifier) => { + const { ruleSourceName, namespace, groupName, ruleName } = ruleIdentifier; + const namespaces = await fetchPrometheusNamespaces({ + ruleSourceName, + namespace, + groupName, + ruleName, + }).unwrap(); + + const matchingGroup = namespaces.find((ns) => ns.name === namespace)?.groups.find((g) => g.name === groupName); + + const hasMatchingRule = matchingGroup?.rules.some((r) => { + const currentRuleIdentifier = ruleId.fromRule(ruleSourceName, namespace, groupName, r); + return ruleId.equal(currentRuleIdentifier, ruleIdentifier); + }); + + return hasMatchingRule ?? false; + }, + [fetchPrometheusNamespaces] + ); + + return { matchingPromRuleExists }; +} + +export function usePrometheusConsistencyCheck() { + const { matchingPromRuleExists } = useMatchingPromRuleExists(); + + const removalConsistencyInterval = useRef(); + const creationConsistencyInterval = useRef(); + + useEffect(() => { + return () => { + clearRemovalInterval(); + clearCreationInterval(); + }; + }, []); + + const clearRemovalInterval = () => { + if (removalConsistencyInterval.current) { + clearInterval(removalConsistencyInterval.current); + removalConsistencyInterval.current = undefined; + } + }; + + const clearCreationInterval = () => { + if (creationConsistencyInterval.current) { + clearInterval(creationConsistencyInterval.current); + creationConsistencyInterval.current = undefined; + } + }; + + async function waitForRemoval(ruleIdentifier: CloudRuleIdentifier) { + // We can wait only for one rule at a time + clearRemovalInterval(); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + clearRemovalInterval(); + reject(new Error('Timeout while waiting for rule removal')); + }, CONSISTENCY_CHECK_TIMEOUT); + }); + + const waitPromise = new Promise((resolve, reject) => { + removalConsistencyInterval.current = setInterval(() => { + matchingPromRuleExists(ruleIdentifier) + .then((ruleExists) => { + if (ruleExists === false) { + clearRemovalInterval(); + resolve(); + } + }) + .catch((error) => { + clearRemovalInterval(); + reject(error); + }); + }, CONSISTENCY_CHECK_POOL_INTERVAL); + }); + + return Promise.race([timeoutPromise, waitPromise]); + } + + async function waitForCreation(ruleIdentifier: CloudRuleIdentifier) { + // We can wait only for one rule at a time + clearCreationInterval(); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + clearCreationInterval(); + reject(new Error('Timeout while waiting for rule creation')); + }, CONSISTENCY_CHECK_TIMEOUT); + }); + + const waitPromise = new Promise((resolve, reject) => { + creationConsistencyInterval.current = setInterval(() => { + matchingPromRuleExists(ruleIdentifier) + .then((ruleExists) => { + if (ruleExists === true) { + clearCreationInterval(); + resolve(); + } + }) + .catch((error) => { + clearCreationInterval(); + reject(error); + }); + }, CONSISTENCY_CHECK_POOL_INTERVAL); + }); + + return Promise.race([timeoutPromise, waitPromise]); + } + + return { waitForRemoval, waitForCreation }; +} + +export function usePrometheusCreationConsistencyCheck(ruleIdentifier: RuleIdentifier) { + const { matchingPromRuleExists } = useMatchingPromRuleExists(); + const { waitForCreation } = usePrometheusConsistencyCheck(); + + const [actions, state] = useAsync(async (identifier: RuleIdentifier) => { + if (isCloudRuleIdentifier(identifier)) { + return waitForCreation(identifier); + } else { + // GMA rules are not supported yet + return Promise.resolve(); + } + }); + + useEffect(() => { + if (isCloudRuleIdentifier(ruleIdentifier)) { + // We need to check if the rule exists first, because most of the times it does, + // and wait for the consistency only if the rule does not exist. + matchingPromRuleExists(ruleIdentifier).then((ruleExists) => { + if (!ruleExists) { + actions.execute(ruleIdentifier); + } + }); + } + }, [actions, ruleIdentifier, matchingPromRuleExists]); + + return { isConsistent: state.status === 'success' || state.status === 'not-executed', error: state.error }; +} diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index c7dad4ca3b1..77565cfa437 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -37,7 +37,7 @@ import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager'; import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames, getRulesDataSource } from '../utils/datasource'; import { makeAMLink } from '../utils/misc'; import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux'; -import { getAlertInfo, isRulerNotSupportedResponse } from '../utils/rules'; +import { getAlertInfo } from '../utils/rules'; import { safeParsePrometheusDuration } from '../utils/time'; function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) { @@ -154,18 +154,6 @@ export function fetchPromAndRulerRulesAction({ }; } -// this will only trigger ruler rules fetch if rules are not loaded yet and request is not in flight -export function fetchRulerRulesIfNotFetchedYet(rulesSourceName: string): ThunkResult { - return (dispatch, getStore) => { - const { rulerRules } = getStore().unifiedAlerting; - const resp = rulerRules[rulesSourceName]; - const emptyResults = isEmpty(resp?.result); - if (emptyResults && !(resp && isRulerNotSupportedResponse(resp)) && !resp?.loading) { - dispatch(fetchRulerRulesAction({ rulesSourceName })); - } - }; -} - // TODO: memoize this or move to RTK Query so we can cache results! export function fetchAllPromBuildInfoAction(): ThunkResult> { return async (dispatch) => { @@ -280,12 +268,15 @@ export function fetchAllPromAndRulerRulesAction( }; } -export function fetchAllPromRulesAction(force = false): ThunkResult { +export function fetchAllPromRulesAction( + force = false, + options: FetchPromRulesRulesActionProps = {} +): ThunkResult> { return async (dispatch, getStore) => { const { promRules } = getStore().unifiedAlerting; getAllRulesSourceNames().map((rulesSourceName) => { if (force || !promRules[rulesSourceName]?.loading) { - dispatch(fetchPromRulesAction({ rulesSourceName })); + dispatch(fetchPromRulesAction({ rulesSourceName, ...options })); } }); }; diff --git a/public/app/features/alerting/unified/utils/constants.ts b/public/app/features/alerting/unified/utils/constants.ts index 1e4fb11524c..eaeeb23c590 100644 --- a/public/app/features/alerting/unified/utils/constants.ts +++ b/public/app/features/alerting/unified/utils/constants.ts @@ -1,6 +1,6 @@ export const RULER_NOT_SUPPORTED_MSG = 'ruler not supported'; -export const RULE_LIST_POLL_INTERVAL_MS = 20000; +export const RULE_LIST_POLL_INTERVAL_MS = 30000; export const ALERTMANAGER_NAME_QUERY_KEY = 'alertmanager'; export const ALERTMANAGER_NAME_LOCAL_STORAGE_KEY = 'alerting-alertmanager'; diff --git a/public/app/features/alerting/unified/utils/rule-id.test.ts b/public/app/features/alerting/unified/utils/rule-id.test.ts index d03f4b099e9..a24a468c260 100644 --- a/public/app/features/alerting/unified/utils/rule-id.test.ts +++ b/public/app/features/alerting/unified/utils/rule-id.test.ts @@ -1,5 +1,6 @@ import { renderHook } from '@testing-library/react-hooks'; +import { config } from '@grafana/runtime'; import { AlertingRule, RecordingRule, RuleIdentifier } from 'app/types/unified-alerting'; import { GrafanaAlertStateDecision, @@ -47,15 +48,6 @@ const recordingRule = { }; describe('hashRulerRule', () => { - it('should not hash unknown rule types', () => { - const unknownRule = {}; - - expect(() => { - // @ts-ignore - hashRulerRule(unknownRule); - }).toThrow('Only recording and alerting ruler rules can be hashed'); - }); - it('should hash recording rules', () => { const recordingRule: RulerRecordingRuleDTO = { record: 'instance:node_num_cpu:sum', @@ -151,6 +143,30 @@ describe('hashRulerRule', () => { it('should throw for malformed identifier', () => { expect(() => parse('foo$bar$baz', false)).toThrow(/failed to parse/i); }); + + describe('when prometheusRulesPrimary is enabled', () => { + beforeAll(() => { + config.featureToggles.alertingPrometheusRulesPrimary = true; + }); + afterAll(() => { + config.featureToggles.alertingPrometheusRulesPrimary = false; + }); + + it('should not take query into account', () => { + const rule1: RulerAlertingRuleDTO = { + ...alertingRule.ruler, + expr: 'vector(20) > 7', + }; + + const rule2: RulerAlertingRuleDTO = { + ...alertingRule.ruler, + expr: 'http_requests_total{node="node1"}', + }; + + expect(rule1.expr).not.toBe(rule2.expr); + expect(hashRulerRule(rule1)).toBe(hashRulerRule(rule2)); + }); + }); }); describe('hashRule', () => { @@ -167,6 +183,30 @@ describe('hashRule', () => { expect(promHash).toBe(rulerHash); }); + + describe('when prometheusRulesPrimary is enabled', () => { + beforeAll(() => { + config.featureToggles.alertingPrometheusRulesPrimary = true; + }); + afterAll(() => { + config.featureToggles.alertingPrometheusRulesPrimary = false; + }); + + it('should not take query into account', () => { + const rule1: AlertingRule = { + ...alertingRule.prom, + query: 'vector(20) > 7', + }; + + const rule2: AlertingRule = { + ...alertingRule.prom, + query: 'http_requests_total{node="node1"}', + }; + + expect(rule1.query).not.toBe(rule2.query); + expect(hashRule(rule1)).toBe(hashRule(rule2)); + }); + }); }); describe('equal', () => { diff --git a/public/app/features/alerting/unified/utils/rule-id.ts b/public/app/features/alerting/unified/utils/rule-id.ts index d9a5cd59193..8e44a5a3dc4 100644 --- a/public/app/features/alerting/unified/utils/rule-id.ts +++ b/public/app/features/alerting/unified/utils/rule-id.ts @@ -10,7 +10,9 @@ import { RuleIdentifier, RuleWithLocation, } from 'app/types/unified-alerting'; -import { Annotations, Labels, PromRuleType, RulerRuleDTO } from 'app/types/unified-alerting-dto'; +import { Annotations, Labels, PromRuleType, RulerCloudRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; + +import { shouldUsePrometheusRulesPrimary } from '../featureToggles'; import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { @@ -249,18 +251,19 @@ export function hashRulerRule(rule: RulerRuleDTO): string { return hash(JSON.stringify(fingerprint)).toString(); } -function getRulerRuleFingerprint(rule: RulerRuleDTO) { +function getRulerRuleFingerprint(rule: RulerCloudRuleDTO) { + const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); + // If the prometheusRulesPrimary feature toggle is enabled, we don't need to hash the query + // We need to make fingerprint compatibility between Prometheus and Ruler rules + // Query often differs between the two, so we can't use it to generate a fingerprint + const queryHash = prometheusRulesPrimary ? '' : hashQuery(rule.expr); + const labelsHash = hashLabelsOrAnnotations(rule.labels); + if (isRecordingRulerRule(rule)) { - return [rule.record, PromRuleType.Recording, hashQuery(rule.expr), hashLabelsOrAnnotations(rule.labels)]; + return [rule.record, PromRuleType.Recording, queryHash, labelsHash]; } if (isAlertingRulerRule(rule)) { - return [ - rule.alert, - PromRuleType.Alerting, - hashQuery(rule.expr), - hashLabelsOrAnnotations(rule.annotations), - hashLabelsOrAnnotations(rule.labels), - ]; + return [rule.alert, PromRuleType.Alerting, queryHash, hashLabelsOrAnnotations(rule.annotations), labelsHash]; } throw new Error('Only recording and alerting ruler rules can be hashed'); } @@ -271,17 +274,16 @@ export function hashRule(rule: Rule): string { } function getPromRuleFingerprint(rule: Rule) { + const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); + + const queryHash = prometheusRulesPrimary ? '' : hashQuery(rule.query); + const labelsHash = hashLabelsOrAnnotations(rule.labels); + if (isRecordingRule(rule)) { - return [rule.name, PromRuleType.Recording, hashQuery(rule.query), hashLabelsOrAnnotations(rule.labels)]; + return [rule.name, PromRuleType.Recording, queryHash, labelsHash]; } if (isAlertingRule(rule)) { - return [ - rule.name, - PromRuleType.Alerting, - hashQuery(rule.query), - hashLabelsOrAnnotations(rule.annotations), - hashLabelsOrAnnotations(rule.labels), - ]; + return [rule.name, PromRuleType.Alerting, queryHash, hashLabelsOrAnnotations(rule.annotations), labelsHash]; } throw new Error('Only recording and alerting rules can be hashed'); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index c1dab28f8ed..8d8bebbfc39 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -1,6 +1,5 @@ -import { act, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { render } from 'test/test-utils'; import { byTestId } from 'testing-library-selector'; import { DataSourceApi } from '@grafana/data'; @@ -76,11 +75,7 @@ const mocks = { }; const renderAlertTabContent = (model: PanelDataAlertingTab, initialStore?: ReturnType) => { - render( - - - - ); + render(); }; const promResponse: PromRulesResponse = { @@ -348,9 +343,9 @@ async function clickNewButton() { const oldPush = locationService.push; locationService.push = pushMock; const button = await ui.createButton.find(); - await act(async () => { - await userEvent.click(button); - }); + + await userEvent.click(button); + const match = pushMock.mock.lastCall[0].match(/alerting\/new\?defaults=(.*)&returnTo=/); const defaults = JSON.parse(decodeURIComponent(match![1])); locationService.push = oldPush; diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index 4e5418c7305..0c379f929b1 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -132,7 +132,7 @@ export interface PromAlertingRuleDTO extends PromRuleDTOBase { activeAt: string; value: string; }>; - labels: Labels; + labels?: Labels; annotations?: Annotations; duration?: number; // for state: PromAlertingRuleState; diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts index 734e6c650ad..74eb00ee147 100644 --- a/public/app/types/unified-alerting.ts +++ b/public/app/types/unified-alerting.ts @@ -42,7 +42,7 @@ interface RuleBase { export interface AlertingRule extends RuleBase { alerts?: Alert[]; - labels: { + labels?: { [key: string]: string; }; annotations?: { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index b75b66b65e3..4adc53c31a8 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -215,6 +215,12 @@ "paused": "Paused", "recording-rule": "Recording rule" }, + "rule-viewer": { + "prometheus-consistency-check": { + "alert-message": "Alert rule has been updated. Changes may take up to a minute to appear on the Alert rules list view.", + "alert-title": "Update in progress" + } + }, "rules": { "add-rule": { "success": "Rule added successfully" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index a237969e6d4..359abaed9f0 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -215,6 +215,12 @@ "paused": "Päūşęđ", "recording-rule": "Ŗęčőřđįʼnģ řūľę" }, + "rule-viewer": { + "prometheus-consistency-check": { + "alert-message": "Åľęřŧ řūľę ĥäş þęęʼn ūpđäŧęđ. Cĥäʼnģęş mäy ŧäĸę ūp ŧő ä mįʼnūŧę ŧő äppęäř őʼn ŧĥę Åľęřŧ řūľęş ľįşŧ vįęŵ.", + "alert-title": "Ůpđäŧę įʼn přőģřęşş" + } + }, "rules": { "add-rule": { "success": "Ŗūľę äđđęđ şūččęşşƒūľľy" From 9fc44364186309377a40c48abebf1fc90487be2f Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:28:37 +0100 Subject: [PATCH 027/174] I18n: Download translations from Crowdin (#93904) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 3 +++ public/locales/es-ES/grafana.json | 3 +++ public/locales/fr-FR/grafana.json | 3 +++ public/locales/pt-BR/grafana.json | 3 +++ public/locales/zh-Hans/grafana.json | 3 +++ 5 files changed, 15 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index d3d76b05f1d..f6e136d81f8 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1845,6 +1845,9 @@ "server-discovery-modal-close": "", "server-discovery-modal-loading": "", "server-discovery-modal-submit": "" + }, + "login": { + "error": "" } }, "panel": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index fff0891604b..f0aae406d36 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1845,6 +1845,9 @@ "server-discovery-modal-close": "", "server-discovery-modal-loading": "", "server-discovery-modal-submit": "" + }, + "login": { + "error": "" } }, "panel": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index c466169af69..e26f2e76f42 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1845,6 +1845,9 @@ "server-discovery-modal-close": "", "server-discovery-modal-loading": "", "server-discovery-modal-submit": "" + }, + "login": { + "error": "" } }, "panel": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index ed03bd7997e..ef605469c07 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1845,6 +1845,9 @@ "server-discovery-modal-close": "", "server-discovery-modal-loading": "", "server-discovery-modal-submit": "" + }, + "login": { + "error": "" } }, "panel": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 98e517fd53c..ebaeca8e959 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1836,6 +1836,9 @@ "server-discovery-modal-close": "", "server-discovery-modal-loading": "", "server-discovery-modal-submit": "" + }, + "login": { + "error": "" } }, "panel": { From 5b53b37634cd56ebcdb9a99f6b2bfc8820e6c21a Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 27 Sep 2024 15:39:29 +0300 Subject: [PATCH 028/174] Routing: Update components using props.match to use hooks (#93792) * RuleViewed: Get params from hook * ProviderConfigPage: Use hooks for redux logic * Update NewDashboardWithDS * Update StorageFolderPage * Update StoragePage * Cleanup * Update PublicDashboardPage * Update RuleEditor * Update BrowseFolderAlertingPage * Update BrowseFolderLibraryPanelsPage * Update SoloPanelPage * Fix test * Add useParams mocks * Update ServiceAccountPage * Simplify mocks * Cleanup * Reuse types for path params * Remove mock for router compat in test * Switch to element --------- Co-authored-by: Tom Ratcliffe --- .../features/alerting/unified/RuleEditor.tsx | 16 ++-- .../unified/RuleEditorExisting.test.tsx | 16 ++-- .../features/alerting/unified/RuleViewer.tsx | 12 +-- .../auth-config/ProviderConfigPage.tsx | 41 +++------- .../BrowseFolderAlertingPage.test.tsx | 33 +++----- .../BrowseFolderAlertingPage.tsx | 8 +- .../BrowseFolderLibraryPanelsPage.test.tsx | 32 +++----- .../BrowseFolderLibraryPanelsPage.tsx | 5 +- .../containers/NewDashboardWithDS.tsx | 6 +- .../containers/PublicDashboardPage.test.tsx | 15 ++-- .../containers/PublicDashboardPage.tsx | 10 ++- .../PublicDashboardPageProxy.test.tsx | 6 +- .../dashboard/containers/SoloPanelPage.tsx | 81 ++++++++----------- .../ServiceAccountPage.test.tsx | 34 ++------ .../serviceaccounts/ServiceAccountPage.tsx | 8 +- .../features/storage/StorageFolderPage.tsx | 8 +- public/app/features/storage/StoragePage.tsx | 3 +- 17 files changed, 135 insertions(+), 199 deletions(-) diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 4dfa212e4e3..9b482e07c04 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -1,9 +1,9 @@ import { useCallback } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { NavModelItem } from '@grafana/data'; import { withErrorBoundary } from '@grafana/ui'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useDispatch } from 'app/types'; import { RuleIdentifier } from 'app/types/unified-alerting'; @@ -17,10 +17,10 @@ import { fetchRulesSourceBuildInfoAction } from './state/actions'; import { useRulesAccess } from './utils/accessControlHooks'; import * as ruleId from './utils/rule-id'; -type RuleEditorProps = GrafanaRouteComponentProps<{ +type RuleEditorPathParams = { id?: string; type?: 'recording' | 'alerting' | 'grafana-recording'; -}>; +}; const defaultPageNav: Partial = { icon: 'bell', @@ -28,7 +28,7 @@ const defaultPageNav: Partial = { }; // sadly we only get the "type" when a new rule is being created, when editing an existing recording rule we can't actually know it from the URL -const getPageNav = (identifier?: RuleIdentifier, type?: 'recording' | 'alerting' | 'grafana-recording') => { +const getPageNav = (identifier?: RuleIdentifier, type?: RuleEditorPathParams['type']) => { if (type === 'recording' || type === 'grafana-recording') { if (identifier) { // this branch should never trigger actually, the type param isn't used when editing rules @@ -46,12 +46,12 @@ const getPageNav = (identifier?: RuleIdentifier, type?: 'recording' | 'alerting' } }; -const RuleEditor = ({ match }: RuleEditorProps) => { +const RuleEditor = () => { const dispatch = useDispatch(); const [searchParams] = useURLSearchParams(); - - const { type } = match.params; - const id = ruleId.getRuleIdFromPathname(match.params); + const params = useParams(); + const { type } = params; + const id = ruleId.getRuleIdFromPathname(params); const identifier = ruleId.tryParse(id, true); const copyFromId = searchParams.get('copyFrom') ?? undefined; diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx index 67b966d1ef8..a3aeec38bdc 100644 --- a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx @@ -1,4 +1,4 @@ -import { Route } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom-v5-compat'; import { ui } from 'test/helpers/alertingRuleEditor'; import { render, screen } from 'test/test-utils'; @@ -43,10 +43,15 @@ const mocks = { setupMswServer(); -function renderRuleEditor(identifier?: string) { - return render(, { - historyOptions: { initialEntries: [identifier ? `/alerting/${identifier}/edit` : `/alerting/new`] }, - }); +function renderRuleEditor(identifier: string) { + return render( + + } /> + , + { + historyOptions: { initialEntries: [`/alerting/${identifier}/edit`] }, + } + ); } describe('RuleEditor grafana managed rules', () => { @@ -106,7 +111,6 @@ describe('RuleEditor grafana managed rules', () => { // mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); mocks.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]); - const { user } = renderRuleEditor(grafanaRulerRule.grafana_alert.uid); // check that it's filled in diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 7e9aa0152b2..9584539e41b 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -1,10 +1,10 @@ import { useMemo } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; import { isFetchError } from '@grafana/runtime'; import { Alert, withErrorBoundary } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertRuleProvider } from './components/rule-viewer/RuleContext'; @@ -13,13 +13,9 @@ import { useCombinedRule } from './hooks/useCombinedRule'; import { stringifyErrorLike } from './utils/misc'; import { getRuleIdFromPathname, parse as parseRuleId } from './utils/rule-id'; -type RuleViewerProps = GrafanaRouteComponentProps<{ - id: string; - sourceName: string; -}>; - -const RuleViewer = (props: RuleViewerProps): JSX.Element => { - const id = getRuleIdFromPathname(props.match.params); +const RuleViewer = (): JSX.Element => { + const params = useParams(); + const id = getRuleIdFromPathname(params); const [activeTab] = useActiveTab(); const instancesTab = activeTab === ActiveTab.Instances; diff --git a/public/app/features/auth-config/ProviderConfigPage.tsx b/public/app/features/auth-config/ProviderConfigPage.tsx index 82f3ec5072b..7e8e23b5e0f 100644 --- a/public/app/features/auth-config/ProviderConfigPage.tsx +++ b/public/app/features/auth-config/ProviderConfigPage.tsx @@ -1,13 +1,11 @@ import { useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; import { Badge, Stack, Text } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; - -import { PageNotFound } from '../../core/components/PageNotFound/PageNotFound'; -import { StoreState } from '../../types'; +import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound'; +import { useDispatch, useSelector } from 'app/types'; import { ProviderConfigForm } from './ProviderConfigForm'; import { UIMap } from './constants'; @@ -34,33 +32,18 @@ const getPageNav = (config?: SSOProvider): NavModelItem => { }; }; -interface RouteProps extends GrafanaRouteComponentProps<{ provider: string }> {} - -function mapStateToProps(state: StoreState, props: RouteProps) { - const { isLoading, providers } = state.authConfig; - const { provider } = props.match.params; - const config = providers.find((config) => config.provider === provider); - return { - config, - isLoading, - provider, - }; -} - -const mapDispatchToProps = { - loadProviders, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); -export type Props = ConnectedProps; - /** * Separate the Page logic from the Content logic for easier testing. */ -export const ProviderConfigPage = ({ config, loadProviders, isLoading, provider }: Props) => { +export const ProviderConfigPage = () => { + const dispatch = useDispatch(); + const { isLoading, providers } = useSelector((store) => store.authConfig); + const { provider = '' } = useParams(); + const config = providers.find((config) => config.provider === provider); + useEffect(() => { - loadProviders(provider); - }, [loadProviders, provider]); + dispatch(loadProviders(provider)); + }, [dispatch, provider]); if (!config || !config.provider || !UIMap[config.provider]) { return ; @@ -88,4 +71,4 @@ export const ProviderConfigPage = ({ config, loadProviders, isLoading, provider ); }; -export default connector(ProviderConfigPage); +export default ProviderConfigPage; diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx index 69b22171aba..95f10cc443e 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx @@ -1,13 +1,13 @@ import { render as rtlRender, screen } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { SetupServer, setupServer } from 'msw/node'; +import { useParams } from 'react-router-dom-v5-compat'; import { TestProvider } from 'test/helpers/TestProvider'; import { contextSrv } from 'app/core/core'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { backendSrv } from 'app/core/services/backend_srv'; -import BrowseFolderAlertingPage, { OwnProps } from './BrowseFolderAlertingPage'; +import BrowseFolderAlertingPage from './BrowseFolderAlertingPage'; import { getPrometheusRulesResponse, getRulerRulesResponse } from './fixtures/alertRules.fixture'; import * as permissions from './permissions'; @@ -23,7 +23,10 @@ jest.mock('@grafana/runtime', () => ({ unifiedAlertingEnabled: true, }, })); - +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn(), +})); const mockFolderName = 'myFolder'; const mockFolderUid = '12345'; @@ -31,7 +34,7 @@ const mockRulerRulesResponse = getRulerRulesResponse(mockFolderName, mockFolderU const mockPrometheusRulesResponse = getPrometheusRulesResponse(mockFolderName); describe('browse-dashboards BrowseFolderAlertingPage', () => { - let props: OwnProps; + (useParams as jest.Mock).mockReturnValue({ uid: mockFolderUid }); let server: SetupServer; const mockPermissions = { canCreateDashboards: true, @@ -68,18 +71,6 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { beforeEach(() => { jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); - props = { - ...getRouteComponentProps({ - match: { - params: { - uid: mockFolderUid, - }, - isExact: false, - path: '', - url: '', - }, - }), - }; }); afterEach(() => { @@ -88,12 +79,12 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { }); it('displays the folder title', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); }); it('displays the "Folder actions" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); @@ -107,13 +98,13 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { canSetPermissions: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('displays all the folder tabs and shows the "Alert rules" tab as selected', async () => { - render(); + render(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toBeInTheDocument(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toHaveAttribute('aria-selected', 'false'); @@ -125,7 +116,7 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { }); it('displays the alert rules returned by the API', async () => { - render(); + render(); const ruleName = mockPrometheusRulesResponse.data.groups[0].rules[0].name; expect(await screen.findByRole('link', { name: ruleName })).toBeInTheDocument(); diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx index 4c137afff9b..74931bc2170 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { buildNavModel, getAlertingTabID } from 'app/features/folders/state/navModel'; import { useSelector } from 'app/types'; @@ -10,10 +10,8 @@ import { AlertsFolderView } from '../alerting/unified/AlertsFolderView'; import { useGetFolderQuery, useSaveFolderMutation } from './api/browseDashboardsAPI'; import { FolderActionsButton } from './components/FolderActionsButton'; -export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {} - -export function BrowseFolderAlertingPage({ match }: OwnProps) { - const { uid: folderUID } = match.params; +export function BrowseFolderAlertingPage() { + const { uid: folderUID = '' } = useParams(); const { data: folderDTO } = useGetFolderQuery(folderUID); const folder = useSelector((state) => state.folder); const [saveFolder] = useSaveFolderMutation(); diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx index bb758e875e8..dec629c0181 100644 --- a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx @@ -1,13 +1,13 @@ import { render as rtlRender, screen } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { SetupServer, setupServer } from 'msw/node'; +import { useParams } from 'react-router-dom-v5-compat'; import { TestProvider } from 'test/helpers/TestProvider'; import { contextSrv } from 'app/core/core'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { backendSrv } from 'app/core/services/backend_srv'; -import BrowseFolderLibraryPanelsPage, { OwnProps } from './BrowseFolderLibraryPanelsPage'; +import BrowseFolderLibraryPanelsPage from './BrowseFolderLibraryPanelsPage'; import { getLibraryElementsResponse } from './fixtures/libraryElements.fixture'; import * as permissions from './permissions'; @@ -23,6 +23,10 @@ jest.mock('@grafana/runtime', () => ({ unifiedAlertingEnabled: true, }, })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn(), +})); const mockFolderName = 'myFolder'; const mockFolderUid = '12345'; @@ -31,7 +35,7 @@ const mockLibraryElementsResponse = getLibraryElementsResponse(1, { }); describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { - let props: OwnProps; + (useParams as jest.Mock).mockReturnValue({ uid: mockFolderUid }); let server: SetupServer; const mockPermissions = { canCreateDashboards: true, @@ -70,18 +74,6 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { beforeEach(() => { jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); - props = { - ...getRouteComponentProps({ - match: { - params: { - uid: mockFolderUid, - }, - isExact: false, - path: '', - url: '', - }, - }), - }; }); afterEach(() => { @@ -90,12 +82,12 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { }); it('displays the folder title', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); }); it('displays the "Folder actions" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); @@ -109,13 +101,13 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { canSetPermissions: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('displays all the folder tabs and shows the "Library panels" tab as selected', async () => { - render(); + render(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toBeInTheDocument(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toHaveAttribute('aria-selected', 'false'); @@ -127,7 +119,7 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { }); it('displays the library panels returned by the API', async () => { - render(); + render(); expect(await screen.findByText(mockLibraryElementsResponse.elements[0].name)).toBeInTheDocument(); }); diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx index 48caa971c62..0211f45a1a9 100644 --- a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { Page } from 'app/core/components/Page/Page'; @@ -13,8 +14,8 @@ import { useGetFolderQuery, useSaveFolderMutation } from './api/browseDashboards export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {} -export function BrowseFolderLibraryPanelsPage({ match }: OwnProps) { - const { uid: folderUID } = match.params; +export function BrowseFolderLibraryPanelsPage() { + const { uid: folderUID = '' } = useParams(); const { data: folderDTO } = useGetFolderQuery(folderUID); const [selected, setSelected] = useState(undefined); const [saveFolder] = useSaveFolderMutation(); diff --git a/public/app/features/dashboard/containers/NewDashboardWithDS.tsx b/public/app/features/dashboard/containers/NewDashboardWithDS.tsx index 5da67c8fe32..e1f543bf3ca 100644 --- a/public/app/features/dashboard/containers/NewDashboardWithDS.tsx +++ b/public/app/features/dashboard/containers/NewDashboardWithDS.tsx @@ -1,15 +1,15 @@ import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useDispatch } from 'app/types'; import { setInitialDatasource } from '../state/reducers'; -export default function NewDashboardWithDS(props: GrafanaRouteComponentProps<{ datasourceUid: string }>) { +export default function NewDashboardWithDS() { const [error, setError] = useState(null); - const { datasourceUid } = props.match.params; + const { datasourceUid } = useParams(); const dispatch = useDispatch(); useEffect(() => { diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index e2619b6f757..6968530b55d 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; -import { match, Router } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { useEffectOnce } from 'react-use'; import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; @@ -34,8 +34,8 @@ jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { }); jest.mock('react-virtualized-auto-sizer', () => { - // // // The size of the children need to be small enough to be outside the view. - // // // So it does not trigger the query to be run by the PanelQueryRunner. + // The size of the children need to be small enough to be outside the view. + // So it does not trigger the query to be run by the PanelQueryRunner. return ({ children }: AutoSizerProps) => children({ height: 1, @@ -55,13 +55,16 @@ jest.mock('app/types', () => ({ useDispatch: () => jest.fn(), })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn().mockReturnValue({ accessToken: 'an-access-token' }), +})); + const setup = (propOverrides?: Partial, initialState?: Partial) => { const context = getGrafanaContextMock(); const store = configureStore(initialState); - const props: Props = { ...getRouteComponentProps({ - match: { params: { accessToken: 'an-access-token' }, isExact: true, url: '', path: '' }, route: { routeName: DashboardRoutes.Public, path: '/public-dashboards/:accessToken', @@ -250,7 +253,7 @@ describe('PublicDashboardPage', () => { describe('When public dashboard changes', () => { it('Should init again', async () => { const { rerender } = setup(); - rerender({ match: { params: { accessToken: 'another-new-access-token' } } as unknown as match }); + rerender({}); await waitFor(() => { expect(initDashboard).toHaveBeenCalledTimes(2); }); diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 7d12cf4bf08..794baab1454 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { useEffect } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { usePrevious } from 'react-use'; import { GrafanaTheme2, PageLayoutType, TimeZone } from '@grafana/data'; @@ -52,7 +53,8 @@ const Toolbar = ({ dashboard }: { dashboard: DashboardModel }) => { }; const PublicDashboardPage = (props: Props) => { - const { match, route, location } = props; + const { route, location } = props; + const { accessToken } = useParams(); const dispatch = useDispatch(); const context = useGrafana(); const prevProps = usePrevious(props); @@ -65,11 +67,11 @@ const PublicDashboardPage = (props: Props) => { initDashboard({ routeName: route.routeName, fixUrl: false, - accessToken: match.params.accessToken, + accessToken, keybindingSrv: context.keybindings, }) ); - }, [route.routeName, match.params.accessToken, context.keybindings, dispatch]); + }, [route.routeName, accessToken, context.keybindings, dispatch]); useEffect(() => { if (prevProps?.location.search !== location.search) { @@ -88,7 +90,7 @@ const PublicDashboardPage = (props: Props) => { getTimeSrv().setAutoRefresh(urlParams.refresh); } } - }, [prevProps, location.search, props.queryParams, dashboard?.timepicker.hidden, match.params.accessToken]); + }, [prevProps, location.search, props.queryParams, dashboard?.timepicker.hidden, accessToken]); if (!dashboard) { return ; diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx index 49193505061..dd8ea8b4826 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx @@ -25,10 +25,14 @@ jest.mock('@grafana/runtime', () => ({ }), })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: () => ({ accessToken: 'an-access-token' }), +})); + function setup(props: Partial) { const context = getGrafanaContextMock(); const store = configureStore({}); - return render( diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 76e38b778c8..48dc90beba1 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -1,15 +1,16 @@ import { css } from '@emotion/css'; -import { Component } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom-v5-compat'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, useStyles2 } from '@grafana/ui'; -import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { StoreState } from 'app/types'; +import { useGrafana } from '../../../core/context/GrafanaContext'; import { DashboardPanel } from '../dashgrid/DashboardPanel'; import { initDashboard } from '../state/initDashboard'; @@ -37,69 +38,55 @@ export interface State { notFound: boolean; } -export class SoloPanelPage extends Component { - declare context: GrafanaContextType; - static contextType = GrafanaContext; +export const SoloPanelPage = ({ route, queryParams, dashboard, initDashboard }: Props) => { + const [panel, setPanel] = useState(null); + const [notFound, setNotFound] = useState(false); + const { keybindings } = useGrafana(); - state: State = { - panel: null, - notFound: false, - }; + const { slug, uid, type } = useParams(); - componentDidMount() { - const { match, route } = this.props; - - this.props.initDashboard({ - urlSlug: match.params.slug, - urlUid: match.params.uid, - urlType: match.params.type, + useEffect(() => { + initDashboard({ + urlSlug: slug, + urlUid: uid, + urlType: type, routeName: route.routeName, fixUrl: false, - keybindingSrv: this.context.keybindings, + keybindingSrv: keybindings, }); - } + }, [slug, uid, type, route.routeName, initDashboard, keybindings]); - getPanelId(): number { - return parseInt(this.props.queryParams.panelId ?? '0', 10); - } + const getPanelId = useCallback(() => { + return parseInt(queryParams.panelId ?? '0', 10); + }, [queryParams.panelId]); - componentDidUpdate(prevProps: Props) { - const { dashboard } = this.props; - - if (!dashboard) { - return; - } - - // we just got a new dashboard - if (!prevProps.dashboard || prevProps.dashboard.uid !== dashboard.uid) { - const panel = dashboard.getPanelByUrlId(this.props.queryParams.panelId); + useEffect(() => { + if (dashboard) { + const panel = dashboard.getPanelByUrlId(queryParams.panelId); if (!panel) { - this.setState({ notFound: true }); + setNotFound(true); return; } if (panel) { dashboard.exitViewPanel(panel); } - - this.setState({ panel }); + setPanel(panel); dashboard.initViewPanel(panel); } - } + }, [dashboard, queryParams.panelId]); - render() { - return ( - - ); - } -} + return ( + + ); +}; export interface SoloPanelProps extends State { dashboard: DashboardModel | null; diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx index 2e6478f738a..fe3e5cb42f8 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx @@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; -import { RouteDescriptor } from 'app/core/navigation/types'; import { ApiKey, OrgRole, ServiceAccountDTO } from 'app/types'; import { ServiceAccountPageUnconnected, Props } from './ServiceAccountPage'; @@ -15,6 +14,11 @@ jest.mock('app/core/core', () => ({ }, })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: () => ({ id: '1' }), +})); + const setup = (propOverrides: Partial) => { const createServiceAccountTokenMock = jest.fn(); const deleteServiceAccountMock = jest.fn(); @@ -23,38 +27,10 @@ const setup = (propOverrides: Partial) => { const loadServiceAccountTokensMock = jest.fn(); const updateServiceAccountMock = jest.fn(); - const mockLocation = { - search: '', - pathname: '', - state: undefined, - hash: '', - }; const props: Props = { serviceAccount: {} as ServiceAccountDTO, tokens: [], isLoading: false, - match: { - params: { id: '1' }, - isExact: true, - path: '/org/serviceaccounts/1', - url: 'http://localhost:3000/org/serviceaccounts/1', - }, - history: { - length: 0, - action: 'PUSH', - location: mockLocation, - push: jest.fn(), - replace: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - block: jest.fn(), - listen: jest.fn(), - createHref: jest.fn(), - }, - location: mockLocation, - queryParams: {}, - route: {} as RouteDescriptor, timezone: '', createServiceAccountToken: createServiceAccountTokenMock, deleteServiceAccount: deleteServiceAccountMock, diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 3ec2b9f5185..d68bdb53233 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom-v5-compat'; import { getTimeZone, NavModelItem } from '@grafana/data'; import { Button, ConfirmModal, IconButton, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AccessControlAction, ApiKey, ServiceAccountDTO, StoreState } from 'app/types'; import { ServiceAccountPermissions } from './ServiceAccountPermissions'; @@ -22,7 +22,7 @@ import { updateServiceAccount, } from './state/actionsServiceAccountPage'; -interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { +interface OwnProps { serviceAccount?: ServiceAccountDTO; tokens: ApiKey[]; isLoading: boolean; @@ -51,7 +51,6 @@ const connector = connect(mapStateToProps, mapDispatchToProps); export type Props = OwnProps & ConnectedProps; export const ServiceAccountPageUnconnected = ({ - match, serviceAccount, tokens, timezone, @@ -67,8 +66,9 @@ export const ServiceAccountPageUnconnected = ({ const [isTokenModalOpen, setIsTokenModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDisableModalOpen, setIsDisableModalOpen] = useState(false); + const { id = '' } = useParams(); - const serviceAccountId = parseInt(match.params.id, 10); + const serviceAccountId = parseInt(id, 10); const tokenActionsDisabled = serviceAccount.isDisabled || serviceAccount.isExternal || diff --git a/public/app/features/storage/StorageFolderPage.tsx b/public/app/features/storage/StorageFolderPage.tsx index b19e4b4791f..1808f6c51c2 100644 --- a/public/app/features/storage/StorageFolderPage.tsx +++ b/public/app/features/storage/StorageFolderPage.tsx @@ -1,16 +1,14 @@ +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { DataFrame, NavModel, NavModelItem } from '@grafana/data'; import { Card, Icon, Spinner } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getGrafanaStorage } from './storage'; -export interface Props extends GrafanaRouteComponentProps<{ slug: string }> {} - -export function StorageFolderPage(props: Props) { - const slug = props.match.params.slug ?? ''; +export function StorageFolderPage() { + const { slug = '' } = useParams(); const listing = useAsync((): Promise => { return getGrafanaStorage().list('content/' + slug); }, [slug]); diff --git a/public/app/features/storage/StoragePage.tsx b/public/app/features/storage/StoragePage.tsx index ac37ab0c9f5..3217e58ccc3 100644 --- a/public/app/features/storage/StoragePage.tsx +++ b/public/app/features/storage/StoragePage.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data'; @@ -46,7 +47,7 @@ const getParentPath = (path: string) => { export default function StoragePage(props: Props) { const styles = useStyles2(getStyles); const navModel = useNavModel('storage'); - const path = props.match.params.path ?? ''; + const { path = '' } = useParams(); const view = props.queryParams.view ?? StorageView.Data; const setPath = (p: string, view?: StorageView) => { let url = ('/admin/storage/' + p).replace('//', '/'); From 598179227c235181dbc864c74b421c47bb7e37e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:44:00 +0100 Subject: [PATCH 029/174] Update dependency yaml to v2.5.1 (#93899) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cab80717a2d..aa7ee811029 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33511,11 +33511,11 @@ __metadata: linkType: hard "yaml@npm:^2.0.0, yaml@npm:^2.3.4": - version: 2.4.5 - resolution: "yaml@npm:2.4.5" + version: 2.5.1 + resolution: "yaml@npm:2.5.1" bin: yaml: bin.mjs - checksum: 10/b09bf5a615a65276d433d76b8e34ad6b4c0320b85eb3f1a39da132c61ae6e2ff34eff4624e6458d96d49566c93cf43408ba5e568218293a8c6541a2006883f64 + checksum: 10/0eecb679db75ea6a989ad97715a9fa5d946972945aa6aa7d2175bca66c213b5564502ccb1cdd04b1bf816ee38b5c43e4e2fda3ff6f5e09da24dabb51ae92c57d languageName: node linkType: hard From 7e94d05d39a525f178f8e43e82f25279328e2538 Mon Sep 17 00:00:00 2001 From: Misi Date: Fri, 27 Sep 2024 14:57:46 +0200 Subject: [PATCH 030/174] Auth: Fix token rotation redirect when session storage redirect is enabled (#93906) Fix token rotation redirect when session storage redirect is enabled --- pkg/api/user_token.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 7aedc5f0d5e..12894a1a103 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -88,7 +88,11 @@ func (hs *HTTPServer) RotateUserAuthTokenRedirect(c *contextmodel.ReqContext) re return response.Redirect(hs.GetRedirectURL(c)) } - return response.Redirect(hs.Cfg.AppSubURL + "/") + redirectTo := c.Query("redirectTo") + if err := hs.ValidateRedirectTo(redirectTo); err != nil { + return response.Redirect(hs.Cfg.AppSubURL + "/") + } + return response.Redirect(hs.Cfg.AppSubURL + redirectTo) } // swagger:route POST /user/auth-tokens/rotate @@ -133,7 +137,6 @@ func (hs *HTTPServer) rotateToken(c *contextmodel.ReqContext) error { IP: ip, UserAgent: c.Req.UserAgent(), }) - if err != nil { return err } From 1941ae21d780e858b59e3a0a9d7d379077de918e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Sep 2024 15:11:28 +0200 Subject: [PATCH 031/174] DashboardScene: Refactor body property to be layout manager interface (#93738) * Began layout refactor * fixing tests * Progress * Progress * Progress * Progress * Progress * Progress * finally no errors * Remove unused interface * Remove unused interface * fixed tests * Update * Update * Fixes to keyboard shortcuts and solo route * fix lint issues --- .betterer.results | 4 + .../inspect/HelpWizard/HelpWizard.test.tsx | 17 +- .../HelpWizard/SupportSnapshotService.test.ts | 17 +- .../inspect/InspectJsonTab.test.tsx | 33 +- .../panel-edit/PanelEditor.test.ts | 16 +- .../saving/DashboardPrompt.test.tsx | 26 +- .../scene/AddLibraryPanelDrawer.test.tsx | 115 +--- .../scene/AddLibraryPanelDrawer.tsx | 39 +- .../DashboardDatasourceBehaviour.test.tsx | 180 +----- .../scene/DashboardGridItem.test.tsx | 14 +- .../scene/DashboardScene.test.tsx | 608 +++--------------- .../dashboard-scene/scene/DashboardScene.tsx | 290 +-------- .../scene/DashboardSceneUrlSync.test.ts | 50 +- .../scene/DashboardSceneUrlSync.ts | 21 +- .../scene/LibraryPanelBehavior.test.tsx | 7 +- .../scene/NavToolbarActions.test.tsx | 48 +- .../scene/PanelMenuBehavior.test.tsx | 16 +- .../scene/RowRepeaterBehavior.test.tsx | 3 +- .../scene/ViewPanelScene.test.tsx | 35 +- .../scene/keyboardShortcuts.ts | 9 +- .../DefaultGridLayoutManager.test.tsx | 281 ++++++++ .../DefaultGridLayoutManager.tsx | 355 ++++++++++ .../scene/row-actions/RowActions.tsx | 23 +- .../features/dashboard-scene/scene/types.ts | 88 ++- .../transformSaveModelToScene.test.ts | 14 +- .../transformSaveModelToScene.ts | 11 +- .../transformSceneToSaveModel.test.ts | 6 +- .../transformSceneToSaveModel.ts | 6 +- .../settings/AnnotationsEditView.test.tsx | 5 +- .../settings/DashboardLinksEditView.test.tsx | 5 +- .../settings/GeneralSettingsEditView.test.tsx | 5 +- .../settings/PermissionsEditView.test.tsx | 5 +- .../settings/VariablesEditView.test.tsx | 28 +- .../settings/VersionsEditView.test.tsx | 5 +- .../ExportButton/ExportButton.test.tsx | 17 +- .../sharing/ExportButton/ExportMenu.test.tsx | 17 +- .../sharing/ShareButton/ShareButton.test.tsx | 17 +- .../sharing/ShareButton/ShareMenu.test.tsx | 17 +- .../share-externally/ShareAlerts.test.tsx | 16 +- .../share-externally/ShareExternally.test.tsx | 31 +- .../sharing/ShareDrawer/ShareDrawer.test.tsx | 5 +- .../sharing/ShareLinkTab.test.tsx | 17 +- .../sharing/public-dashboards/utils.ts | 69 +- .../dashboard-scene/solo/useSoloPanel.ts | 29 +- ...DashboardModelCompatibilityWrapper.test.ts | 119 ++-- .../DashboardModelCompatibilityWrapper.ts | 45 +- .../utils/dashboardSceneGraph.test.ts | 251 +------- .../utils/dashboardSceneGraph.ts | 75 +-- .../dashboard-scene/utils/test-utils.ts | 7 +- .../features/dashboard-scene/utils/utils.ts | 13 - 50 files changed, 1212 insertions(+), 1918 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx diff --git a/.betterer.results b/.betterer.results index 3ec225ceb44..a6c0daa79b8 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2785,6 +2785,10 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], + "public/app/features/dashboard-scene/scene/types.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + ], "public/app/features/dashboard-scene/serialization/angularMigration.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx index e217fbd6d35..da8dd9998f7 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx @@ -2,12 +2,12 @@ import { render, screen } from '@testing-library/react'; import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes'; +import { SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelMenuBehavior } from '../../scene/PanelMenuBehavior'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import { HelpWizard } from './HelpWizard'; @@ -67,18 +67,7 @@ async function buildTestScene() { canEdit: true, isEmbedded: false, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); await new Promise((r) => setTimeout(r, 1)); diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts b/public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts index 4d1fc427cbd..4ecf7826ae4 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts @@ -1,10 +1,10 @@ import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data'; -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes'; +import { SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelMenuBehavior } from '../../scene/PanelMenuBehavior'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import { SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; @@ -114,18 +114,7 @@ async function buildTestScene() { canEdit: true, isEmbedded: false, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); await new Promise((r) => setTimeout(r, 1)); diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 84e7df9aa7c..ed28bd2c630 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -12,7 +12,7 @@ import { } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; -import { SceneCanvasText, SceneDataTransformer, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { SceneCanvasText, SceneDataTransformer, SceneQueryRunner, VizPanel } from '@grafana/scenes'; import * as libpanels from 'app/features/library-panels/state/api'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -20,6 +20,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { activateFullSceneTree } from '../utils/test-utils'; import { findVizPanelByKey } from '../utils/utils'; @@ -106,7 +107,7 @@ describe('InspectJsonTab', () => { const { tab } = await buildTestScene(); const obj = JSON.parse(tab.state.jsonText); - expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 10, h: 12 }); + expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 8, h: 10 }); expect(tab.isEditable()).toBe(true); }); @@ -114,7 +115,7 @@ describe('InspectJsonTab', () => { const { tab } = await buildTestSceneWithLibraryPanel(); const obj = JSON.parse(tab.state.jsonText); - expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 10, h: 12 }); + expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 8, h: 10 }); expect(obj.type).toEqual('table'); expect(tab.isEditable()).toBe(false); }); @@ -202,18 +203,7 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); activateFullSceneTree(scene); @@ -265,24 +255,13 @@ async function buildTestSceneWithLibraryPanel() { jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue({ ...libraryPanelState, ...panel }); - const gridItem = new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: libraryPanel, - }); - const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [gridItem], - }), + body: DefaultGridLayoutManager.fromVizPanels([libraryPanel]), }); activateFullSceneTree(scene); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index 8117146a4fc..78370296e0a 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -21,6 +21,7 @@ import * as libAPI from 'app/features/library-panels/state/api'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { activateFullSceneTree } from '../utils/test-utils'; import { findVizPanelByKey, getQueryRunnerFor } from '../utils/utils'; @@ -124,7 +125,8 @@ describe('PanelEditor', () => { const { panelEditor, dashboard } = await setup({ isNewPanel: true }); panelEditor.onDiscard(); - expect((dashboard.state.body as SceneGridLayout).state.children.length).toBe(0); + const panels = dashboard.state.body.getVizPanels(); + expect(panels.length).toBe(0); }); it('should discard query runner changes', async () => { @@ -212,8 +214,10 @@ describe('PanelEditor', () => { editPanel: editScene, $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), isEditing: true, - body: new SceneGridLayout({ - children: [gridItem], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [gridItem], + }), }), }); @@ -332,8 +336,10 @@ async function setup(options: SetupOptions = {}) { }), ], }), - body: new SceneGridLayout({ - children: [gridItem], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [gridItem], + }), }), }); diff --git a/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx b/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx index 76dc822344f..ac2014b6fd5 100644 --- a/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx +++ b/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx @@ -1,9 +1,9 @@ -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; +import { SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { ignoreChanges } from './DashboardPrompt'; @@ -136,20 +136,14 @@ function buildTestScene(overrides?: Partial) { }), controls: new DashboardControls({}), $behaviors: [new behaviors.CursorSync({})], - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + ]), ...overrides, }); diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index 90116dec207..f298e7b19f9 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -1,12 +1,12 @@ -import { SceneGridLayout, SceneGridRow, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; import { activateFullSceneTree } from '../utils/test-utils'; import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer'; -import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -41,13 +41,12 @@ describe('AddLibraryPanelWidget', () => { addLibPanelDrawer.onAddLibraryPanel(panelInfo); - const layout = dashboard.state.body as SceneGridLayout; - const gridItem = layout.state.children[0] as DashboardGridItem; + const panels = dashboard.state.body.getVizPanels(); + const panel = panels[0]; - expect(layout.state.children.length).toBe(1); - expect(gridItem.state.body!).toBeInstanceOf(VizPanel); - expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); - expect(gridItem.state.body.state.key).toBe('panel-1'); + expect(panels.length).toBe(1); + expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); + expect(panel.state.key).toBe('panel-1'); }); it('should add library panel from menu and enter edit mode in a dashboard that is not already in edit mode', async () => { @@ -60,9 +59,6 @@ describe('AddLibraryPanelWidget', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), overlay: drawer, }); @@ -86,24 +82,15 @@ describe('AddLibraryPanelWidget', () => { drawer.onAddLibraryPanel(panelInfo); - const layout = dashboard.state.body as SceneGridLayout; - const gridItem = layout.state.children[0] as DashboardGridItem; + const panels = dashboard.state.body.getVizPanels(); + const panel = panels[0]; - expect(layout.state.children.length).toBe(1); - expect(gridItem.state.body!).toBeInstanceOf(VizPanel); - expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); - expect(gridItem.state.body.state.key).toBe('panel-1'); + expect(panels.length).toBe(1); + expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); + expect(panel.state.key).toBe('panel-1'); expect(dashboard.state.isEditing).toBe(true); }); - it('should throw error if adding lib panel in a layout that is not SceneGridLayout', () => { - dashboard.setState({ body: undefined }); - - expect(() => addLibPanelDrawer.onAddLibraryPanel({} as LibraryPanel)).toThrow( - 'Trying to add a library panel in a layout that is not SceneGridLayout' - ); - }); - it('should replace grid item when grid item state is passed', async () => { const libPanel = new VizPanel({ title: 'Panel Title', @@ -112,10 +99,6 @@ describe('AddLibraryPanelWidget', () => { $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], }); - let gridItem = new DashboardGridItem({ - body: libPanel, - key: 'grid-item-1', - }); addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() }); dashboard = new DashboardScene({ $timeRange: new SceneTimeRange({}), @@ -125,9 +108,7 @@ describe('AddLibraryPanelWidget', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [gridItem], - }), + body: DefaultGridLayoutManager.fromVizPanels([libPanel]), overlay: addLibPanelDrawer, }); @@ -143,71 +124,12 @@ describe('AddLibraryPanelWidget', () => { addLibPanelDrawer.onAddLibraryPanel(panelInfo); - const layout = dashboard.state.body as SceneGridLayout; - gridItem = layout.state.children[0] as DashboardGridItem; - const behavior = gridItem.state.body!.state.$behaviors![0] as LibraryPanelBehavior; + const panels = dashboard.state.body.getVizPanels(); + expect(panels.length).toBe(1); + + const behavior = panels[0].state.$behaviors![0] as LibraryPanelBehavior; - expect(layout.state.children.length).toBe(1); - expect(gridItem.state.body!).toBeInstanceOf(VizPanel); expect(behavior).toBeInstanceOf(LibraryPanelBehavior); - expect(gridItem.state.key).toBe('grid-item-1'); - expect(behavior.state.uid).toBe('new_uid'); - expect(behavior.state.name).toBe('new_name'); - }); - - it('should replace grid item in row when grid item state is passed', async () => { - const libPanel = new VizPanel({ - title: 'Panel Title', - pluginId: 'table', - key: 'panel-1', - $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], - }); - - let gridItem = new DashboardGridItem({ - body: libPanel, - key: 'grid-item-1', - }); - addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() }); - dashboard = new DashboardScene({ - $timeRange: new SceneTimeRange({}), - title: 'hello', - uid: 'dash-1', - version: 4, - meta: { - canEdit: true, - }, - body: new SceneGridLayout({ - children: [ - new SceneGridRow({ - children: [gridItem], - }), - ], - }), - overlay: addLibPanelDrawer, - }); - - const panelInfo: LibraryPanel = { - uid: 'new_uid', - model: { - type: 'timeseries', - }, - name: 'new_name', - version: 1, - type: 'timeseries', - }; - - addLibPanelDrawer.onAddLibraryPanel(panelInfo); - - const layout = dashboard.state.body as SceneGridLayout; - const gridRow = layout.state.children[0] as SceneGridRow; - gridItem = gridRow.state.children[0] as DashboardGridItem; - const behavior = gridItem.state.body!.state.$behaviors![0] as LibraryPanelBehavior; - - expect(layout.state.children.length).toBe(1); - expect(gridRow.state.children.length).toBe(1); - expect(gridItem.state.body!).toBeInstanceOf(VizPanel); - expect(behavior).toBeInstanceOf(LibraryPanelBehavior); - expect(gridItem.state.key).toBe('grid-item-1'); expect(behavior.state.uid).toBe('new_uid'); expect(behavior.state.name).toBe('new_name'); }); @@ -223,9 +145,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), overlay: drawer, }); diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx index 54f7679c87a..a06154459ff 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx @@ -1,11 +1,4 @@ -import { - SceneComponentProps, - SceneGridLayout, - SceneObjectBase, - SceneObjectRef, - SceneObjectState, - VizPanel, -} from '@grafana/scenes'; +import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes'; import { LibraryPanel } from '@grafana/schema'; import { Drawer } from '@grafana/ui'; import { t } from 'app/core/internationalization'; @@ -14,8 +7,7 @@ import { LibraryPanelsSearchVariant, } from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; -import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; -import { NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getDashboardSceneFor, getDefaultVizPanel } from '../utils/utils'; +import { getDashboardSceneFor, getDefaultVizPanel } from '../utils/utils'; import { DashboardGridItem } from './DashboardGridItem'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; @@ -31,16 +23,10 @@ export class AddLibraryPanelDrawer extends SceneObjectBase { const dashboard = getDashboardSceneFor(this); - const layout = dashboard.state.body; - if (!(layout instanceof SceneGridLayout)) { - throw new Error('Trying to add a library panel in a layout that is not SceneGridLayout'); - } + const newPanel = getDefaultVizPanel(dashboard); - const panelId = dashboardSceneGraph.getNextPanelId(dashboard); - - const body = getDefaultVizPanel(dashboard); - body.setState({ + newPanel.setState({ $behaviors: [new LibraryPanelBehavior({ uid: panelInfo.uid, name: panelInfo.name })], }); @@ -53,22 +39,9 @@ export class AddLibraryPanelDrawer extends SceneObjectBase { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel]), }); activateFullSceneTree(scene); @@ -183,22 +172,10 @@ describe('DashboardDatasourceBehaviour', () => { const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries'); - const layout = scene.state.body as SceneGridLayout; + //const layout = scene.state.body as DefaultGridLayoutManager; // we add the new panel, it should run it's query as usual - layout.setState({ - children: [ - ...layout.state.children, - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }); + scene.addPanel(dashboardDSPanel); dashboardDSPanel.activate(); @@ -237,26 +214,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries'); @@ -313,26 +271,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); const sceneDeactivate = activateFullSceneTree(scene); @@ -382,26 +321,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: anotherPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, anotherPanel]), }); const sceneDeactivate = activateFullSceneTree(scene); @@ -457,26 +377,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); try { @@ -526,26 +427,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); const sceneDeactivate = activateFullSceneTree(scene); @@ -605,26 +487,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); activateFullSceneTree(scene); @@ -684,26 +547,7 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); const sceneDeactivate = activateFullSceneTree(scene); diff --git a/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx b/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx index de74e9934a7..cb695ff747d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx @@ -8,6 +8,7 @@ import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-ut import { DashboardGridItem, DashboardGridItemState } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -122,8 +123,8 @@ describe('PanelRepeaterGridItem', () => { $variables: new SceneVariableSet({ variables: [variable], }), - body: new SceneGridLayout({ - children: [panel], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ children: [panel] }), }), }); @@ -193,8 +194,10 @@ describe('PanelRepeaterGridItem', () => { $variables: new SceneVariableSet({ variables: [variable], }), - body: new SceneGridLayout({ - children: [panel, panel2], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [panel, panel2], + }), }), }); @@ -269,7 +272,8 @@ describe('PanelRepeaterGridItem', () => { const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 }); const layoutForceRender = jest.fn(); - (scene.state.body as SceneGridLayout).forceRender = layoutForceRender; + const layout = scene.state.body as DefaultGridLayoutManager; + layout.state.grid.forceRender = layoutForceRender; activateFullSceneTree(scene); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 3df3a8f73e6..6dd0aedbfdf 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -25,13 +25,14 @@ import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { historySrv } from '../settings/version-history/HistorySrv'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; -import { findVizPanelByKey } from '../utils/utils'; +import { findVizPanelByKey, getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils'; import { DashboardControls } from './DashboardControls'; import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { PanelTimeRange } from './PanelTimeRange'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { RowActions } from './row-actions/RowActions'; jest.mock('../settings/version-history/HistorySrv'); @@ -343,9 +344,8 @@ describe('DashboardScene', () => { }); it('A change to any library panel name should set isDirty true', () => { - const libraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state - .body; - const behavior = libraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior; + const panel = findVizPanelByKey(scene, 'panel-5')!; + const behavior = getLibraryPanelBehavior(panel)!; const prevValue = behavior.state.name; behavior.setState({ name: 'new name' }); @@ -353,9 +353,9 @@ describe('DashboardScene', () => { expect(scene.state.isDirty).toBe(true); scene.exitEditMode({ skipConfirm: true }); - const restoredLibraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem) - .state.body; - const restoredBehavior = restoredLibraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior; + + const restoredPanel = findVizPanelByKey(scene, 'panel-5')!; + const restoredBehavior = getLibraryPanelBehavior(restoredPanel)!; expect(restoredBehavior.state.name).toBe(prevValue); }); @@ -451,160 +451,15 @@ describe('DashboardScene', () => { expect(scene.state.isDirty).toBeFalsy(); }); - it('Should throw an error when adding a panel to a layout that is not SceneGridLayout', () => { - const scene = buildTestScene({ body: undefined }); - - expect(() => { - scene.addPanel(new VizPanel({ title: 'Panel Title', key: 'panel-4', pluginId: 'timeseries' })); - }).toThrow('Trying to add a panel in a layout that is not SceneGridLayout'); - }); - - it('Should add a new panel to the dashboard', () => { - const vizPanel = new VizPanel({ - title: 'Panel Title', - key: 'panel-5', - pluginId: 'timeseries', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }); - - scene.addPanel(vizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; - - expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-5'); - expect(gridItem.state.y).toBe(0); - }); - it('Should create and add a new panel to the dashboard', () => { scene.exitEditMode({ skipConfirm: true }); expect(scene.state.isEditing).toBe(false); - scene.onCreateNewPanel(); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; + const panel = scene.onCreateNewPanel(); expect(scene.state.isEditing).toBe(true); - expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-7'); - }); - - it('Should create and add a new row to the dashboard', () => { - scene.onCreateNewRow(); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[0] as SceneGridRow; - - expect(scene.state.isEditing).toBe(true); - expect(body.state.children.length).toBe(4); - expect(gridRow.state.key).toBe('panel-7'); - expect(gridRow.state.children[0].state.key).toBe('griditem-1'); - expect(gridRow.state.children[1].state.key).toBe('griditem-2'); - }); - - it('Should create a row and add all panels in the dashboard under it', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - key: 'griditem-2', - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - ], - }), - }); - - scene.onCreateNewRow(); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[0] as SceneGridRow; - - expect(body.state.children.length).toBe(1); - expect(gridRow.state.children.length).toBe(2); - }); - - it('Should create and add two new rows, but the second has no children', () => { - scene.onCreateNewRow(); - scene.onCreateNewRow(); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[0] as SceneGridRow; - - expect(body.state.children.length).toBe(5); - expect(gridRow.state.children.length).toBe(0); - }); - - it('Should create an empty row when nothing else in dashboard', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [], - }), - }); - - scene.onCreateNewRow(); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[0] as SceneGridRow; - - expect(body.state.children.length).toBe(1); - expect(gridRow.state.children.length).toBe(0); - }); - - it('Should remove a row and move its children to the grid layout', () => { - const body = scene.state.body as SceneGridLayout; - const row = body.state.children[2] as SceneGridRow; - - scene.removeRow(row); - - const vizPanel = (body.state.children[2] as DashboardGridItem).state.body as VizPanel; - - expect(body.state.children.length).toBe(6); - expect(vizPanel.state.key).toBe('panel-4'); - }); - - it('Should remove a row and its children', () => { - const body = scene.state.body as SceneGridLayout; - const row = body.state.children[2] as SceneGridRow; - - scene.removeRow(row, true); - - expect(body.state.children.length).toBe(4); - }); - - it('Should remove an empty row from the layout', () => { - const row = new SceneGridRow({ - key: 'panel-1', - }); - - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [row], - }), - }); - - const body = scene.state.body as SceneGridLayout; - - expect(body.state.children.length).toBe(1); - - scene.removeRow(row); - - expect(body.state.children.length).toBe(0); + expect(scene.state.body.getVizPanels().length).toBe(7); + expect(panel.state.key).toBe('panel-7'); }); it('Should fail to copy a panel if it does not have a grid item parent', () => { @@ -633,16 +488,16 @@ describe('DashboardScene', () => { }); it('Should copy a panel', () => { - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body; + const vizPanel = findVizPanelByKey(scene, 'panel-1')!; scene.copyPanel(vizPanel as VizPanel); expect(store.exists(LS_PANEL_COPY_KEY)).toBe(true); }); it('Should copy a library viz panel', () => { - const libVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body; + const libVizPanel = findVizPanelByKey(scene, 'panel-6')!; - expect(libVizPanel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); + expect(isLibraryPanel(libVizPanel)).toBe(true); scene.copyPanel(libVizPanel); @@ -651,7 +506,6 @@ describe('DashboardScene', () => { it('Should paste a panel', () => { store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' })); - jest.spyOn(JSON, 'parse').mockReturnThis(); jest.mocked(buildGridItemForPanel).mockReturnValue( new DashboardGridItem({ key: 'griditem-9', @@ -665,19 +519,15 @@ describe('DashboardScene', () => { scene.pastePanel(); - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; - expect(buildGridItemForPanel).toHaveBeenCalledTimes(1); - expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-7'); - expect(gridItem.state.y).toBe(0); + + const addedPanel = findVizPanelByKey(scene, 'panel-7')!; + expect(addedPanel).toBeDefined(); expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false); }); it('Should paste a library viz panel', () => { store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' })); - jest.spyOn(JSON, 'parse').mockReturnValue({ libraryPanel: { uid: 'uid', name: 'libraryPanel' } }); jest.mocked(buildGridItemForPanel).mockReturnValue( new DashboardGridItem({ body: new VizPanel({ @@ -691,200 +541,14 @@ describe('DashboardScene', () => { scene.pastePanel(); - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; - - const libVizPanel = gridItem.state.body; - expect(buildGridItemForPanel).toHaveBeenCalledTimes(1); - expect(body.state.children.length).toBe(6); - expect(libVizPanel.state.key).toBe('panel-7'); - expect(gridItem.state.y).toBe(0); + + const addedPanel = findVizPanelByKey(scene, 'panel-7')!; + expect(addedPanel).toBeDefined(); + expect(addedPanel.state.key).toBe('panel-7'); expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false); }); - it('Should remove a panel', () => { - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body; - scene.removePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - expect(body.state.children.length).toBe(4); - }); - - it('Should remove a panel within a row', () => { - const vizPanel = ( - ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state - .children[0] as DashboardGridItem - ).state.body; - scene.removePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[2] as SceneGridRow; - - expect(gridRow.state.children.length).toBe(1); - }); - - it('Should remove a library panel', () => { - const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body; - scene.removePanel(libraryPanel); - - const body = scene.state.body as SceneGridLayout; - expect(body.state.children.length).toBe(4); - }); - - it('Should remove a library panel within a row', () => { - const libraryPanel = ( - ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state - .children[1] as DashboardGridItem - ).state.body; - - scene.removePanel(libraryPanel); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[2] as SceneGridRow; - expect(gridRow.state.children.length).toBe(1); - }); - - it('Should duplicate a panel', () => { - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body; - scene.duplicatePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[5] as DashboardGridItem; - - expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-7'); - }); - - it('Should maintain size of duplicated panel', () => { - const gItem = (scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem; - gItem.setState({ height: 1 }); - const vizPanel = gItem.state.body; - scene.duplicatePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const newGridItem = body.state.children[5] as DashboardGridItem; - - expect(body.state.children.length).toBe(6); - expect(newGridItem.state.body!.state.key).toBe('panel-7'); - expect(newGridItem.state.height).toBe(1); - }); - - it('Should duplicate a library panel', () => { - const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body; - scene.duplicatePanel(libraryPanel); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[5] as DashboardGridItem; - - const libVizPanel = gridItem.state.body; - - expect(body.state.children.length).toBe(6); - expect(libVizPanel.state.key).toBe('panel-7'); - }); - - it('Should deep clone data provider when duplicating a panel', () => { - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body; - scene.duplicatePanel(vizPanel as VizPanel); - - const panelQueries = ( - ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body.state.$data?.state - .$data as SceneQueryRunner - ).state.queries; - const duplicatedPanelQueries = ( - ((scene.state.body as SceneGridLayout).state.children[5] as DashboardGridItem).state.body.state.$data?.state - .$data as SceneQueryRunner - ).state.queries; - - expect(panelQueries[0]).not.toBe(duplicatedPanelQueries[0]); - }); - - it('Should duplicate a repeated panel', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: `grid-item-1`, - width: 24, - height: 8, - repeatedPanels: [ - new VizPanel({ - title: 'Library Panel', - key: 'panel-1', - pluginId: 'table', - }), - ], - body: new VizPanel({ - title: 'Library Panel', - key: 'panel-1', - pluginId: 'table', - }), - variableName: 'custom', - }), - ], - }), - }); - - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state - .repeatedPanels![0]; - - scene.duplicatePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[1] as DashboardGridItem; - - expect(body.state.children.length).toBe(2); - expect(gridItem.state.body!.state.key).toBe('panel-2'); - }); - - it('Should duplicate a panel in a row', () => { - const vizPanel = ( - ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state - .children[0] as DashboardGridItem - ).state.body; - scene.duplicatePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[2] as SceneGridRow; - const gridItem = gridRow.state.children[2] as DashboardGridItem; - - expect(gridRow.state.children.length).toBe(3); - expect(gridItem.state.body!.state.key).toBe('panel-7'); - }); - - it('Should duplicate a library panel in a row', () => { - const libraryPanel = ( - ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state - .children[1] as DashboardGridItem - ).state.body; - - scene.duplicatePanel(libraryPanel); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[2] as SceneGridRow; - const gridItem = gridRow.state.children[2] as DashboardGridItem; - - const libVizPanel = gridItem.state.body; - - expect(gridRow.state.children.length).toBe(3); - expect(libVizPanel.state.key).toBe('panel-7'); - }); - - it('Should fail to duplicate a panel if it does not have a grid item parent', () => { - const vizPanel = new VizPanel({ - title: 'Panel Title', - key: 'panel-5', - pluginId: 'timeseries', - }); - - scene.duplicatePanel(vizPanel); - - const body = scene.state.body as SceneGridLayout; - - // length remains unchanged - expect(body.state.children.length).toBe(5); - }); - it('Should unlink a library panel', () => { const libPanel = new VizPanel({ title: 'Panel B', @@ -893,24 +557,14 @@ describe('DashboardScene', () => { }); const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-2', - body: libPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([libPanel]), }); + expect(isLibraryPanel(libPanel)).toBe(true); + scene.unlinkLibraryPanel(libPanel); - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; - - expect(body.state.children.length).toBe(1); - expect(gridItem.state.body).toBeInstanceOf(VizPanel); - expect(gridItem.state.$behaviors).toBeUndefined(); + expect(isLibraryPanel(libPanel)).toBe(false); }); it('Should create a library panel', () => { @@ -925,10 +579,9 @@ describe('DashboardScene', () => { body: vizPanel, }); + const grid = new SceneGridLayout({ children: [gridItem] }); const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [gridItem], - }), + body: new DefaultGridLayoutManager({ grid }), }); const libPanel = { @@ -938,52 +591,10 @@ describe('DashboardScene', () => { scene.createLibraryPanel(vizPanel, libPanel as LibraryPanel); - const layout = scene.state.body as SceneGridLayout; - const newGridItem = layout.state.children[0] as DashboardGridItem; + const newGridItem = grid.state.children[0] as DashboardGridItem; const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior; - expect(layout.state.children.length).toBe(1); - expect(newGridItem.state.body).toBeInstanceOf(VizPanel); - expect(behavior.state.uid).toBe('uid'); - expect(behavior.state.name).toBe('name'); - }); - - it('Should create a library panel under a row', () => { - const vizPanel = new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - }); - - const gridItem = new DashboardGridItem({ - key: 'griditem-1', - body: vizPanel, - }); - - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new SceneGridRow({ - key: 'row-1', - children: [gridItem], - }), - ], - }), - }); - - const libPanel = { - uid: 'uid', - name: 'name', - }; - - scene.createLibraryPanel(vizPanel, libPanel as LibraryPanel); - - const layout = scene.state.body as SceneGridLayout; - const newGridItem = (layout.state.children[0] as SceneGridRow).state.children[0] as DashboardGridItem; - const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior; - - expect(layout.state.children.length).toBe(1); - expect((layout.state.children[0] as SceneGridRow).state.children.length).toBe(1); + expect(grid.state.children.length).toBe(1); expect(newGridItem.state.body).toBeInstanceOf(VizPanel); expect(behavior.state.uid).toBe('uid'); expect(behavior.state.name).toBe('name'); @@ -1166,6 +777,19 @@ describe('DashboardScene', () => { describe('When coming from explore', () => { // When coming from Explore the first panel in a dashboard is a temporary panel it('should remove first panel from the grid when discarding changes', () => { + const layout = DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + ]); const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', @@ -1176,37 +800,18 @@ describe('DashboardScene', () => { }), controls: new DashboardControls({}), $behaviors: [new behaviors.CursorSync({})], - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - key: 'griditem-2', - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - ], - }), + body: layout, }); scene.onEnterEditMode(true); expect(scene.state.isEditing).toBe(true); - expect((scene.state.body as SceneGridLayout).state.children.length).toBe(2); + expect(layout.state.grid.state.children.length).toBe(2); scene.exitEditMode({ skipConfirm: true }); + + const restoredGrid = scene.state.body as DefaultGridLayoutManager; expect(scene.state.isEditing).toBe(false); - expect((scene.state.body as SceneGridLayout).state.children.length).toBe(1); + expect(restoredGrid.state.grid.state.children.length).toBe(1); }); }); }); @@ -1223,75 +828,74 @@ function buildTestScene(overrides?: Partial) { }), controls: new DashboardControls({}), $behaviors: [new behaviors.CursorSync({}), new behaviors.LiveNowTimer({})], - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $timeRange: new PanelTimeRange({ - from: 'now-12h', - to: 'now', - timeZone: 'browser', - }), - $data: new SceneDataTransformer({ - transformations: [], - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A', target: 'aliasByMetric(carbon.**)' }], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $timeRange: new PanelTimeRange({ + from: 'now-12h', + to: 'now', + timeZone: 'browser', + }), + $data: new SceneDataTransformer({ + transformations: [], + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), }), }), }), - }), - new DashboardGridItem({ - key: 'griditem-2', - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - new SceneGridRow({ - key: 'panel-3', - actions: new RowActions({}), - children: [ - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-4', - pluginId: 'table', - }), + new DashboardGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Library Panel', - pluginId: 'table', - key: 'panel-5', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + }), + new SceneGridRow({ + key: 'panel-3', + actions: new RowActions({}), + children: [ + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel C', + key: 'panel-4', + pluginId: 'table', + }), }), + new DashboardGridItem({ + body: new VizPanel({ + title: 'Library Panel', + pluginId: 'table', + key: 'panel-5', + $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + }), + }), + ], + }), + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2-clone-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), }), - ], - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2-clone-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Library Panel', - pluginId: 'table', - key: 'panel-6', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + new DashboardGridItem({ + body: new VizPanel({ + title: 'Library Panel', + pluginId: 'table', + key: 'panel-6', + $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + }), }), - }), - ], + ], + }), }), ...overrides, }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 6c1bffe482a..72c7025a25f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -12,14 +12,12 @@ import { } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { - SceneFlexLayout, - sceneGraph, - SceneGridLayout, SceneGridRow, SceneObject, SceneObjectBase, SceneObjectRef, SceneObjectState, + SceneTimeRange, sceneUtils, SceneVariable, SceneVariableDependencyConfigLike, @@ -52,15 +50,10 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl, getViewPanelUrl } from '../utils/urlBuilders'; import { - NEW_PANEL_HEIGHT, - NEW_PANEL_WIDTH, - forceRenderChildren, getClosestVizPanel, getDashboardSceneFor, - getDefaultRow, getDefaultVizPanel, getPanelIdForVizPanel, - getQueryRunnerFor, getVizPanelKeyForPanelId, isPanelClone, } from '../utils/utils'; @@ -74,6 +67,8 @@ import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { RowRepeaterBehavior } from './RowRepeaterBehavior'; import { ViewPanelScene } from './ViewPanelScene'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; +import { DashboardLayoutManager } from './types'; export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta', 'preload']; @@ -95,7 +90,7 @@ export interface DashboardSceneState extends SceneObjectState { /** @deprecated */ id?: number | null; /** Layout of panels */ - body: SceneObject; + body: DashboardLayoutManager; /** NavToolbar actions */ actions?: SceneObject[]; /** Fixed row at the top of the canvas with for example variables and time range controls */ @@ -175,7 +170,8 @@ export class DashboardScene extends SceneObjectBase { title: 'Dashboard', meta: {}, editable: true, - body: state.body ?? new SceneFlexLayout({ children: [] }), + $timeRange: state.$timeRange ?? new SceneTimeRange({}), + body: state.body ?? DefaultGridLayoutManager.fromVizPanels(), links: state.links ?? [], ...state, }); @@ -235,7 +231,7 @@ export class DashboardScene extends SceneObjectBase { this.setState({ isEditing: true }); // Propagate change edit mode change to children - this.propagateEditModeChange(); + this.state.body.editModeChanged(true); // Propagate edit mode to scopes this._scopesFacade?.enterReadOnly(); @@ -272,13 +268,6 @@ export class DashboardScene extends SceneObjectBase { this._changeTracker.startTrackingChanges(); } - private propagateEditModeChange() { - if (this.state.body instanceof SceneGridLayout) { - this.state.body.setState({ isDraggable: this.state.isEditing, isResizable: this.state.isEditing }); - forceRenderChildren(this.state.body, true); - } - } - public exitEditMode({ skipConfirm, restoreInitialState }: { skipConfirm: boolean; restoreInitialState?: boolean }) { if (!this.canDiscard()) { console.error('Trying to discard back to a state that does not exist, initialState undefined'); @@ -342,7 +331,7 @@ export class DashboardScene extends SceneObjectBase { } // Disable grid dragging - this.propagateEditModeChange(); + this.state.body.editModeChanged(false); } private cleanupStateFromExplore() { @@ -352,10 +341,8 @@ export class DashboardScene extends SceneObjectBase { this._initialSaveModel.panels = this._initialSaveModel.panels.slice(1); } - if (this._initialState && this._initialState.body instanceof SceneGridLayout) { - this._initialState.body.setState({ - children: this._initialState.body.state.children.slice(1), - }); + if (this._initialState) { + this._initialState.body.cleanUpStateFromExplore?.(); } } @@ -467,79 +454,15 @@ export class DashboardScene extends SceneObjectBase { return this._initialState; } - public addRow(row: SceneGridRow) { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); - } - - const sceneGridLayout = this.state.body; - - // find all panels until the first row and put them into the newly created row. If there are no other rows, - // add all panels to the row. If there are no panels just create an empty row - const indexTillNextRow = sceneGridLayout.state.children.findIndex((child) => child instanceof SceneGridRow); - const rowChildren = sceneGridLayout.state.children - .splice(0, indexTillNextRow === -1 ? sceneGridLayout.state.children.length : indexTillNextRow) - .map((child) => child.clone()); - - if (rowChildren) { - row.setState({ - children: rowChildren, - }); - } - - sceneGridLayout.setState({ - children: [row, ...sceneGridLayout.state.children], - }); - } - - public removeRow(row: SceneGridRow, removePanels = false) { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); - } - - const sceneGridLayout = this.state.body; - - const children = sceneGridLayout.state.children.filter((child) => child.state.key !== row.state.key); - - if (!removePanels) { - const rowChildren = row.state.children.map((child) => child.clone()); - const indexOfRow = sceneGridLayout.state.children.findIndex((child) => child.state.key === row.state.key); - - children.splice(indexOfRow, 0, ...rowChildren); - } - - sceneGridLayout.setState({ children }); - } - public addPanel(vizPanel: VizPanel): void { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + if (!this.state.isEditing) { + this.onEnterEditMode(); } - const sceneGridLayout = this.state.body; - - const panelId = getPanelIdForVizPanel(vizPanel); - const newGridItem = new DashboardGridItem({ - height: NEW_PANEL_HEIGHT, - width: NEW_PANEL_WIDTH, - x: 0, - y: 0, - body: vizPanel, - key: `grid-item-${panelId}`, - }); - - sceneGridLayout.setState({ - children: [newGridItem, ...sceneGridLayout.state.children], - }); + this.state.body.addPanel(vizPanel); } public createLibraryPanel(panelToReplace: VizPanel, libPanel: LibraryPanel) { - const layout = this.state.body; - - if (!(layout instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); - } - const body = panelToReplace.clone({ $behaviors: [new LibraryPanelBehavior({ uid: libPanel.uid, name: libPanel.name })], }); @@ -554,74 +477,7 @@ export class DashboardScene extends SceneObjectBase { } public duplicatePanel(vizPanel: VizPanel) { - if (!vizPanel.parent) { - return; - } - - const gridItem = vizPanel.parent; - - if (!(gridItem instanceof DashboardGridItem)) { - console.error('Trying to duplicate a panel in a layout that is not DashboardGridItem'); - return; - } - - let panelState; - let panelData; - let newGridItem; - const newPanelId = dashboardSceneGraph.getNextPanelId(this); - - if (gridItem instanceof DashboardGridItem) { - panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state); - - let queryRunner = getQueryRunnerFor(gridItem.state.body); - const queries = queryRunner?.state.queries.map((q) => ({ ...q })); - queryRunner = queryRunner?.clone({ queries }); - panelData = sceneGraph.getData(gridItem.state.body).clone({ $data: queryRunner }); - } else { - panelState = sceneUtils.cloneSceneObjectState(vizPanel.state); - - let queryRunner = getQueryRunnerFor(vizPanel); - const queries = queryRunner?.state.queries.map((q) => ({ ...q })); - queryRunner = queryRunner?.clone({ queries }); - panelData = sceneGraph.getData(vizPanel).clone({ $data: queryRunner }); - } - - // when we duplicate a panel we don't want to clone the alert state - delete panelData.state.data?.alertState; - - newGridItem = new DashboardGridItem({ - x: gridItem.state.x, - y: gridItem.state.y, - height: gridItem.state.height, - width: gridItem.state.width, - variableName: gridItem.state.variableName, - repeatDirection: gridItem.state.repeatDirection, - maxPerRow: gridItem.state.maxPerRow, - body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }), - }); - - if (!(this.state.body instanceof SceneGridLayout)) { - console.error('Trying to duplicate a panel in a layout that is not SceneGridLayout '); - return; - } - - const sceneGridLayout = this.state.body; - - if (gridItem.parent instanceof SceneGridRow) { - const row = gridItem.parent; - - row.setState({ - children: [...row.state.children, newGridItem], - }); - - sceneGridLayout.forceRender(); - - return; - } - - sceneGridLayout.setState({ - children: [...sceneGridLayout.state.children, newGridItem], - }); + this.state.body.duplicatePanel(vizPanel); } public copyPanel(vizPanel: VizPanel) { @@ -643,83 +499,23 @@ export class DashboardScene extends SceneObjectBase { } public pastePanel() { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); - } - const jsonData = store.get(LS_PANEL_COPY_KEY); const jsonObj = JSON.parse(jsonData); const panelModel = new PanelModel(jsonObj); const gridItem = buildGridItemForPanel(panelModel); - - const sceneGridLayout = this.state.body; - - if (!(gridItem instanceof DashboardGridItem)) { - throw new Error('Cannot paste invalid grid item'); - } - const panelId = dashboardSceneGraph.getNextPanelId(this); + const panel = gridItem.state.body; - gridItem.state.body.setState({ - key: getVizPanelKeyForPanelId(panelId), - }); + panel.setState({ key: getVizPanelKeyForPanelId(panelId) }); + panel.clearParent(); - gridItem.setState({ - height: NEW_PANEL_HEIGHT, - width: NEW_PANEL_WIDTH, - x: 0, - y: 0, - key: `grid-item-${panelId}`, - }); - - sceneGridLayout.setState({ - children: [gridItem, ...sceneGridLayout.state.children], - }); + this.state.body.addPanel(panel); store.delete(LS_PANEL_COPY_KEY); } public removePanel(panel: VizPanel) { - const panels: SceneObject[] = []; - const key = panel.parent?.state.key; - - if (!key) { - return; - } - - let row: SceneGridRow | undefined; - - try { - row = sceneGraph.getAncestor(panel, SceneGridRow); - } catch { - row = undefined; - } - - if (row) { - row.state.children.forEach((child: SceneObject) => { - if (child.state.key !== key) { - panels.push(child); - } - }); - - row.setState({ children: panels }); - - this.state.body.forceRender(); - - return; - } - - this.state.body.forEachChild((child: SceneObject) => { - if (child.state.key !== key) { - panels.push(child); - } - }); - - const layout = this.state.body; - - if (layout instanceof SceneGridLayout || layout instanceof SceneFlexLayout) { - layout.setState({ children: panels }); - } + this.state.body.removePanel(panel); } public unlinkLibraryPanel(panel: VizPanel) { @@ -775,18 +571,10 @@ export class DashboardScene extends SceneObjectBase { } public onCreateNewRow() { - const row = getDefaultRow(this); - - this.addRow(row); - - return getPanelIdForVizPanel(row); + this.state.body.addNewRow(); } public onCreateNewPanel(): VizPanel { - if (!this.state.isEditing) { - this.onEnterEditMode(); - } - const vizPanel = getDefaultVizPanel(this); this.addPanel(vizPanel); @@ -852,40 +640,6 @@ export class DashboardScene extends SceneObjectBase { locationService.replace('/'); } - public collapseAllRows() { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Dashboard scene layout is not SceneGridLayout'); - } - - const sceneGridLayout = this.state.body; - - sceneGridLayout.state.children.forEach((child) => { - if (!(child instanceof SceneGridRow)) { - return; - } - if (!child.state.isCollapsed) { - sceneGridLayout.toggleRow(child); - } - }); - } - - public expandAllRows() { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Dashboard scene layout is not SceneGridLayout'); - } - - const sceneGridLayout = this.state.body; - - sceneGridLayout.state.children.forEach((child) => { - if (!(child instanceof SceneGridRow)) { - return; - } - if (child.state.isCollapsed) { - sceneGridLayout.toggleRow(child); - } - }); - } - public onSetScrollRef = (scrollElement: ScrollRefElement): void => { this._scrollRef = scrollElement; }; @@ -925,11 +679,11 @@ export class DashboardVariableDependency implements SceneVariableDependencyConfi * The first repeated row has the row repeater behavior but it also has a local SceneVariableSet with a local variable value */ const layout = this._dashboard.state.body; - if (!(layout instanceof SceneGridLayout)) { + if (!(layout instanceof DefaultGridLayoutManager)) { return; } - for (const child of layout.state.children) { + for (const child of layout.state.grid.state.children) { if (!(child instanceof SceneGridRow) || !child.state.$behaviors) { continue; } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts index c1a7172044f..e676a90930e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts @@ -1,10 +1,11 @@ import { AppEvents } from '@grafana/data'; -import { SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { KioskMode } from 'app/types'; import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { DashboardRepeatsProcessedEvent } from './types'; describe('DashboardSceneUrlSync', () => { @@ -30,14 +31,17 @@ describe('DashboardSceneUrlSync', () => { it('Should set UNSAFE_fitPanels when url has autofitpanels', () => { const scene = buildTestScene(); scene.urlSync?.updateFromUrl({ autofitpanels: '' }); - expect((scene.state.body as SceneGridLayout).state.UNSAFE_fitPanels).toBe(true); + const layout = scene.state.body as DefaultGridLayoutManager; + + expect(layout.state.grid.state.UNSAFE_fitPanels).toBe(true); }); it('Should get the autofitpanels from the scene state', () => { const scene = buildTestScene(); expect(scene.urlSync?.getUrlState().autofitpanels).toBeUndefined(); - (scene.state.body as SceneGridLayout).setState({ UNSAFE_fitPanels: true }); + const layout = scene.state.body as DefaultGridLayoutManager; + layout.state.grid.setState({ UNSAFE_fitPanels: true }); expect(scene.urlSync?.getUrlState().autofitpanels).toBe('true'); }); @@ -89,8 +93,9 @@ describe('DashboardSceneUrlSync', () => { expect(errorNotice).toBe(0); // fake adding clone panel - const layout = scene.state.body as SceneGridLayout; - layout.setState({ + const layout = scene.state.body as DefaultGridLayoutManager; + + layout.state.grid.setState({ children: [ new DashboardGridItem({ key: 'griditem-1', @@ -114,27 +119,20 @@ function buildTestScene() { const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + + new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + ]), }); return scene; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 21335a1904d..2892c555d07 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -2,7 +2,7 @@ import { Unsubscribable } from 'rxjs'; import { AppEvents } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; -import { SceneGridLayout, SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes'; +import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { KioskMode } from 'app/types'; @@ -16,6 +16,7 @@ import { findVizPanelByKey, getLibraryPanelBehavior, isPanelClone } from '../uti import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { ViewPanelScene } from './ViewPanelScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { DashboardRepeatsProcessedEvent } from './types'; export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { @@ -29,9 +30,10 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { getUrlState(): SceneObjectUrlValues { const state = this._scene.state; + return { inspect: state.inspectPanelKey, - autofitpanels: state.body instanceof SceneGridLayout && !!state.body.state.UNSAFE_fitPanels ? 'true' : undefined, + autofitpanels: this.getAutoFitPanels(), viewPanel: state.viewPanelScene?.getUrlKey(), editview: state.editview?.getUrlKey(), editPanel: state.editPanel?.getUrlKey() || undefined, @@ -40,6 +42,14 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { }; } + private getAutoFitPanels(): string | undefined { + if (this._scene.state.body instanceof DefaultGridLayoutManager) { + return this._scene.state.body.state.grid.state.UNSAFE_fitPanels ? 'true' : undefined; + } + + return undefined; + } + updateFromUrl(values: SceneObjectUrlValues): void { const { inspectPanelKey, viewPanelScene, isEditing, editPanel, shareView } = this._scene.state; const update: Partial = {}; @@ -142,11 +152,12 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { update.shareView = undefined; } - if (this._scene.state.body instanceof SceneGridLayout) { + const layout = this._scene.state.body; + if (layout instanceof DefaultGridLayoutManager) { const UNSAFE_fitPanels = typeof values.autofitpanels === 'string'; - if (!!this._scene.state.body.state.UNSAFE_fitPanels !== UNSAFE_fitPanels) { - this._scene.state.body.setState({ UNSAFE_fitPanels }); + if (!!layout.state.grid.state.UNSAFE_fitPanels !== UNSAFE_fitPanels) { + layout.state.grid.setState({ UNSAFE_fitPanels }); } } diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx index 7a558d8c5f5..78378863e89 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx @@ -13,6 +13,7 @@ import { activateFullSceneTree } from '../utils/test-utils'; import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; setPluginImportUtils({ importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), @@ -170,8 +171,10 @@ async function buildTestSceneWithLibraryPanel() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [gridItem], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [gridItem], + }), }), }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index 39048ed5724..468bcc5adf9 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -5,15 +5,15 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { selectors } from '@grafana/e2e-selectors'; import { LocationServiceProvider, config, locationService } from '@grafana/runtime'; -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes'; +import { SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { DashboardMeta } from 'app/types'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; -import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { ToolbarActions } from './NavToolbarActions'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; jest.mock('app/features/playlist/PlaylistSrv', () => ({ playlistSrv: { @@ -120,9 +120,8 @@ describe('NavToolbarActions', () => { await act(() => { dashboard.onEnterEditMode(); - const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state - .body as VizPanel; - dashboard.setState({ editPanel: buildPanelEditScene(editingPanel, true) }); + const panel = dashboard.state.body.getVizPanels()[0]; + dashboard.setState({ editPanel: buildPanelEditScene(panel, true) }); }); expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); @@ -135,9 +134,8 @@ describe('NavToolbarActions', () => { await act(() => { dashboard.onEnterEditMode(); - const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state - .body as VizPanel; - dashboard.setState({ editPanel: buildPanelEditScene(editingPanel) }); + const panel = dashboard.state.body.getVizPanels()[0]; + dashboard.setState({ editPanel: buildPanelEditScene(panel) }); }); expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); @@ -207,27 +205,19 @@ function setup(meta?: DashboardMeta) { }, title: 'hello', uid: 'dash-1', - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + ]), }); const context = getGrafanaContextMock(); diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 5863a58fe47..5b0fa84910a 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -11,7 +11,6 @@ import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPluginLinkExtensions, locationService } from '@grafana/runtime'; import { LocalValueVariable, - SceneGridLayout, SceneQueryRunner, SceneTimeRange, SceneVariableSet, @@ -23,10 +22,10 @@ import { GetExploreUrlArguments } from 'app/core/utils/explore'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; -import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; import { panelMenuBehavior } from './PanelMenuBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; const mocks = { contextSrv: jest.mocked(contextSrv), @@ -588,18 +587,7 @@ async function buildTestScene(options: SceneOptions) { canEdit: true, isEmbedded: options.isEmbedded ?? false, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); await new Promise((r) => setTimeout(r, 1)); diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx index f9cd350a9a7..a5b3c0a0b81 100644 --- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx @@ -21,6 +21,7 @@ import { DashboardGridItem, RepeatDirection } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { panelMenuBehavior, repeatPanelMenuBehavior } from './PanelMenuBehavior'; import { RowRepeaterBehavior } from './RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { RowActions } from './row-actions/RowActions'; jest.mock('@grafana/runtime', () => ({ @@ -350,7 +351,7 @@ function buildScene( }), ], }), - body: grid, + body: new DefaultGridLayoutManager({ grid }), }); const rowToRepeat = repeatBehavior.parent as SceneGridRow; diff --git a/public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx b/public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx index 5919e5c432c..47f6a3307d0 100644 --- a/public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx @@ -3,6 +3,7 @@ import { LocalValueVariable, SceneGridLayout, SceneGridRow, SceneVariableSet, Vi import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { ViewPanelScene } from './ViewPanelScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -32,23 +33,25 @@ function buildScene(options?: SceneOptions) { }); const dashboard = new DashboardScene({ - body: new SceneGridLayout({ - children: [ - new SceneGridRow({ - x: 0, - y: 10, - width: 24, - $variables: new SceneVariableSet({ - variables: [new LocalValueVariable({ value: 'row-var-value' })], - }), - height: 1, - children: [ - new DashboardGridItem({ - body: panel, + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new SceneGridRow({ + x: 0, + y: 10, + width: 24, + $variables: new SceneVariableSet({ + variables: [new LocalValueVariable({ value: 'row-var-value' })], }), - ], - }), - ], + height: 1, + children: [ + new DashboardGridItem({ + body: panel, + }), + ], + }), + ], + }), }), }); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index 4e025631e01..10fc2feac74 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -14,6 +14,7 @@ import { getPanelIdForVizPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; import { onRemovePanel, toggleVizPanelLegend } from './PanelMenuBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; export function setupKeyboardShortcuts(scene: DashboardScene) { const keybindings = new KeybindingSet(); @@ -214,7 +215,9 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { keybindings.addBinding({ key: 'd shift+c', onTrigger: () => { - scene.collapseAllRows(); + if (scene.state.body instanceof DefaultGridLayoutManager) { + scene.state.body.collapseAllRows(); + } }, }); @@ -222,7 +225,9 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { keybindings.addBinding({ key: 'd shift+e', onTrigger: () => { - scene.expandAllRows(); + if (scene.state.body instanceof DefaultGridLayoutManager) { + scene.state.body.expandAllRows(); + } }, }); } diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx new file mode 100644 index 00000000000..37f25cc54d0 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx @@ -0,0 +1,281 @@ +import { SceneGridItemLike, SceneGridLayout, SceneGridRow, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { findVizPanelByKey } from '../../utils/utils'; +import { DashboardGridItem } from '../DashboardGridItem'; + +import { DefaultGridLayoutManager } from './DefaultGridLayoutManager'; + +describe('DefaultGridLayoutManager', () => { + describe('getVizPanels', () => { + it('Should return all panels', () => { + const { manager } = setup(); + const vizPanels = manager.getVizPanels(); + + expect(vizPanels.length).toBe(4); + expect(vizPanels[0].state.title).toBe('Panel A'); + expect(vizPanels[1].state.title).toBe('Panel B'); + expect(vizPanels[2].state.title).toBe('Panel C'); + expect(vizPanels[3].state.title).toBe('Panel D'); + }); + + it('Should return an empty array when scene has no panels', () => { + const { manager } = setup({ gridItems: [] }); + const vizPanels = manager.getVizPanels(); + expect(vizPanels.length).toBe(0); + }); + }); + + describe('getNextPanelId', () => { + it('should get next panel id in a simple 3 panel layout', () => { + const { manager } = setup(); + const id = manager.getNextPanelId(); + + expect(id).toBe(4); + }); + + it('should return 1 if no panels are found', () => { + const { manager } = setup({ gridItems: [] }); + const id = manager.getNextPanelId(); + + expect(id).toBe(1); + }); + }); + + describe('addPanel', () => { + it('Should add a new panel', () => { + const { manager } = setup(); + + const vizPanel = new VizPanel({ + title: 'Panel Title', + key: 'panel-55', + pluginId: 'timeseries', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }); + + manager.addPanel(vizPanel); + + const panel = findVizPanelByKey(manager, 'panel-55')!; + const gridItem = panel.parent as DashboardGridItem; + + expect(panel).toBeDefined(); + expect(gridItem.state.y).toBe(0); + }); + }); + + describe('addNewRow', () => { + it('Should create and add a new row to the dashboard', () => { + const { manager, grid } = setup(); + const row = manager.addNewRow(); + + expect(grid.state.children.length).toBe(2); + expect(row.state.key).toBe('panel-4'); + expect(row.state.children[0].state.key).toBe('griditem-1'); + expect(row.state.children[1].state.key).toBe('griditem-2'); + }); + + it('Should create a row and add all panels in the dashboard under it', () => { + const { manager, grid } = setup({ + gridItems: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new DashboardGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + ], + }); + + const row = manager.addNewRow(); + + expect(grid.state.children.length).toBe(1); + expect(row.state.children.length).toBe(2); + }); + + it('Should create and add two new rows, but the second has no children', () => { + const { manager, grid } = setup(); + const row1 = manager.addNewRow(); + const row2 = manager.addNewRow(); + + expect(grid.state.children.length).toBe(3); + expect(row1.state.children.length).toBe(2); + expect(row2.state.children.length).toBe(0); + }); + + it('Should create an empty row when nothing else in dashboard', () => { + const { manager, grid } = setup({ gridItems: [] }); + const row = manager.addNewRow(); + + expect(grid.state.children.length).toBe(1); + expect(row.state.children.length).toBe(0); + }); + }); + + describe('Remove row', () => { + it('Should remove a row and move its children to the grid layout', () => { + const { manager, grid } = setup(); + const row = grid.state.children[2] as SceneGridRow; + + manager.removeRow(row); + + expect(grid.state.children.length).toBe(4); + }); + + it('Should remove a row and its children', () => { + const { manager, grid } = setup(); + const row = grid.state.children[2] as SceneGridRow; + + manager.removeRow(row, true); + + expect(grid.state.children.length).toBe(2); + }); + + it('Should remove an empty row from the layout', () => { + const row = new SceneGridRow({ key: 'panel-1' }); + const { manager, grid } = setup({ gridItems: [row] }); + + manager.removeRow(row); + + expect(grid.state.children.length).toBe(0); + }); + }); + + describe('removePanel', () => { + it('Should remove grid item', () => { + const { manager } = setup(); + const panel = findVizPanelByKey(manager, 'panel-1')!; + manager.removePanel(panel); + + expect(findVizPanelByKey(manager, 'panel-1')).toBeNull(); + }); + + it('Should remove a grid item within a row', () => { + const { manager, grid } = setup(); + const vizPanel = findVizPanelByKey(manager, 'panel-within-row1')!; + + manager.removePanel(vizPanel); + + const gridRow = grid.state.children[2] as SceneGridRow; + expect(gridRow.state.children.length).toBe(1); + }); + }); + + describe('duplicatePanel', () => { + it('Should duplicate a panel', () => { + const { manager, grid } = setup(); + const vizPanel = findVizPanelByKey(manager, 'panel-1')!; + + expect(grid.state.children.length).toBe(3); + + manager.duplicatePanel(vizPanel); + + const newGridItem = grid.state.children[3]; + + expect(grid.state.children.length).toBe(4); + expect(newGridItem.state.key).toBe('grid-item-4'); + }); + + it('Should maintain size of duplicated panel', () => { + const { manager, grid } = setup(); + + const gItem = grid.state.children[0] as DashboardGridItem; + gItem.setState({ height: 1 }); + + const vizPanel = gItem.state.body; + manager.duplicatePanel(vizPanel); + + const newGridItem = grid.state.children[grid.state.children.length - 1] as DashboardGridItem; + + expect(newGridItem.state.height).toBe(1); + }); + + it('Should duplicate a repeated panel', () => { + const { manager, grid } = setup(); + const gItem = grid.state.children[0] as DashboardGridItem; + gItem.setState({ variableName: 'server', repeatDirection: 'v', maxPerRow: 100 }); + const vizPanel = gItem.state.body; + manager.duplicatePanel(vizPanel as VizPanel); + + const newGridItem = grid.state.children[grid.state.children.length - 1] as DashboardGridItem; + + expect(newGridItem.state.variableName).toBe('server'); + expect(newGridItem.state.repeatDirection).toBe('v'); + expect(newGridItem.state.maxPerRow).toBe(100); + }); + + it('Should duplicate a panel in a row', () => { + const { manager } = setup(); + const vizPanel = findVizPanelByKey(manager, 'panel-within-row1')!; + const gridRow = vizPanel.parent?.parent as SceneGridRow; + + expect(gridRow.state.children.length).toBe(2); + + manager.duplicatePanel(vizPanel); + + expect(gridRow.state.children.length).toBe(3); + }); + }); +}); + +interface TestOptions { + gridItems: SceneGridItemLike[]; +} + +function setup(options?: TestOptions) { + const gridItems = options?.gridItems ?? [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new DashboardGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + new SceneGridRow({ + key: 'panel-3', + title: 'row', + children: [ + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel C', + key: 'panel-within-row1', + pluginId: 'table', + }), + }), + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel D', + key: 'panel-within-row2', + pluginId: 'table', + }), + }), + ], + }), + ]; + + const grid = new SceneGridLayout({ children: gridItems }); + const manager = new DefaultGridLayoutManager({ grid: grid }); + + return { manager, grid }; +} diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx new file mode 100644 index 00000000000..6728a02e669 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -0,0 +1,355 @@ +import { + SceneObjectState, + SceneGridLayout, + SceneObjectBase, + SceneGridRow, + VizPanel, + sceneGraph, + sceneUtils, + SceneComponentProps, +} from '@grafana/scenes'; +import { GRID_COLUMN_COUNT } from 'app/core/constants'; + +import { + forceRenderChildren, + getPanelIdForVizPanel, + NEW_PANEL_HEIGHT, + NEW_PANEL_WIDTH, + getVizPanelKeyForPanelId, +} from '../../utils/utils'; +import { DashboardGridItem } from '../DashboardGridItem'; +import { RowRepeaterBehavior } from '../RowRepeaterBehavior'; +import { RowActions } from '../row-actions/RowActions'; +import { DashboardLayoutManager } from '../types'; + +interface DefaultGridLayoutManagerState extends SceneObjectState { + grid: SceneGridLayout; +} + +/** + * State manager for the default grid layout + */ +export class DefaultGridLayoutManager + extends SceneObjectBase + implements DashboardLayoutManager +{ + public editModeChanged(isEditing: boolean): void { + this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing }); + forceRenderChildren(this.state.grid, true); + } + + /** + * Removes the first panel + */ + public cleanUpStateFromExplore(): void { + this.state.grid.setState({ + children: this.state.grid.state.children.slice(1), + }); + } + + public addPanel(vizPanel: VizPanel): void { + const panelId = getPanelIdForVizPanel(vizPanel); + const newGridItem = new DashboardGridItem({ + height: NEW_PANEL_HEIGHT, + width: NEW_PANEL_WIDTH, + x: 0, + y: 0, + body: vizPanel, + key: `grid-item-${panelId}`, + }); + + this.state.grid.setState({ + children: [newGridItem, ...this.state.grid.state.children], + }); + } + + /** + * Adds a new emtpy row + */ + public addNewRow(): SceneGridRow { + const id = this.getNextPanelId(); + const row = new SceneGridRow({ + key: getVizPanelKeyForPanelId(id), + title: 'Row title', + actions: new RowActions({}), + y: 0, + }); + + const sceneGridLayout = this.state.grid; + + // find all panels until the first row and put them into the newly created row. If there are no other rows, + // add all panels to the row. If there are no panels just create an empty row + const indexTillNextRow = sceneGridLayout.state.children.findIndex((child) => child instanceof SceneGridRow); + const rowChildren = sceneGridLayout.state.children + .splice(0, indexTillNextRow === -1 ? sceneGridLayout.state.children.length : indexTillNextRow) + .map((child) => child.clone()); + + if (rowChildren) { + row.setState({ children: rowChildren }); + } + + sceneGridLayout.setState({ children: [row, ...sceneGridLayout.state.children] }); + + return row; + } + + /** + * Removes a row + * @param row + * @param removePanels + */ + public removeRow(row: SceneGridRow, removePanels = false) { + const sceneGridLayout = this.state.grid; + + const children = sceneGridLayout.state.children.filter((child) => child.state.key !== row.state.key); + + if (!removePanels) { + const rowChildren = row.state.children.map((child) => child.clone()); + const indexOfRow = sceneGridLayout.state.children.findIndex((child) => child.state.key === row.state.key); + + children.splice(indexOfRow, 0, ...rowChildren); + } + + sceneGridLayout.setState({ children }); + } + + /** + * Removes a panel + */ + public removePanel(panel: VizPanel) { + const gridItem = panel.parent!; + + if (!(gridItem instanceof DashboardGridItem)) { + throw new Error('Trying to remove panel that is not inside a DashboardGridItem'); + } + + const layout = this.state.grid; + + let row: SceneGridRow | undefined; + + try { + row = sceneGraph.getAncestor(gridItem, SceneGridRow); + } catch { + row = undefined; + } + + if (row) { + row.setState({ children: row.state.children.filter((child) => child !== gridItem) }); + layout.forceRender(); + return; + } + + this.state.grid.setState({ + children: layout.state.children.filter((child) => child !== gridItem), + }); + } + + public duplicatePanel(vizPanel: VizPanel): void { + const gridItem = vizPanel.parent; + if (!(gridItem instanceof DashboardGridItem)) { + console.error('Trying to duplicate a panel that is not inside a DashboardGridItem'); + return; + } + + let panelState; + let panelData; + let newGridItem; + + const newPanelId = this.getNextPanelId(); + const grid = this.state.grid; + + if (gridItem instanceof DashboardGridItem) { + panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state); + panelData = sceneGraph.getData(gridItem.state.body).clone(); + } else { + panelState = sceneUtils.cloneSceneObjectState(vizPanel.state); + panelData = sceneGraph.getData(vizPanel).clone(); + } + + // when we duplicate a panel we don't want to clone the alert state + delete panelData.state.data?.alertState; + + newGridItem = new DashboardGridItem({ + x: gridItem.state.x, + y: gridItem.state.y, + height: gridItem.state.height, + width: gridItem.state.width, + variableName: gridItem.state.variableName, + repeatDirection: gridItem.state.repeatDirection, + maxPerRow: gridItem.state.maxPerRow, + key: `grid-item-${newPanelId}`, + body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }), + }); + + if (gridItem.parent instanceof SceneGridRow) { + const row = gridItem.parent; + + row.setState({ children: [...row.state.children, newGridItem] }); + + grid.forceRender(); + return; + } + + grid.setState({ children: [...grid.state.children, newGridItem] }); + } + + public getVizPanels(): VizPanel[] { + const panels: VizPanel[] = []; + + this.state.grid.forEachChild((child) => { + if (!(child instanceof DashboardGridItem) && !(child instanceof SceneGridRow)) { + throw new Error('Child is not a DashboardGridItem or SceneGridRow, invalid scene'); + } + + if (child instanceof DashboardGridItem) { + if (child.state.body instanceof VizPanel) { + panels.push(child.state.body); + } + } else if (child instanceof SceneGridRow) { + child.forEachChild((child) => { + if (child instanceof DashboardGridItem) { + if (child.state.body instanceof VizPanel) { + panels.push(child.state.body); + } + } + }); + } + }); + + return panels; + } + + public getNextPanelId(): number { + let max = 0; + + for (const child of this.state.grid.state.children) { + if (child instanceof DashboardGridItem) { + const vizPanel = child.state.body; + + if (vizPanel) { + const panelId = getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + + if (child instanceof SceneGridRow) { + //rows follow the same key pattern --- e.g.: `panel-6` + const panelId = getPanelIdForVizPanel(child); + + if (panelId > max) { + max = panelId; + } + + for (const rowChild of child.state.children) { + if (rowChild instanceof DashboardGridItem) { + const vizPanel = rowChild.state.body; + + if (vizPanel) { + const panelId = getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + } + } + } + + return max + 1; + } + + public collapseAllRows() { + this.state.grid.state.children.forEach((child) => { + if (!(child instanceof SceneGridRow)) { + return; + } + if (!child.state.isCollapsed) { + this.state.grid.toggleRow(child); + } + }); + } + + public expandAllRows() { + this.state.grid.state.children.forEach((child) => { + if (!(child instanceof SceneGridRow)) { + return; + } + if (child.state.isCollapsed) { + this.state.grid.toggleRow(child); + } + }); + } + + activateRepeaters(): void { + this.state.grid.forEachChild((child) => { + if (child instanceof DashboardGridItem && !child.isActive) { + child.activate(); + return; + } + + if (child instanceof SceneGridRow && child.state.$behaviors) { + for (const behavior of child.state.$behaviors) { + if (behavior instanceof RowRepeaterBehavior && !child.isActive) { + child.activate(); + break; + } + } + + child.state.children.forEach((child) => { + if (child instanceof DashboardGridItem && !child.isActive) { + child.activate(); + return; + } + }); + } + }); + } + + /** + * For simple test grids + * @param panels + */ + public static fromVizPanels(panels: VizPanel[] = []): DefaultGridLayoutManager { + const children: DashboardGridItem[] = []; + const panelHeight = 10; + const panelWidth = GRID_COLUMN_COUNT / 3; + let currentY = 0; + let currentX = 0; + + for (let panel of panels) { + children.push( + new DashboardGridItem({ + key: `griditem-${getPanelIdForVizPanel(panel)}`, + x: currentX, + y: currentY, + width: panelWidth, + height: panelHeight, + body: panel, + }) + ); + + currentX += panelWidth; + + if (currentX + panelWidth >= GRID_COLUMN_COUNT) { + currentX = 0; + currentY += panelHeight; + } + } + + return new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: children, + isDraggable: false, + isResizable: false, + }), + }); + } + + public static Component = ({ model }: SceneComponentProps) => { + return ; + }; +} diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx index 9dadfd0cbad..a1cc833602b 100644 --- a/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx +++ b/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx @@ -1,7 +1,14 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { SceneComponentProps, SceneGridRow, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { + SceneComponentProps, + sceneGraph, + SceneGridRow, + SceneObjectBase, + SceneObjectState, + VizPanel, +} from '@grafana/scenes'; import { Icon, TextLink, useStyles2 } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; @@ -11,6 +18,7 @@ import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils'; import { DashboardGridItem } from '../DashboardGridItem'; import { DashboardScene } from '../DashboardScene'; import { RowRepeaterBehavior } from '../RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; import { RowOptionsButton } from './RowOptionsButton'; @@ -29,6 +37,11 @@ export class RowActions extends SceneObjectBase { return getDashboardSceneFor(this); } + public removeRow(removePanels?: boolean) { + const manager = sceneGraph.getAncestor(this, DefaultGridLayoutManager); + manager.removeRow(this.getParent(), removePanels); + } + public onUpdate = (title: string, repeat?: string | null): void => { const row = this.getParent(); let repeatBehavior: RowRepeaterBehavior | undefined; @@ -60,12 +73,8 @@ export class RowActions extends SceneObjectBase { text: 'Are you sure you want to remove this row and all its panels?', altActionText: 'Delete row only', icon: 'trash-alt', - onConfirm: () => { - this.getDashboard().removeRow(this.getParent(), true); - }, - onAltAction: () => { - this.getDashboard().removeRow(this.getParent()); - }, + onConfirm: () => this.removeRow(true), + onAltAction: () => this.removeRow(), }) ); }; diff --git a/public/app/features/dashboard-scene/scene/types.ts b/public/app/features/dashboard-scene/scene/types.ts index 9bf602cc171..37a932ceb20 100644 --- a/public/app/features/dashboard-scene/scene/types.ts +++ b/public/app/features/dashboard-scene/scene/types.ts @@ -1,5 +1,89 @@ -import { BusEventWithPayload } from '@grafana/data'; -import { SceneObject } from '@grafana/scenes'; +import { BusEventWithPayload, RegistryItem } from '@grafana/data'; +import { SceneObject, VizPanel } from '@grafana/scenes'; + +/** + * A scene object that usually wraps an underlying layout + * Dealing with all the state management and editing of the layout + */ +export interface DashboardLayoutManager extends SceneObject { + /** + * Notify the layout manager that the edit mode has changed + * @param isEditing + */ + editModeChanged(isEditing: boolean): void; + /** + * We should be able to figure out how to add the explore panel in a way that leaves the + * initialSaveModel clean from it so we can leverage the default discard changes logic. + * Then we can get rid of this. + */ + cleanUpStateFromExplore?(): void; + /** + * Not sure we will need this in the long run, we should be able to handle this inside internally + */ + getNextPanelId(): number; + /** + * Remove an elemenet / panel + * @param element + */ + removePanel(panel: VizPanel): void; + /** + * Creates a copy of an existing element and adds it to the layout + * @param element + */ + duplicatePanel(panel: VizPanel): void; + /** + * Adds a new panel to the layout + */ + addPanel(panel: VizPanel): void; + /** + * Add row + */ + addNewRow(): void; + /** + * getVizPanels + */ + getVizPanels(): VizPanel[]; + /** + * Turn into a save model + * @param saveModel + */ + toSaveModel?(): any; + /** + * For dynamic panels that need to be viewed in isolation (SoloRoute) + */ + activateRepeaters?(): void; +} + +/** + * The layout descriptor used when selecting / switching layouts + */ +export interface LayoutRegistryItem extends RegistryItem { + /** + * When switching between layouts + * @param currentLayout + */ + createFromLayout(currentLayout: DashboardLayoutManager): DashboardLayoutManager; + /** + * Create from persisted state + * @param saveModel + */ + createFromSaveModel?(saveModel: any): void; +} + +export interface LayoutEditorProps { + layoutManager: T; +} + +/** + * This interface is needed to support layouts existing on different levels of the scene (DashboardScene and inside the TabsLayoutManager) + */ +export interface LayoutParent extends SceneObject { + switchLayout(newLayout: DashboardLayoutManager): void; +} + +export function isLayoutParent(obj: SceneObject): obj is LayoutParent { + return 'switchLayout' in obj; +} export interface DashboardRepeatsProcessedEventPayload { source: SceneObject; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 868dfd24768..37badeff929 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -31,6 +31,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { NEW_LINK } from '../settings/links/utils'; import { getQueryRunnerFor } from '../utils/utils'; @@ -323,7 +324,8 @@ describe('transformSaveModelToScene', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard); - const body = scene.state.body as SceneGridLayout; + const layout = scene.state.body as DefaultGridLayoutManager; + const body = layout.state.grid; expect(body.state.children).toHaveLength(3); const rowScene1 = body.state.children[0] as SceneGridRow; @@ -375,7 +377,8 @@ describe('transformSaveModelToScene', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard); - const body = scene.state.body as SceneGridLayout; + const layout = scene.state.body as DefaultGridLayoutManager; + const body = layout.state.grid; expect(body.state.children).toHaveLength(1); const rowScene = body.state.children[0] as SceneGridRow; @@ -471,7 +474,8 @@ describe('transformSaveModelToScene', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard); - const body = scene.state.body as SceneGridLayout; + const layout = scene.state.body as DefaultGridLayoutManager; + const body = layout.state.grid; expect(body.state.children).toHaveLength(4); expect(body).toBeInstanceOf(SceneGridLayout); @@ -750,7 +754,9 @@ describe('transformSaveModelToScene', () => { dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO, meta: {}, }); - const body = scene.state.body as SceneGridLayout; + + const layout = scene.state.body as DefaultGridLayoutManager; + const body = layout.state.grid; const row2 = body.state.children[1] as SceneGridRow; expect(row2.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 0195625f24b..b5237a83683 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -37,6 +37,7 @@ import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavio import { PanelNotices } from '../scene/PanelNotices'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowActions } from '../scene/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; @@ -221,10 +222,12 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, title: oldModel.title, uid: oldModel.uid, version: oldModel.version, - body: new SceneGridLayout({ - isLazy: dto.preload ? false : true, - children: createSceneObjectsForPanels(oldModel.panels), - $behaviors: [trackIfEmpty], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + isLazy: dto.preload ? false : true, + children: createSceneObjectsForPanels(oldModel.panels), + $behaviors: [trackIfEmpty], + }), }), $timeRange: new SceneTimeRange({ from: oldModel.time.from, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index eef3e336c16..bbc6d3dbb7a 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -15,7 +15,7 @@ import { } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime'; -import { MultiValueVariable, sceneGraph, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes'; +import { MultiValueVariable, sceneGraph, SceneGridRow, VizPanel } from '@grafana/scenes'; import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema'; import { PanelModel } from 'app/features/dashboard/state'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; @@ -27,6 +27,7 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { NEW_LINK } from '../settings/links/utils'; import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils'; import { getVizPanelKeyForPanelId } from '../utils/utils'; @@ -225,7 +226,8 @@ describe('transformSceneToSaveModel', () => { const variable = scene.state.$variables?.state.variables[0] as MultiValueVariable; variable.changeValueTo(['a', 'b', 'c']); - const grid = scene.state.body as SceneGridLayout; + const layout = scene.state.body as DefaultGridLayoutManager; + const grid = layout.state.grid; const rowWithRepeat = grid.state.children[1] as SceneGridRow; const rowRepeater = rowWithRepeat.state.$behaviors![0] as RowRepeaterBehavior; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 01605ed28ab..86aebd5ee65 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -4,7 +4,6 @@ import { isEmptyObject, ScopedVars, TimeRange } from '@grafana/data'; import { behaviors, SceneGridItemLike, - SceneGridLayout, SceneGridRow, VizPanel, SceneDataTransformer, @@ -36,6 +35,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils'; @@ -53,8 +53,8 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa let panels: Panel[] = []; let variables: VariableModel[] = []; - if (body instanceof SceneGridLayout) { - for (const child of body.state.children) { + if (body instanceof DefaultGridLayoutManager) { + for (const child of body.state.grid.state.children) { if (child instanceof DashboardGridItem) { // handle panel repeater scenario if (child.state.variableName) { diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx index d464023a07a..547e4d8e41b 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx @@ -11,7 +11,7 @@ import { LoadingState, PanelData, } from '@grafana/data'; -import { SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes'; +import { SceneTimeRange, dataLayers } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; @@ -221,9 +221,6 @@ async function buildTestScene() { }), ], }), - body: new SceneGridLayout({ - children: [], - }), editview: annotationsView, }); diff --git a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx index 7e25117a5e8..5cbd37a99fb 100644 --- a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx @@ -2,7 +2,7 @@ import { render as RTLRender } from '@testing-library/react'; import * as React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; -import { SceneGridLayout, SceneTimeRange, behaviors } from '@grafana/scenes'; +import { SceneTimeRange, behaviors } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { DashboardControls } from '../scene/DashboardControls'; @@ -212,9 +212,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), editview: settings, }); diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx index e2f343c383c..a7dcd617186 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx @@ -1,4 +1,4 @@ -import { behaviors, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { behaviors, SceneTimeRange } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import * as utils from '../pages/utils'; @@ -125,9 +125,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), editview: settings, }); diff --git a/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx index 4bb9868502f..dd6e97e6594 100644 --- a/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx @@ -1,4 +1,4 @@ -import { SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { SceneTimeRange } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -36,9 +36,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), editview: permissionsView, }); diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx index a547fa10d4f..f3d93596d03 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx @@ -13,7 +13,6 @@ import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; import { SceneVariableSet, CustomVariable, - SceneGridLayout, VizPanel, AdHocFiltersVariable, SceneVariableState, @@ -22,8 +21,8 @@ import { import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { activateFullSceneTree } from '../utils/test-utils'; import { VariablesEditView } from './VariablesEditView'; @@ -288,8 +287,7 @@ describe('VariablesEditView', () => { it('should keep dependencies with panels when the type is changed so the variable is replaced', async () => { // Uses function to avoid store reference to previous existing variables const getSourceVariable = () => variableView.getVariables()[0] as CustomVariable; - const getDependantPanel = () => - ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body as VizPanel; + const getDependantPanel = () => dashboard.state.body.getVizPanels()[0]; expect(getSourceVariable().getValue()).toBe('test'); // Using description to get the interpolated value @@ -342,20 +340,14 @@ async function buildTestScene() { }), ], }), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - description: 'Panel A depends on customVar with current value $customVar', - key: 'panel-1', - pluginId: 'table', - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + description: 'Panel A depends on customVar with current value $customVar', + key: 'panel-1', + pluginId: 'table', + }), + ]), editview: variableView, }); diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx index 9e69917dfee..6d2b37219f6 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx @@ -1,4 +1,4 @@ -import { SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { SceneTimeRange } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -162,9 +162,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), editview: versionsView, }); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx index 4bc553b457c..22c5d54e16f 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx @@ -2,10 +2,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import ExportButton from './ExportButton'; @@ -38,18 +38,7 @@ function setup() { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); render(); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx index 38045ba05b0..530dd8ae765 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx @@ -1,10 +1,10 @@ import { render, screen } from '@testing-library/react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import ExportMenu from './ExportMenu'; @@ -27,18 +27,7 @@ function setup() { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); render(); } diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx index 842b257470a..f939ea8820f 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx @@ -2,10 +2,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import ShareButton from './ShareButton'; @@ -44,18 +44,7 @@ function setup() { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); render(); diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx index 525ece5f205..0e7940e34d1 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx @@ -1,12 +1,12 @@ import { render, screen } from '@testing-library/react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { contextSrv } from 'app/core/services/context_srv'; import { config } from '../../../../core/config'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import ShareMenu from './ShareMenu'; @@ -87,18 +87,7 @@ function setup(overrides?: Partial) { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), ...overrides, }); diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx index 205bf6bc12c..2dab2d48804 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx @@ -4,7 +4,6 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { CustomVariable, SceneDataTransformer, - SceneGridLayout, SceneQueryRunner, SceneTimeRange, SceneVariableSet, @@ -12,8 +11,8 @@ import { VizPanelState, } from '@grafana/scenes'; import { contextSrv } from 'app/core/core'; -import { DashboardGridItem } from 'app/features/dashboard-scene/scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from 'app/features/dashboard-scene/scene/DashboardScene'; +import { DefaultGridLayoutManager } from 'app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager'; import { ShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext'; @@ -91,18 +90,7 @@ async function setup(panelState?: Partial, dashboardState?: Parti title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), ...dashboardState, }); diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx index 7c25a0910cf..07fdec6b6a1 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx @@ -7,18 +7,17 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { config, setPluginImportUtils } from '@grafana/runtime'; import { CustomVariable, - SceneGridLayout, SceneQueryRunner, SceneTimeRange, SceneVariableSet, VizPanel, VizPanelState, } from '@grafana/scenes'; +import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; +import { DefaultGridLayoutManager } from 'app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager'; import { contextSrv } from '../../../../../core/services/context_srv'; import * as sharePublicDashboardUtils from '../../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { shareDashboardType } from '../../../../dashboard/components/ShareModal/utils'; -import { DashboardGridItem } from '../../../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../../../scene/DashboardScene'; import { activateFullSceneTree } from '../../../utils/test-utils'; import { ShareDrawer } from '../../ShareDrawer/ShareDrawer'; @@ -67,6 +66,7 @@ describe('Alerts', () => { }); expect(screen.queryByTestId(selectors.TemplateVariablesWarningAlert)).toBeInTheDocument(); }); + it('when dashboard has unsupported datasources, warning is shown', async () => { await buildAndRenderScenario({ panelOverrides: { @@ -101,23 +101,14 @@ async function buildAndRenderScenario({ canEdit: true, }, $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: new VizPanel({ - title: 'Panel A', - pluginId: 'table', - key: 'panel-12', - ...panelOverrides, - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + pluginId: 'table', + key: 'panel-12', + ...panelOverrides, + }), + ]), overlay: drawer, ...overrides, }); diff --git a/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx b/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx index ea2bd4f3588..b2772d5f856 100644 --- a/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { selectors } from '@grafana/e2e-selectors'; import { locationService, setPluginImportUtils } from '@grafana/runtime'; -import { SceneGridLayout, SceneTimeRange, UrlSyncContextProvider } from '@grafana/scenes'; +import { SceneTimeRange, UrlSyncContextProvider } from '@grafana/scenes'; import { render } from '../../../../../test/test-utils'; import { shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; @@ -63,9 +63,6 @@ async function buildAndRenderScenario() { canEdit: true, }, $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [], - }), overlay: drawer, }); diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx index 7ae9f501f0c..cded7ef3abe 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx @@ -6,10 +6,10 @@ import { dateTime } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService, setPluginImportUtils } from '@grafana/runtime'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { activateFullSceneTree } from '../utils/test-utils'; import { ShareLinkTab } from './ShareLinkTab'; @@ -111,18 +111,7 @@ function buildAndRenderScenario(options: ScenarioOptions) { canEdit: true, }, $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), overlay: tab, }); diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts b/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts index 37b49dc9d29..35e0ddf223b 100644 --- a/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts @@ -1,17 +1,10 @@ import { DataSourceWithBackend } from '@grafana/runtime'; -import { - SceneGridItemLike, - VizPanel, - SceneQueryRunner, - SceneDataTransformer, - SceneGridLayout, - SceneGridRow, -} from '@grafana/scenes'; +import { VizPanel } from '@grafana/scenes'; import { supportedDatasources } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; +import { getQueryRunnerFor } from '../../utils/utils'; export const getUnsupportedDashboardDatasources = async (types: string[]): Promise => { let unsupportedDS = new Set(); @@ -33,63 +26,25 @@ export const getUnsupportedDashboardDatasources = async (types: string[]): Promi export function getPanelDatasourceTypes(scene: DashboardScene): string[] { const types = new Set(); - const body = scene.state.body; - if (!(body instanceof SceneGridLayout)) { - return []; - } + const panels = scene.state.body.getVizPanels(); - for (const child of body.state.children) { - if (child instanceof DashboardGridItem) { - const ts = panelDatasourceTypes(child); - for (const t of ts) { - types.add(t); - } - } - - if (child instanceof SceneGridRow) { - const ts = rowTypes(child); - for (const t of ts) { - types.add(t); - } + for (const child of panels) { + const ts = panelDatasourceTypes(child); + for (const t of ts) { + types.add(t); } } return Array.from(types).sort(); } -function rowTypes(gridRow: SceneGridRow) { - const types = new Set(gridRow.state.children.map((c) => panelDatasourceTypes(c)).flat()); - return types; -} - -function panelDatasourceTypes(gridItem: SceneGridItemLike) { - let vizPanel: VizPanel | undefined; - - if (gridItem instanceof DashboardGridItem) { - if (gridItem.state.body instanceof VizPanel) { - vizPanel = gridItem.state.body; - } else { - throw new Error('DashboardGridItem body expected to be VizPanel'); - } - } - - if (!vizPanel) { - throw new Error('Unsupported grid item type'); - } - const dataProvider = vizPanel.state.$data; +function panelDatasourceTypes(vizPanel: VizPanel) { const types = new Set(); - if (dataProvider instanceof SceneQueryRunner) { - for (const q of dataProvider.state.queries) { - types.add(q.datasource?.type ?? ''); - } - } - if (dataProvider instanceof SceneDataTransformer) { - const panelData = dataProvider.state.$data; - if (panelData instanceof SceneQueryRunner) { - for (const q of panelData.state.queries) { - types.add(q.datasource?.type ?? ''); - } + const queryRunner = getQueryRunnerFor(vizPanel); + if (queryRunner) { + for (const q of queryRunner.state.queries) { + types.add(q.datasource?.type ?? ''); } } diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.ts b/public/app/features/dashboard-scene/solo/useSoloPanel.ts index 28d8abf22d9..8905a79ce32 100644 --- a/public/app/features/dashboard-scene/solo/useSoloPanel.ts +++ b/public/app/features/dashboard-scene/solo/useSoloPanel.ts @@ -1,10 +1,8 @@ import { useState, useEffect } from 'react'; -import { VizPanel, SceneObject, SceneGridRow, UrlSyncManager } from '@grafana/scenes'; +import { VizPanel, UrlSyncManager } from '@grafana/scenes'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardRepeatsProcessedEvent } from '../scene/types'; import { findVizPanelByKey, isPanelClone } from '../utils/utils'; @@ -63,31 +61,10 @@ function findRepeatClone(dashboard: DashboardScene, panelId: string): Promise { - if (child instanceof DashboardGridItem && !child.isActive) { - child.activate(); - return; - } - - if (child instanceof SceneGridRow && child.state.$behaviors) { - for (const behavior of child.state.$behaviors) { - if (behavior instanceof RowRepeaterBehavior && !child.isActive) { - child.activate(); - break; - } - } - - // Activate any panel DashboardGridItem inside the row - activateAllRepeaters(child); - } + dashboard.state.body.activateRepeaters?.(); }); } diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts index a6b43edad2e..d8a9b152b03 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts @@ -1,7 +1,6 @@ import { TimeRangeUpdatedEvent } from '@grafana/runtime'; import { behaviors, - SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, @@ -14,8 +13,8 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { NEW_LINK } from '../settings/links/utils'; import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper'; @@ -97,13 +96,13 @@ describe('DashboardModelCompatibilityWrapper', () => { }); it('Can remove panel', () => { - const { wrapper, scene } = setup(); + const { wrapper } = setup(); - expect((scene.state.body as SceneGridLayout).state.children.length).toBe(5); + expect(wrapper.panels.length).toBe(5); wrapper.removePanel(wrapper.getPanelById(1)!); - expect((scene.state.body as SceneGridLayout).state.children.length).toBe(4); + expect(wrapper.panels.length).toBe(4); }); it('Checks if annotations are editable', () => { @@ -173,75 +172,59 @@ function setup() { controls: new DashboardControls({ hideTimeControls: true, }), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel with a regular data source query', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A' }], - datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }, - }), - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel with a regular data source query', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ + key: 'data-query-runner', + queries: [{ refId: 'A' }], + datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }, }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel with no queries', - key: 'panel-2', - pluginId: 'table', - }), + }), + new VizPanel({ + title: 'Panel with no queries', + key: 'panel-2', + pluginId: 'table', + }), + new VizPanel({ + title: 'Panel with a shared query', + key: 'panel-3', + pluginId: 'table', + $data: new SceneQueryRunner({ + key: 'data-query-runner', + queries: [{ refId: 'A', panelId: 1 }], + datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, }), - - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel with a shared query', - key: 'panel-3', - pluginId: 'table', - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A', panelId: 1 }], - datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, - }), + }), + new VizPanel({ + title: 'Panel with a regular data source query and transformations', + key: 'panel-4', + pluginId: 'table', + $data: new SceneDataTransformer({ + $data: new SceneQueryRunner({ + key: 'data-query-runner', + queries: [{ refId: 'A' }], + datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }, }), + transformations: [], }), - - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel with a regular data source query and transformations', - key: 'panel-4', - pluginId: 'table', - $data: new SceneDataTransformer({ - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A' }], - datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }, - }), - transformations: [], - }), + }), + new VizPanel({ + title: 'Panel with a shared query and transformations', + key: 'panel-4', + pluginId: 'table', + $data: new SceneDataTransformer({ + $data: new SceneQueryRunner({ + key: 'data-query-runner', + queries: [{ refId: 'A', panelId: 1 }], + datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, }), + transformations: [], }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel with a shared query and transformations', - key: 'panel-4', - pluginId: 'table', - $data: new SceneDataTransformer({ - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A', panelId: 1 }], - datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, - }), - transformations: [], - }), - }), - }), - ], - }), + }), + ]), }); const wrapper = new DashboardModelCompatibilityWrapper(scene); diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts index a5e25a93968..9140b9ce6ae 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts @@ -2,17 +2,8 @@ import { Subscription } from 'rxjs'; import { AnnotationQuery, DashboardCursorSync, dateTimeFormat, DateTimeInput, EventBusSrv } from '@grafana/data'; import { TimeRangeUpdatedEvent } from '@grafana/runtime'; -import { - behaviors, - SceneDataLayerSet, - sceneGraph, - SceneGridLayout, - SceneGridRow, - SceneObject, - VizPanel, -} from '@grafana/scenes'; +import { behaviors, SceneDataLayerSet, sceneGraph, SceneObject, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations'; @@ -175,39 +166,7 @@ export class DashboardModelCompatibilityWrapper { return; } - const gridItem = vizPanel.parent; - if (!(gridItem instanceof DashboardGridItem)) { - console.error('Trying to remove a panel that is not wrapped in DashboardGridItem'); - return; - } - - const layout = sceneGraph.getLayout(vizPanel); - if (!(layout instanceof SceneGridLayout)) { - console.error('Trying to remove a panel in a layout that is not SceneGridLayout '); - return; - } - - // if grid item is directly in the layout just remove it - if (layout === gridItem.parent) { - layout.setState({ - children: layout.state.children.filter((child) => child !== gridItem), - }); - } - - // Removing from a row is a bit more complicated - if (gridItem.parent instanceof SceneGridRow) { - // Clone the row and remove the grid item - const newRow = layout.clone({ - children: layout.state.children.filter((child) => child !== gridItem), - }); - - // Now update the grid layout and replace the row with the updated one - if (layout.parent instanceof SceneGridLayout) { - layout.parent.setState({ - children: layout.parent.state.children.map((child) => (child === layout ? newRow : child)), - }); - } - } + this._scene.removePanel(vizPanel); } public canEditAnnotations(dashboardUID?: string) { diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index 7e97d9530dd..28dc6622f12 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -1,4 +1,4 @@ -import { SceneGridLayout, SceneGridRow, SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; +import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; @@ -6,10 +6,10 @@ import { DashboardControls } from '../scene/DashboardControls'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; -import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph'; +import { dashboardSceneGraph } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; describe('dashboardSceneGraph', () => { @@ -27,36 +27,6 @@ describe('dashboardSceneGraph', () => { }); }); - describe('getVizPanels', () => { - let scene: DashboardScene; - - beforeEach(async () => { - scene = buildTestScene(); - }); - - it('Should return all panels', () => { - const vizPanels = dashboardSceneGraph.getVizPanels(scene); - - expect(vizPanels.length).toBe(6); - expect(vizPanels[0].state.title).toBe('Panel A'); - expect(vizPanels[1].state.title).toBe('Panel B'); - expect(vizPanels[2].state.title).toBe('Panel C'); - expect(vizPanels[3].state.title).toBe('Panel D'); - expect(vizPanels[4].state.title).toBe('Panel E'); - expect(vizPanels[5].state.title).toBe('Panel F'); - }); - - it('Should return an empty array when scene has no panels', () => { - scene.setState({ - body: new SceneGridLayout({ children: [] }), - }); - - const vizPanels = dashboardSceneGraph.getVizPanels(scene); - - expect(vizPanels.length).toBe(0); - }); - }); - describe('getDataLayers', () => { let scene: DashboardScene; @@ -80,141 +50,6 @@ describe('dashboardSceneGraph', () => { }); }); - describe('getNextPanelId', () => { - it('should get next panel id in a simple 3 panel layout', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-3', - pluginId: 'table', - }), - }), - ], - }), - }); - - const id = getNextPanelId(scene); - - expect(id).toBe(4); - }); - - it('should take library panels, panels in rows and panel repeaters into account', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Library Panel 1', - key: 'panel-2', - $behaviors: [ - new LibraryPanelBehavior({ - uid: 'uid', - name: 'LibPanel', - title: 'Library Panel 1', - }), - ], - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-2-clone-1', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-4', - pluginId: 'table', - }), - variableName: 'repeat', - repeatedPanels: [], - repeatDirection: 'h', - maxPerRow: 1, - }), - new SceneGridRow({ - key: 'key', - title: 'row', - children: [ - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel E', - key: 'panel-2-clone-2', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Library Panel 2', - key: 'panel-3', - $behaviors: [ - new LibraryPanelBehavior({ - uid: 'uid', - name: 'LibPanel', - title: 'Library Panel 2', - }), - ], - }), - }), - ], - }), - ], - }), - }); - - const id = getNextPanelId(scene); - - expect(id).toBe(5); - }); - - it('should get next panel id in a layout with rows', () => { - const scene = buildTestScene(); - const id = getNextPanelId(scene); - - expect(id).toBe(3); - }); - - it('should return 1 if no panels are found', () => { - const scene = buildTestScene({ body: new SceneGridLayout({ children: [] }) }); - const id = getNextPanelId(scene); - - expect(id).toBe(1); - }); - - it('should throw an error if body is not SceneGridLayout', () => { - const scene = buildTestScene({ body: undefined }); - - expect(() => getNextPanelId(scene)).toThrow('Dashboard body is not a SceneGridLayout'); - }); - }); - describe('getCursorSync', () => { it('should return cursor sync behavior', () => { const scene = buildTestScene(); @@ -224,7 +59,8 @@ describe('dashboardSceneGraph', () => { }); it('should return undefined if no cursor sync behavior', () => { - const scene = buildTestScene({ $behaviors: [] }); + const scene = buildTestScene(); + scene.setState({ $behaviors: [] }); const cursorSync = dashboardSceneGraph.getCursorSync(scene); expect(cursorSync).toBeUndefined(); @@ -259,63 +95,30 @@ function buildTestScene(overrides?: Partial) { }), ], }), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-2-clone-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel D', - key: 'panel-with-links', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }), - titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], - }), - }), - new SceneGridRow({ - key: 'key', - title: 'row', - children: [ - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel E', - key: 'panel-2-clone-2', - pluginId: 'table', - }), + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel F', - key: 'panel-2-clone-2', - pluginId: 'table', - }), + }), + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel D', + key: 'panel-with-links', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }), + titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], }), - ], - }), - ], + }), + ], + }), }), ...overrides, }); diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 7f6198fd605..61ee5d75291 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,12 +1,9 @@ -import { VizPanel, SceneGridRow, sceneGraph, SceneGridLayout, behaviors } from '@grafana/scenes'; +import { VizPanel, sceneGraph, behaviors } from '@grafana/scenes'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { VizPanelLinks } from '../scene/PanelLinks'; -import { getPanelIdForVizPanel } from './utils'; - function getTimePicker(scene: DashboardScene) { return scene.state.controls?.state.timePicker; } @@ -28,29 +25,7 @@ function getPanelLinks(panel: VizPanel) { } function getVizPanels(scene: DashboardScene): VizPanel[] { - const panels: VizPanel[] = []; - - scene.state.body.forEachChild((child) => { - if (!(child instanceof DashboardGridItem) && !(child instanceof SceneGridRow)) { - throw new Error('Child is not a DashboardGridItem or SceneGridRow, invalid scene'); - } - - if (child instanceof DashboardGridItem) { - if (child.state.body instanceof VizPanel) { - panels.push(child.state.body); - } - } else if (child instanceof SceneGridRow) { - child.forEachChild((child) => { - if (child instanceof DashboardGridItem) { - if (child.state.body instanceof VizPanel) { - panels.push(child.state.body); - } - } - }); - } - }); - - return panels; + return scene.state.body.getVizPanels(); } function getDataLayers(scene: DashboardScene): DashboardDataLayerSet { @@ -74,51 +49,7 @@ export function getCursorSync(scene: DashboardScene) { } export function getNextPanelId(dashboard: DashboardScene): number { - let max = 0; - const body = dashboard.state.body; - - if (!(body instanceof SceneGridLayout)) { - throw new Error('Dashboard body is not a SceneGridLayout'); - } - - for (const child of body.state.children) { - if (child instanceof DashboardGridItem) { - const vizPanel = child.state.body; - - if (vizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - - if (child instanceof SceneGridRow) { - //rows follow the same key pattern --- e.g.: `panel-6` - const panelId = getPanelIdForVizPanel(child); - - if (panelId > max) { - max = panelId; - } - - for (const rowChild of child.state.children) { - if (rowChild instanceof DashboardGridItem) { - const vizPanel = rowChild.state.body; - - if (vizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - } - } - } - - return max + 1; + return dashboard.state.body.getNextPanelId(); } export const dashboardSceneGraph = { diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts index aa55c8a7eac..8e55d67d767 100644 --- a/public/app/features/dashboard-scene/utils/test-utils.ts +++ b/public/app/features/dashboard-scene/utils/test-utils.ts @@ -18,6 +18,7 @@ import { DashboardDTO } from 'app/types'; import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; export function setupLoadDashboardMock(rsp: DeepPartial, spy?: jest.Mock) { const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp); @@ -184,8 +185,10 @@ export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel $variables: new SceneVariableSet({ variables: [panelRepeatVariable, rowRepeatVariable], }), - body: new SceneGridLayout({ - children: [row], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [row], + }), }), }); diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 004e152bc4c..a43bfa336ad 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -6,7 +6,6 @@ import { MultiValueVariable, SceneDataTransformer, sceneGraph, - SceneGridRow, SceneObject, SceneQueryRunner, VizPanel, @@ -19,7 +18,6 @@ import { DashboardScene } from '../scene/DashboardScene'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; -import { RowActions } from '../scene/row-actions/RowActions'; import { dashboardSceneGraph } from './dashboardSceneGraph'; @@ -241,17 +239,6 @@ export function getDefaultVizPanel(dashboard: DashboardScene): VizPanel { }); } -export function getDefaultRow(dashboard: DashboardScene): SceneGridRow { - const id = dashboardSceneGraph.getNextPanelId(dashboard); - - return new SceneGridRow({ - key: getVizPanelKeyForPanelId(id), - title: 'Row title', - actions: new RowActions({}), - y: 0, - }); -} - export function isLibraryPanel(vizPanel: VizPanel): boolean { return getLibraryPanelBehavior(vizPanel) !== undefined; } From e5d0877af7695722cce3fb4c2fb45313f2e14caa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:47:40 +0000 Subject: [PATCH 032/174] Update dependency knip to v5.30.6 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index aa7ee811029..4d0f6e2335c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22122,8 +22122,8 @@ __metadata: linkType: hard "knip@npm:^5.10.0": - version: 5.30.5 - resolution: "knip@npm:5.30.5" + version: 5.30.6 + resolution: "knip@npm:5.30.6" dependencies: "@nodelib/fs.walk": "npm:1.2.8" "@snyk/github-codeowners": "npm:1.1.0" @@ -22147,7 +22147,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/0cf47fee73ae57a41014a78cb6768228746b8b8fcfd908262caae2c857f5b04c86b6ae2184f5534345fcad5931d8f6a8ecd274a80b1103ab1115ae91785d574a + checksum: 10/42973ec1f5208017c63232dd5a8c29b32accc9bbc00712afbb247fe14b2e8687e27a66a91cf48250cdae30aff6c456d71271c82dda9136785f96520b8c8a049d languageName: node linkType: hard From 7710f1c3cf0b22696b3fd19a1ce6cec7070fab35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:15:36 +0000 Subject: [PATCH 033/174] Update dependency rollup to v4.22.5 --- yarn.lock | 144 +++++++++++++++++++++++++++--------------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4d0f6e2335c..e3176d1ab9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7026,114 +7026,114 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.22.4" +"@rollup/rollup-android-arm-eabi@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.22.5" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-android-arm64@npm:4.22.4" +"@rollup/rollup-android-arm64@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-android-arm64@npm:4.22.5" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-darwin-arm64@npm:4.22.4" +"@rollup/rollup-darwin-arm64@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-darwin-arm64@npm:4.22.5" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-darwin-x64@npm:4.22.4" +"@rollup/rollup-darwin-x64@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-darwin-x64@npm:4.22.5" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.22.4" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.22.5" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.22.4" +"@rollup/rollup-linux-arm-musleabihf@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.22.5" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.22.4" +"@rollup/rollup-linux-arm64-gnu@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.22.5" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.22.4" +"@rollup/rollup-linux-arm64-musl@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.22.5" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.22.4" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.22.5" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.22.4" +"@rollup/rollup-linux-riscv64-gnu@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.22.5" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.22.4" +"@rollup/rollup-linux-s390x-gnu@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.22.5" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.22.4" +"@rollup/rollup-linux-x64-gnu@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.22.5" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.22.4" +"@rollup/rollup-linux-x64-musl@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.22.5" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.22.4" +"@rollup/rollup-win32-arm64-msvc@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.22.5" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.22.4" +"@rollup/rollup-win32-ia32-msvc@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.22.5" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.22.4": - version: 4.22.4 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.22.4" +"@rollup/rollup-win32-x64-msvc@npm:4.22.5": + version: 4.22.5 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.22.5" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -9894,10 +9894,10 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": - version: 1.0.5 - resolution: "@types/estree@npm:1.0.5" - checksum: 10/7de6d928dd4010b0e20c6919e1a6c27b61f8d4567befa89252055fad503d587ecb9a1e3eab1b1901f923964d7019796db810b7fd6430acb26c32866d126fd408 +"@types/estree@npm:*, @types/estree@npm:1.0.6, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": + version: 1.0.6 + resolution: "@types/estree@npm:1.0.6" + checksum: 10/9d35d475095199c23e05b431bcdd1f6fec7380612aed068b14b2a08aa70494de8a9026765a5a91b1073f636fb0368f6d8973f518a31391d519e20c59388ed88d languageName: node linkType: hard @@ -28990,26 +28990,26 @@ __metadata: linkType: hard "rollup@npm:^4.22.4": - version: 4.22.4 - resolution: "rollup@npm:4.22.4" + version: 4.22.5 + resolution: "rollup@npm:4.22.5" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.22.4" - "@rollup/rollup-android-arm64": "npm:4.22.4" - "@rollup/rollup-darwin-arm64": "npm:4.22.4" - "@rollup/rollup-darwin-x64": "npm:4.22.4" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.22.4" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.22.4" - "@rollup/rollup-linux-arm64-gnu": "npm:4.22.4" - "@rollup/rollup-linux-arm64-musl": "npm:4.22.4" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.22.4" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.22.4" - "@rollup/rollup-linux-s390x-gnu": "npm:4.22.4" - "@rollup/rollup-linux-x64-gnu": "npm:4.22.4" - "@rollup/rollup-linux-x64-musl": "npm:4.22.4" - "@rollup/rollup-win32-arm64-msvc": "npm:4.22.4" - "@rollup/rollup-win32-ia32-msvc": "npm:4.22.4" - "@rollup/rollup-win32-x64-msvc": "npm:4.22.4" - "@types/estree": "npm:1.0.5" + "@rollup/rollup-android-arm-eabi": "npm:4.22.5" + "@rollup/rollup-android-arm64": "npm:4.22.5" + "@rollup/rollup-darwin-arm64": "npm:4.22.5" + "@rollup/rollup-darwin-x64": "npm:4.22.5" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.22.5" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.22.5" + "@rollup/rollup-linux-arm64-gnu": "npm:4.22.5" + "@rollup/rollup-linux-arm64-musl": "npm:4.22.5" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.22.5" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.22.5" + "@rollup/rollup-linux-s390x-gnu": "npm:4.22.5" + "@rollup/rollup-linux-x64-gnu": "npm:4.22.5" + "@rollup/rollup-linux-x64-musl": "npm:4.22.5" + "@rollup/rollup-win32-arm64-msvc": "npm:4.22.5" + "@rollup/rollup-win32-ia32-msvc": "npm:4.22.5" + "@rollup/rollup-win32-x64-msvc": "npm:4.22.5" + "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -29048,7 +29048,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/0fbee8c14d9052624c76a09fe79ed4d46024832be3ceea86c69f1521ae84b581a64c6e6596fdd796030c206835987e1a0a3be85f4c0d35b71400be5dce799d12 + checksum: 10/f34812fa982442ab71410b649630c24434b2dc02485e543607734766eb7211ce7e0a79102f27210f337af00f3617006adebb4f87fb2e9d24cac7100d0e599352 languageName: node linkType: hard From 0160f4f72ce5d770a1ab4dd2fd2a433eec45628b Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 27 Sep 2024 15:53:11 +0200 Subject: [PATCH 034/174] RBAC: Add legacy authorization checks to service accounts (#93753) * Extract a helper funtion to perform list with authorization checks * Add k8s verb to utils package * Construct default mapping when no custom mapping is passed * Configure authorization checks for service accounts * Fix helper and add filtering to service accounts --- pkg/apimachinery/utils/verbs.go | 22 ++++ .../iam/v0alpha1/types_servier_account.go | 8 ++ pkg/apis/iam/v0alpha1/types_user.go | 12 +- pkg/registry/apis/iam/authorizer.go | 22 +++- pkg/registry/apis/iam/common/common.go | 94 +++++++++++++++ pkg/registry/apis/iam/common/common_test.go | 105 +++++++++++++++++ .../apis/iam/legacy/service_account.go | 78 +++++++++++++ .../legacy/service_account_internal_id.sql | 7 ++ pkg/registry/apis/iam/legacy/sql.go | 1 + pkg/registry/apis/iam/register.go | 2 +- pkg/registry/apis/iam/serviceaccount/store.go | 52 +++++---- pkg/registry/apis/iam/user/store.go | 107 ++++-------------- pkg/services/accesscontrol/authorizer.go | 21 +++- pkg/services/accesscontrol/authorizer_test.go | 11 +- 14 files changed, 424 insertions(+), 118 deletions(-) create mode 100644 pkg/apimachinery/utils/verbs.go create mode 100644 pkg/registry/apis/iam/common/common_test.go create mode 100644 pkg/registry/apis/iam/legacy/service_account_internal_id.sql diff --git a/pkg/apimachinery/utils/verbs.go b/pkg/apimachinery/utils/verbs.go new file mode 100644 index 00000000000..7ab5086332f --- /dev/null +++ b/pkg/apimachinery/utils/verbs.go @@ -0,0 +1,22 @@ +package utils + +// Kubernetes request verbs +// http://kubernetes.io/docs/reference/access-authn-authz/authorization/#request-verb-resource +const ( + // VerbGet is mapped from HTTP GET for individual resource + VerbGet = "get" + // VerbList is mapped from HTTP GET for collections + VerbList = "list" + // VerbWatch is mapped from HTTP GET for watching an individual resource or collection of resources + VerbWatch = "watch" + // VerbCreate is mapped from HTTP POST + VerbCreate = "create" + // VerbUpdate is mapped from HTTP PUT + VerbUpdate = "update" + // VerbPatch is mapped from HTTP PATCH + VerbPatch = "patch" + // VerbDelete is mapped from HTTP DELETE for individual resources + VerbDelete = "delete" + // VerbDelete is mapped from HTTP DELETE for collections + VerbDeleteCollection = "deletecollection" +) diff --git a/pkg/apis/iam/v0alpha1/types_servier_account.go b/pkg/apis/iam/v0alpha1/types_servier_account.go index 8451095c04d..04e75eda7a2 100644 --- a/pkg/apis/iam/v0alpha1/types_servier_account.go +++ b/pkg/apis/iam/v0alpha1/types_servier_account.go @@ -1,6 +1,8 @@ package v0alpha1 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -12,9 +14,15 @@ type ServiceAccount struct { Spec ServiceAccountSpec `json:"spec,omitempty"` } +func (s ServiceAccount) AuthID() string { + return fmt.Sprintf("%d", s.Spec.InternalID) +} + type ServiceAccountSpec struct { Title string `json:"title,omitempty"` Disabled bool `json:"disabled,omitempty"` + // This is currently used for authorization checks but we don't want to expose it + InternalID int64 `json:"-"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/iam/v0alpha1/types_user.go b/pkg/apis/iam/v0alpha1/types_user.go index 713b2041876..2ea79d2ecdb 100644 --- a/pkg/apis/iam/v0alpha1/types_user.go +++ b/pkg/apis/iam/v0alpha1/types_user.go @@ -1,6 +1,10 @@ package v0alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type User struct { @@ -10,12 +14,18 @@ type User struct { Spec UserSpec `json:"spec,omitempty"` } +func (u User) AuthID() string { + return fmt.Sprintf("%d", u.Spec.InternalID) +} + type UserSpec struct { Name string `json:"name,omitempty"` Login string `json:"login,omitempty"` Email string `json:"email,omitempty"` EmailVerified bool `json:"emailVerified,omitempty"` Disabled bool `json:"disabled,omitempty"` + // This is currently used for authorization checks but we don't want to expose it + InternalID int64 `json:"-"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 81dc1ee0187..8cc8cb68aba 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/authlib/claims" "k8s.io/apiserver/pkg/authorization/authorizer" + "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -20,8 +21,8 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden Resource: iamv0.UserResourceInfo.GetName(), Attr: "id", Mapping: map[string]string{ - "get": accesscontrol.ActionOrgUsersRead, - "list": accesscontrol.ActionOrgUsersRead, + utils.VerbGet: accesscontrol.ActionOrgUsersRead, + utils.VerbList: accesscontrol.ActionOrgUsersRead, }, Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) { res, err := store.GetUserInternalID(ctx, ns, legacy.GetUserInternalIDQuery{ @@ -36,10 +37,23 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden accesscontrol.ResourceAuthorizerOptions{ Resource: "display", Unchecked: map[string]bool{ - "get": true, - "list": true, + utils.VerbGet: true, + utils.VerbList: true, }, }, + accesscontrol.ResourceAuthorizerOptions{ + Resource: iamv0.ServiceAccountResourceInfo.GetName(), + Attr: "id", + Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) { + res, err := store.GetServiceAccountInternalID(ctx, ns, legacy.GetServiceAccountInternalIDQuery{ + UID: name, + }) + if err != nil { + return nil, err + } + return []string{fmt.Sprintf("serviceaccounts:id:%d", res.ID)}, nil + }), + }, ) return gfauthorizer.NewResourceAuthorizer(client), client diff --git a/pkg/registry/apis/iam/common/common.go b/pkg/registry/apis/iam/common/common.go index 1e3533f3b73..ab6cf73bef5 100644 --- a/pkg/registry/apis/iam/common/common.go +++ b/pkg/registry/apis/iam/common/common.go @@ -1,9 +1,14 @@ package common import ( + "context" "strconv" + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/team" ) @@ -23,3 +28,92 @@ func MapTeamPermission(p team.PermissionType) iamv0.TeamPermission { return iamv0.TeamPermissionMember } } + +// Resource is required to be implemented for list return types so we can +// perform authorization. +type Resource interface { + AuthID() string +} + +type ListResponse[T Resource] struct { + Items []T + RV int64 + Continue int64 +} + +type ListFunc[T Resource] func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[T], error) + +// List is a helper function that will perform access check on resources if +// prvovided with a claims.AccessClient. +func List[T Resource]( + ctx context.Context, + resourceName string, + ac claims.AccessClient, + p Pagination, + fn ListFunc[T], +) (*ListResponse[T], error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + ident, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + check := func(_ string, _ string) bool { return true } + if ac != nil { + var err error + check, err = ac.Compile(ctx, ident, claims.AccessRequest{ + Verb: utils.VerbList, + Resource: resourceName, + Namespace: ns.Value, + }) + + if err != nil { + return nil, err + } + } + + res := &ListResponse[T]{Items: make([]T, 0, p.Limit)} + + first, err := fn(ctx, ns, p) + if err != nil { + return nil, err + } + + for _, item := range first.Items { + if !check(ns.Value, item.AuthID()) { + continue + } + res.Items = append(res.Items, item) + } + res.Continue = first.Continue + res.RV = first.RV + +outer: + for len(res.Items) < int(p.Limit) && res.Continue != 0 { + // FIXME: it is not optimal to reduce the amout we look for here but it is the easiest way to + // correctly handle pagination and continue tokens + r, err := fn(ctx, ns, Pagination{Limit: p.Limit - int64(len(res.Items)), Continue: res.Continue}) + if err != nil { + return nil, err + } + + for _, item := range r.Items { + if len(res.Items) == int(p.Limit) { + res.Continue = r.Continue + break outer + } + + if !check(ns.Value, item.AuthID()) { + continue + } + + res.Items = append(res.Items, item) + } + } + + return res, nil +} diff --git a/pkg/registry/apis/iam/common/common_test.go b/pkg/registry/apis/iam/common/common_test.go new file mode 100644 index 00000000000..0f1d359842f --- /dev/null +++ b/pkg/registry/apis/iam/common/common_test.go @@ -0,0 +1,105 @@ +package common + +import ( + "context" + "testing" + + "github.com/grafana/authlib/claims" + "github.com/stretchr/testify/assert" + "k8s.io/apiserver/pkg/endpoints/request" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +type item struct { + id string +} + +func (i item) AuthID() string { + return i.id +} + +func TestList(t *testing.T) { + ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) + + t.Run("should allow all items if no access client is passed", func(t *testing.T) { + ctx := newContext("stacks-1", newIdent()) + + res, err := List(ctx, "items", nil, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) { + return &ListResponse[item]{ + Items: []item{item{"1"}, item{"2"}}, + }, nil + }) + assert.NoError(t, err) + assert.Len(t, res.Items, 2) + }) + + t.Run("should filter out items that are allowed", func(t *testing.T) { + ctx := newContext("stacks-1", newIdent(accesscontrol.Permission{Action: "items:read", Scope: "items:uid:1"})) + + a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{ + Resource: "items", + Attr: "uid", + }) + res, err := List(ctx, "items", a, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) { + return &ListResponse[item]{ + Items: []item{item{"1"}, item{"2"}}, + }, nil + }) + assert.NoError(t, err) + assert.Len(t, res.Items, 1) + }) + + t.Run("should fetch more for partial response with continue token", func(t *testing.T) { + ctx := newContext("stacks-1", newIdent( + accesscontrol.Permission{Action: "items:read", Scope: "items:uid:1"}, + accesscontrol.Permission{Action: "items:read", Scope: "items:uid:3"}, + )) + + a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{ + Resource: "items", + Attr: "uid", + }) + + var called bool + + res, err := List(ctx, "items", a, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) { + if called { + return &ListResponse[item]{ + Items: []item{item{"3"}}, + }, nil + } + + called = true + return &ListResponse[item]{ + Items: []item{item{"1"}, item{"2"}}, + Continue: 3, + }, nil + }) + assert.NoError(t, err) + assert.Len(t, res.Items, 2) + + assert.Equal(t, "1", res.Items[0].AuthID()) + assert.Equal(t, "3", res.Items[1].AuthID()) + }) +} + +func newContext(namespace string, ident *identity.StaticRequester) context.Context { + return request.WithNamespace(identity.WithRequester(context.Background(), ident), namespace) +} + +func newIdent(permissions ...accesscontrol.Permission) *identity.StaticRequester { + pmap := map[string][]string{} + for _, p := range permissions { + pmap[p.Action] = append(pmap[p.Action], p.Scope) + } + + return &identity.StaticRequester{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: pmap}, + } +} diff --git a/pkg/registry/apis/iam/legacy/service_account.go b/pkg/registry/apis/iam/legacy/service_account.go index 03610e2a3f2..6e87be7f13d 100644 --- a/pkg/registry/apis/iam/legacy/service_account.go +++ b/pkg/registry/apis/iam/legacy/service_account.go @@ -2,6 +2,7 @@ package legacy import ( "context" + "errors" "fmt" "time" @@ -11,6 +12,83 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) +type GetServiceAccountInternalIDQuery struct { + OrgID int64 + UID string +} + +type GetServiceAccountInternalIDResult struct { + ID int64 +} + +var sqlQueryServiceAccountInternalIDTemplate = mustTemplate("service_account_internal_id.sql") + +func newGetServiceAccountInternalID(sql *legacysql.LegacyDatabaseHelper, q *GetServiceAccountInternalIDQuery) getServiceAccountInternalIDQuery { + return getServiceAccountInternalIDQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + UserTable: sql.Table("user"), + OrgUserTable: sql.Table("org_user"), + Query: q, + } +} + +type getServiceAccountInternalIDQuery struct { + sqltemplate.SQLTemplate + UserTable string + OrgUserTable string + Query *GetServiceAccountInternalIDQuery +} + +func (r getServiceAccountInternalIDQuery) Validate() error { + return nil // TODO +} + +func (s *legacySQLStore) GetServiceAccountInternalID( + ctx context.Context, + ns claims.NamespaceInfo, + query GetServiceAccountInternalIDQuery, +) (*GetServiceAccountInternalIDResult, error) { + query.OrgID = ns.OrgID + if query.OrgID == 0 { + return nil, fmt.Errorf("expected non zero org id") + } + + sql, err := s.sql(ctx) + if err != nil { + return nil, err + } + + req := newGetServiceAccountInternalID(sql, &query) + q, err := sqltemplate.Execute(sqlQueryServiceAccountInternalIDTemplate, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlQueryServiceAccountInternalIDTemplate.Name(), err) + } + + rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + defer func() { + if rows != nil { + _ = rows.Close() + } + }() + + if err != nil { + return nil, err + } + + if !rows.Next() { + return nil, errors.New("service account not found") + } + + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + + return &GetServiceAccountInternalIDResult{ + id, + }, nil +} + type ListServiceAccountsQuery struct { UID string OrgID int64 diff --git a/pkg/registry/apis/iam/legacy/service_account_internal_id.sql b/pkg/registry/apis/iam/legacy/service_account_internal_id.sql new file mode 100644 index 00000000000..f6c468bf577 --- /dev/null +++ b/pkg/registry/apis/iam/legacy/service_account_internal_id.sql @@ -0,0 +1,7 @@ +SELECT u.id +FROM {{ .Ident .UserTable }} as u +INNER JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id +WHERE o.org_id = {{ .Arg .Query.OrgID }} +AND u.uid = {{ .Arg .Query.UID }} +AND u.is_service_account +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/sql.go b/pkg/registry/apis/iam/legacy/sql.go index 47b29e65e82..5e0e3f470ed 100644 --- a/pkg/registry/apis/iam/legacy/sql.go +++ b/pkg/registry/apis/iam/legacy/sql.go @@ -18,6 +18,7 @@ type LegacyIdentityStore interface { ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error) ListUserTeams(ctx context.Context, ns claims.NamespaceInfo, query ListUserTeamsQuery) (*ListUserTeamsResult, error) + GetServiceAccountInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetServiceAccountInternalIDQuery) (*GetServiceAccountInternalIDResult, error) ListServiceAccounts(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountsQuery) (*ListServiceAccountResult, error) ListServiceAccountTokens(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountTokenQuery) (*ListServiceAccountTokenResult, error) diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 46bbd06b920..1a69dcf136c 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -108,7 +108,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store) serviceAccountResource := iamv0.ServiceAccountResourceInfo - storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store) + storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store, b.accessClient) storage[serviceAccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store) if b.sso != nil { diff --git a/pkg/registry/apis/iam/serviceaccount/store.go b/pkg/registry/apis/iam/serviceaccount/store.go index 618f2089793..fdc3e2f826e 100644 --- a/pkg/registry/apis/iam/serviceaccount/store.go +++ b/pkg/registry/apis/iam/serviceaccount/store.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" @@ -27,12 +28,13 @@ var ( var resource = iamv0.ServiceAccountResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore) *LegacyStore { - return &LegacyStore{store} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient) *LegacyStore { + return &LegacyStore{store, ac} } type LegacyStore struct { store legacy.LegacyIdentityStore + ac claims.AccessClient } func (s *LegacyStore) New() runtime.Object { @@ -58,28 +60,38 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - ns, err := request.NamespaceInfoFrom(ctx, true) + res, err := common.List( + ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options), + func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.ServiceAccount], error) { + found, err := s.store.ListServiceAccounts(ctx, ns, legacy.ListServiceAccountsQuery{ + Pagination: p, + }) + + if err != nil { + return nil, err + } + + items := make([]iamv0.ServiceAccount, 0, len(found.Items)) + for _, sa := range found.Items { + items = append(items, toSAItem(sa, ns.Value)) + } + + return &common.ListResponse[iamv0.ServiceAccount]{ + Items: items, + RV: found.RV, + Continue: found.Continue, + }, nil + }, + ) + if err != nil { return nil, err } - found, err := s.store.ListServiceAccounts(ctx, ns, legacy.ListServiceAccountsQuery{ - OrgID: ns.OrgID, - Pagination: common.PaginationFromListOptions(options), - }) - if err != nil { - return nil, err - } - - list := &iamv0.ServiceAccountList{} - for _, item := range found.Items { - list.Items = append(list.Items, toSAItem(item, ns.Value)) - } - - list.ListMeta.Continue = common.OptionalFormatInt(found.Continue) - list.ListMeta.ResourceVersion = common.OptionalFormatInt(found.RV) - - return list, err + obj := &iamv0.ServiceAccountList{Items: res.Items} + obj.ListMeta.Continue = common.OptionalFormatInt(res.Continue) + obj.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV) + return obj, nil } func toSAItem(sa legacy.ServiceAccount, ns string) iamv0.ServiceAccount { diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go index 26104cf9638..86e1f4669e4 100644 --- a/pkg/registry/apis/iam/user/store.go +++ b/pkg/registry/apis/iam/user/store.go @@ -11,7 +11,6 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "github.com/grafana/authlib/claims" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" @@ -62,99 +61,40 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - ns, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } + res, err := common.List( + ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options), + func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.User], error) { + found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ + Pagination: p, + }) - if s.ac != nil { - return s.listWithCheck(ctx, ns, common.PaginationFromListOptions(options)) - } - - return s.listWithoutCheck(ctx, ns, common.PaginationFromListOptions(options)) -} - -func (s *LegacyStore) listWithCheck(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (runtime.Object, error) { - ident, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - - check, err := s.ac.Compile(ctx, ident, claims.AccessRequest{ - Verb: "list", - Resource: resource.GetName(), - Namespace: ns.Value, - }) - - if err != nil { - return nil, err - } - - list := func(p common.Pagination) ([]iamv0.User, int64, int64, error) { - found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ - Pagination: p, - }) - - if err != nil { - return nil, 0, 0, err - } - - out := make([]iamv0.User, 0, len(found.Users)) - for _, u := range found.Users { - if check(ns.Value, strconv.FormatInt(u.ID, 10)) { - out = append(out, toUserItem(&u, ns.Value)) + if err != nil { + return nil, err } - } - return out, found.Continue, found.RV, nil - } + users := make([]iamv0.User, 0, len(found.Users)) + for _, u := range found.Users { + users = append(users, toUserItem(&u, ns.Value)) + } + + return &common.ListResponse[iamv0.User]{ + Items: users, + RV: found.RV, + Continue: found.Continue, + }, nil + }, + ) - items, c, rv, err := list(p) if err != nil { return nil, err } -outer: - for len(items) < int(p.Limit) && c != 0 { - var more []iamv0.User - more, c, _, err = list(common.Pagination{Limit: p.Limit, Continue: c}) - if err != nil { - return nil, err - } - - for _, u := range more { - if len(items) == int(p.Limit) { - break outer - } - items = append(items, u) - } - } - - obj := &iamv0.UserList{Items: items} - obj.ListMeta.Continue = common.OptionalFormatInt(c) - obj.ListMeta.ResourceVersion = common.OptionalFormatInt(rv) + obj := &iamv0.UserList{Items: res.Items} + obj.ListMeta.Continue = common.OptionalFormatInt(res.Continue) + obj.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV) return obj, nil } -func (s *LegacyStore) listWithoutCheck(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (runtime.Object, error) { - found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ - Pagination: p, - }) - - if err != nil { - return nil, err - } - - list := &iamv0.UserList{} - for _, item := range found.Users { - list.Items = append(list.Items, toUserItem(&item, ns.Value)) - } - - list.ListMeta.Continue = common.OptionalFormatInt(found.Continue) - list.ListMeta.ResourceVersion = common.OptionalFormatInt(found.RV) - return list, nil -} - func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { @@ -191,6 +131,7 @@ func toUserItem(u *user.User, ns string) iamv0.User { Email: u.Email, EmailVerified: u.EmailVerified, Disabled: u.IsDisabled, + InternalID: u.ID, }, } obj, _ := utils.MetaAccessor(item) diff --git a/pkg/services/accesscontrol/authorizer.go b/pkg/services/accesscontrol/authorizer.go index 9134166f0f3..5d400b06dc9 100644 --- a/pkg/services/accesscontrol/authorizer.go +++ b/pkg/services/accesscontrol/authorizer.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" ) // ResourceResolver is called before authorization is performed. @@ -35,6 +36,7 @@ type ResourceAuthorizerOptions struct { Attr string // Mapping is used to translate k8s verb to rbac action. // Key is the desired verb and value the rbac action it should be translated into. + // If no mapping is provided it will get a "default" mapping. Mapping map[string]string // Resolver if passed can translate into one or more scopes used to authorize resource. // This is useful when stored scopes are based on something else than k8s name or @@ -47,13 +49,28 @@ var _ claims.AccessClient = (*LegacyAccessClient)(nil) func NewLegacyAccessClient(ac AccessControl, opts ...ResourceAuthorizerOptions) *LegacyAccessClient { stored := map[string]ResourceAuthorizerOptions{} + defaultMapping := func(r string) map[string]string { + return map[string]string{ + utils.VerbGet: fmt.Sprintf("%s:read", r), + utils.VerbList: fmt.Sprintf("%s:read", r), + utils.VerbWatch: fmt.Sprintf("%s:read", r), + utils.VerbCreate: fmt.Sprintf("%s:create", r), + utils.VerbUpdate: fmt.Sprintf("%s:write", r), + utils.VerbPatch: fmt.Sprintf("%s:write", r), + utils.VerbDelete: fmt.Sprintf("%s:delete", r), + utils.VerbDeleteCollection: fmt.Sprintf("%s:delete", r), + } + } + for _, o := range opts { if o.Unchecked == nil { o.Unchecked = map[string]bool{} } + if o.Mapping == nil { - o.Mapping = map[string]string{} + o.Mapping = defaultMapping(o.Resource) } + stored[o.Resource] = o } @@ -107,7 +124,7 @@ func (c *LegacyAccessClient) HasAccess(ctx context.Context, id claims.AuthInfo, } else { eval = EvalPermission(action, fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, req.Name)) } - } else if req.Verb == "list" { + } else if req.Verb == utils.VerbList { // For list request we need to filter out in storage layer. eval = EvalPermission(action) } else { diff --git a/pkg/services/accesscontrol/authorizer_test.go b/pkg/services/accesscontrol/authorizer_test.go index d602d105cf2..5d7e7f28f71 100644 --- a/pkg/services/accesscontrol/authorizer_test.go +++ b/pkg/services/accesscontrol/authorizer_test.go @@ -14,14 +14,11 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" ) -func TestResourceAuthorizer_HasAccess(t *testing.T) { +func TestLegacyAccessClient_HasAccess(t *testing.T) { ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) - t.Run("should have no opinion for non resource requests", func(t *testing.T) { - a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{ - Resource: "dashboards", - Attr: "uid", - }) + t.Run("should reject when when no configuration for resource exist", func(t *testing.T) { + a := accesscontrol.NewLegacyAccessClient(ac) ok, err := a.HasAccess(context.Background(), &identity.StaticRequester{}, claims.AccessRequest{ Verb: "get", @@ -29,7 +26,7 @@ func TestResourceAuthorizer_HasAccess(t *testing.T) { Namespace: "default", Name: "1", }) - assert.Error(t, err) + assert.NoError(t, err) assert.Equal(t, false, ok) }) From ce8c42ab35487b4087319ac7ecec5df8f4548c8d Mon Sep 17 00:00:00 2001 From: xiyu95 Date: Fri, 27 Sep 2024 07:01:18 -0700 Subject: [PATCH 035/174] chore: enforce the validationMessageHorizontalOverflow for InlineField (#93858) chore: enforce the validationMessageHorizontalOverflow prop for inlineField --- .../src/components/Forms/InlineField.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Forms/InlineField.tsx b/packages/grafana-ui/src/components/Forms/InlineField.tsx index 8750e300122..2eb4b5c1f25 100644 --- a/packages/grafana-ui/src/components/Forms/InlineField.tsx +++ b/packages/grafana-ui/src/components/Forms/InlineField.tsx @@ -45,6 +45,7 @@ export const InlineField = ({ error, transparent, interactive, + validationMessageHorizontalOverflow, ...htmlProps }: Props) => { const theme = useTheme2(); @@ -72,7 +73,11 @@ export const InlineField = ({
{cloneElement(children, { invalid, disabled, loading })} {invalid && error && ( -
+
{error}
)} @@ -100,5 +105,13 @@ const getStyles = (theme: GrafanaTheme2, grow?: boolean, shrink?: boolean) => { fieldValidationWrapper: css({ marginTop: theme.spacing(0.5), }), + validationMessageHorizontalOverflow: css({ + width: 0, + overflowX: 'visible', + + '& > *': { + whiteSpace: 'nowrap', + }, + }), }; }; From 0e4c90dd875eeb25fdc2417a32a5b3c595971353 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:16:12 +0100 Subject: [PATCH 036/174] Update emotion monorepo (#93914) * Update emotion monorepo * add @emotion/serialize package + update unit test --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison --- package.json | 6 +- packages/grafana-flamegraph/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 4 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 5 +- .../Layout/utils/responsiveness.tsx | 2 +- .../src/themes/GlobalStyles/utilityClasses.ts | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../VariableQueryEditor.test.tsx.snap | 6 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 199 ++++++++++-------- 21 files changed, 138 insertions(+), 114 deletions(-) diff --git a/package.json b/package.json index f02237ac485..ad06ee5f1fa 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@betterer/cli": "5.4.0", "@betterer/eslint": "5.4.0", "@cypress/webpack-preprocessor": "6.0.2", - "@emotion/eslint-plugin": "11.11.0", + "@emotion/eslint-plugin": "11.12.0", "@grafana/eslint-config": "7.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", "@grafana/plugin-e2e": "1.8.1", @@ -246,8 +246,8 @@ "yargs": "^17.5.1" }, "dependencies": { - "@emotion/css": "11.11.2", - "@emotion/react": "11.11.4", + "@emotion/css": "11.13.0", + "@emotion/react": "11.13.3", "@fingerprintjs/fingerprintjs": "^3.4.2", "@floating-ui/react": "0.26.24", "@formatjs/intl-durationformat": "^0.2.4", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index e8daf20dabd..29f945b8d28 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -43,7 +43,7 @@ "not IE 11" ], "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@grafana/data": "11.3.0-pre", "@grafana/ui": "11.3.0-pre", "@leeoniya/ufuzzy": "1.0.14", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index f6c5c6cb8ca..df4acb26060 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -17,7 +17,7 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit" }, "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@grafana/data": "11.3.0-pre", "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/experimental": "1.8.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 573cf60102a..5833a424a3d 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -36,7 +36,7 @@ "postpack": "mv package.json.bak package.json" }, "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@floating-ui/react": "0.26.24", "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.8.0", @@ -75,7 +75,7 @@ "whatwg-fetch": "3.6.20" }, "devDependencies": { - "@emotion/eslint-plugin": "11.11.0", + "@emotion/eslint-plugin": "11.12.0", "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-image": "3.0.3", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 409e725d56e..854d02f8ac6 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -14,7 +14,7 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit" }, "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@grafana/data": "11.3.0-pre", "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/experimental": "1.8.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 7499d50b06b..0dd2d285a57 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -47,8 +47,9 @@ "not IE 11" ], "dependencies": { - "@emotion/css": "11.11.2", - "@emotion/react": "11.11.4", + "@emotion/css": "11.13.0", + "@emotion/react": "11.13.3", + "@emotion/serialize": "1.3.2", "@floating-ui/react": "0.26.24", "@grafana/data": "11.3.0-pre", "@grafana/e2e-selectors": "11.3.0-pre", diff --git a/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx b/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx index 8c3a5a632a6..e1f1e5258e2 100644 --- a/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx +++ b/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx @@ -1,4 +1,4 @@ -import { CSSInterpolation } from '@emotion/css'; +import { CSSInterpolation } from '@emotion/serialize'; import { GrafanaTheme2, ThemeBreakpointsKey } from '@grafana/data'; diff --git a/packages/grafana-ui/src/themes/GlobalStyles/utilityClasses.ts b/packages/grafana-ui/src/themes/GlobalStyles/utilityClasses.ts index c1f392c096c..4d52642a762 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/utilityClasses.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/utilityClasses.ts @@ -1,5 +1,5 @@ -import { CSSInterpolation } from '@emotion/css'; import { css } from '@emotion/react'; +import { CSSInterpolation } from '@emotion/serialize'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 4eb8cb54aa4..6ffbd08797f 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -4,7 +4,7 @@ "private": true, "version": "11.3.0-pre", "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.8.0", "@grafana/runtime": "11.3.0-pre", diff --git a/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap b/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap index 0f342bc4bf9..59a0857a532 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap @@ -18,7 +18,7 @@ exports[`VariableQueryEditor renders correctly 1`] = `
Loading...
=16.8.0" peerDependenciesMeta: "@types/react": optional: true - checksum: 10/e7da3a1ddc1d72a4179010bdfd17423c13b1a77bf83a8b18271e919fd382d08c62dc2313ed5347acfd1ef85bb1bae8932597647a986e8a1ea1462552716cd495 + checksum: 10/ee70d3afc2e8dd771e6fe176d27dd87a5e21a54e54d871438fd1caa5aa2312d848c6866292fdc65a6ea1c945147c8422bda2d22ed739178af9902dc86d6b298a + languageName: node + linkType: hard + +"@emotion/serialize@npm:1.3.2, @emotion/serialize@npm:^1.1.2, @emotion/serialize@npm:^1.2.0, @emotion/serialize@npm:^1.3.0, @emotion/serialize@npm:^1.3.1": + version: 1.3.2 + resolution: "@emotion/serialize@npm:1.3.2" + dependencies: + "@emotion/hash": "npm:^0.9.2" + "@emotion/memoize": "npm:^0.9.0" + "@emotion/unitless": "npm:^0.10.0" + "@emotion/utils": "npm:^1.4.1" + csstype: "npm:^3.0.2" + checksum: 10/ead557c1ff19d917ef8169c02738ef36f0851fbfdf0bf69a543045bddea3b7281dc8252ee466cc5fb44ed27d1e61280ff943bb60a2c04158751fb07b3457cc93 languageName: node linkType: hard @@ -2100,19 +2135,6 @@ __metadata: languageName: node linkType: hard -"@emotion/serialize@npm:^1.1.2, @emotion/serialize@npm:^1.1.3": - version: 1.1.3 - resolution: "@emotion/serialize@npm:1.1.3" - dependencies: - "@emotion/hash": "npm:^0.9.1" - "@emotion/memoize": "npm:^0.8.1" - "@emotion/unitless": "npm:^0.8.1" - "@emotion/utils": "npm:^1.2.1" - csstype: "npm:^3.0.2" - checksum: 10/48d88923663273ae70359bc1a1f30454136716cbe0ddd9664be08e257ce56acedab911f125b627627358e37c9f450bbac3ea09b534ef42f9f67325d47b1e2a7b - languageName: node - linkType: hard - "@emotion/sheet@npm:0.9.4": version: 0.9.4 resolution: "@emotion/sheet@npm:0.9.4" @@ -2120,10 +2142,10 @@ __metadata: languageName: node linkType: hard -"@emotion/sheet@npm:^1.2.2": - version: 1.2.2 - resolution: "@emotion/sheet@npm:1.2.2" - checksum: 10/cc46b20ef7273dc28de889927ae1498f854be2890905745fcc3154fbbacaa54df1e28c3d89ff3339c2022782c78933f51955bb950d105d5a219576db1eadfb7a +"@emotion/sheet@npm:^1.2.2, @emotion/sheet@npm:^1.4.0": + version: 1.4.0 + resolution: "@emotion/sheet@npm:1.4.0" + checksum: 10/8ac6e9bf6b373a648f26ae7f1c24041038524f4c72f436f4f8c4761c665e58880c3229d8d89b1f7a4815dd8e5b49634d03e60187cb6f93097d7f7c1859e869d5 languageName: node linkType: hard @@ -2141,19 +2163,19 @@ __metadata: languageName: node linkType: hard -"@emotion/unitless@npm:^0.8.1": - version: 0.8.1 - resolution: "@emotion/unitless@npm:0.8.1" - checksum: 10/918f73c46ac0b7161e3c341cc07d651ce87e31ab1695e74b12adb7da6bb98dfbff8c69cf68a4e40d9eb3d820ca055dc1267aeb3007927ce88f98b885bf729b63 +"@emotion/unitless@npm:^0.10.0": + version: 0.10.0 + resolution: "@emotion/unitless@npm:0.10.0" + checksum: 10/6851c16edce01c494305f43b2cad7a26b939a821131b7c354e49b8e3b012c8810024755b0f4a03ef51117750309e55339825a97bd10411fb3687e68904769106 languageName: node linkType: hard -"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.1": - version: 1.0.1 - resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.1" +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.1, @emotion/use-insertion-effect-with-fallbacks@npm:^1.1.0": + version: 1.1.0 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.1.0" peerDependencies: react: ">=16.8.0" - checksum: 10/7d7ead9ba3f615510f550aea67815281ec5a5487de55aafc250f820317afc1fd419bd9e9e27602a0206ec5c152f13dc6130bccad312c1036706c584c65d66ef7 + checksum: 10/33a10f44a873b3f5ccd2a1a3d13c2f34ed628f5a2be1ccf28540a86535a14d3a930afcbef209d48346a22ec60ff48f43c86ee9c846b9480d23a55a17145da66c languageName: node linkType: hard @@ -2164,10 +2186,10 @@ __metadata: languageName: node linkType: hard -"@emotion/utils@npm:^1.2.1": - version: 1.2.1 - resolution: "@emotion/utils@npm:1.2.1" - checksum: 10/472fa529c64a13edff80aa11698092e8841c1ffb5001c739d84eb9d0fdd6d8e1cd1848669310578ccfa6383b8601132eca54f8749fca40af85d21fdfc9b776c4 +"@emotion/utils@npm:^1.2.1, @emotion/utils@npm:^1.4.0, @emotion/utils@npm:^1.4.1": + version: 1.4.1 + resolution: "@emotion/utils@npm:1.4.1" + checksum: 10/95e56fc0c9e05cf01a96268f0486ce813f1109a8653d2f575c67df9e8765d9c1b2daf09ad1ada67d933efbb08ca7990228e14b210c713daf90156b4869abe6a7 languageName: node linkType: hard @@ -2178,10 +2200,10 @@ __metadata: languageName: node linkType: hard -"@emotion/weak-memoize@npm:^0.3.1": - version: 0.3.1 - resolution: "@emotion/weak-memoize@npm:0.3.1" - checksum: 10/b2be47caa24a8122622ea18cd2d650dbb4f8ad37b636dc41ed420c2e082f7f1e564ecdea68122b546df7f305b159bf5ab9ffee872abd0f052e687428459af594 +"@emotion/weak-memoize@npm:^0.4.0": + version: 0.4.0 + resolution: "@emotion/weak-memoize@npm:0.4.0" + checksum: 10/db5da0e89bd752c78b6bd65a1e56231f0abebe2f71c0bd8fc47dff96408f7065b02e214080f99924f6a3bfe7ee15afc48dad999d76df86b39b16e513f7a94f52 languageName: node linkType: hard @@ -3117,7 +3139,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/grafana-azure-monitor-datasource@workspace:public/app/plugins/datasource/azuremonitor" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" "@grafana/experimental": "npm:1.8.0" @@ -3161,7 +3183,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/grafana-postgresql-datasource@workspace:public/app/plugins/datasource/grafana-postgresql-datasource" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" "@grafana/experimental": "npm:1.8.0" @@ -3192,7 +3214,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/grafana-pyroscope-datasource@workspace:public/app/plugins/datasource/grafana-pyroscope-datasource" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/plugin-configs": "npm:11.3.0-pre" "@grafana/runtime": "npm:11.3.0-pre" @@ -3233,7 +3255,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/grafana-testdata-datasource@workspace:public/app/plugins/datasource/grafana-testdata-datasource" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" "@grafana/experimental": "npm:1.8.0" @@ -3274,7 +3296,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/jaeger@workspace:public/app/plugins/datasource/jaeger" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" "@grafana/experimental": "npm:1.8.0" @@ -3316,7 +3338,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/mssql@workspace:public/app/plugins/datasource/mssql" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" "@grafana/experimental": "npm:1.8.0" @@ -3347,7 +3369,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/mysql@workspace:public/app/plugins/datasource/mysql" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" "@grafana/experimental": "npm:1.8.0" @@ -3378,7 +3400,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/plugin-configs": "npm:11.3.0-pre" "@grafana/runtime": "npm:11.3.0-pre" @@ -3410,7 +3432,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/stackdriver@workspace:public/app/plugins/datasource/cloud-monitoring" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" "@grafana/experimental": "npm:1.8.0" @@ -3458,7 +3480,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/tempo@workspace:public/app/plugins/datasource/tempo" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" "@grafana/experimental": "npm:1.8.0" @@ -3518,7 +3540,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/zipkin@workspace:public/app/plugins/datasource/zipkin" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" "@grafana/experimental": "npm:1.8.0" @@ -3791,7 +3813,7 @@ __metadata: "@babel/core": "npm:7.25.2" "@babel/preset-env": "npm:7.25.4" "@babel/preset-react": "npm:7.24.7" - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@grafana/ui": "npm:11.3.0-pre" @@ -3875,7 +3897,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/o11y-ds-frontend@workspace:packages/grafana-o11y-ds-frontend" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" "@grafana/experimental": "npm:1.8.0" @@ -3944,8 +3966,8 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/prometheus@workspace:packages/grafana-prometheus" dependencies: - "@emotion/css": "npm:11.11.2" - "@emotion/eslint-plugin": "npm:11.11.0" + "@emotion/css": "npm:11.13.0" + "@emotion/eslint-plugin": "npm:11.12.0" "@floating-ui/react": "npm:0.26.24" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" @@ -4173,7 +4195,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/sql@workspace:packages/grafana-sql" dependencies: - "@emotion/css": "npm:11.11.2" + "@emotion/css": "npm:11.13.0" "@grafana/data": "npm:11.3.0-pre" "@grafana/e2e-selectors": "npm:11.3.0-pre" "@grafana/experimental": "npm:1.8.0" @@ -4234,8 +4256,9 @@ __metadata: resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: "@babel/core": "npm:7.25.2" - "@emotion/css": "npm:11.11.2" - "@emotion/react": "npm:11.11.4" + "@emotion/css": "npm:11.13.0" + "@emotion/react": "npm:11.13.3" + "@emotion/serialize": "npm:1.3.2" "@faker-js/faker": "npm:^8.4.1" "@floating-ui/react": "npm:0.26.24" "@grafana/data": "npm:11.3.0-pre" @@ -11169,7 +11192,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^5.58.0": +"@typescript-eslint/utils@npm:^5.25.0, @typescript-eslint/utils@npm:^5.58.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" dependencies: @@ -18907,9 +18930,9 @@ __metadata: "@betterer/cli": "npm:5.4.0" "@betterer/eslint": "npm:5.4.0" "@cypress/webpack-preprocessor": "npm:6.0.2" - "@emotion/css": "npm:11.11.2" - "@emotion/eslint-plugin": "npm:11.11.0" - "@emotion/react": "npm:11.11.4" + "@emotion/css": "npm:11.13.0" + "@emotion/eslint-plugin": "npm:11.12.0" + "@emotion/react": "npm:11.13.3" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@floating-ui/react": "npm:0.26.24" "@formatjs/intl-durationformat": "npm:^0.2.4" From bc6752a51c3114f403368bd4301b3bb3c785eab1 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 27 Sep 2024 15:20:45 +0100 Subject: [PATCH 037/174] SingleTopNav: Add "Grafana" header to MegaMenu (#93798) * add "Grafana" header to MegaMenu * add truncation for really long custom app titles * revert padding change since paddingLeft will handle it --- .../components/AppChrome/AppChromeMenu.tsx | 4 +- .../AppChrome/MegaMenu/MegaMenu.tsx | 31 ++++--- .../AppChrome/MegaMenu/MegaMenuHeader.tsx | 85 +++++++++++++++++++ .../AppChrome/TopBar/SingleTopBar.tsx | 17 ++-- 4 files changed, 119 insertions(+), 18 deletions(-) create mode 100644 public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx diff --git a/public/app/core/components/AppChrome/AppChromeMenu.tsx b/public/app/core/components/AppChrome/AppChromeMenu.tsx index 7926e41aae9..cf02b6a7c48 100644 --- a/public/app/core/components/AppChrome/AppChromeMenu.tsx +++ b/public/app/core/components/AppChrome/AppChromeMenu.tsx @@ -91,7 +91,7 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { left: 0, position: 'fixed', right: 0, - top: searchBarHidden ? 0 : TOP_BAR_LEVEL_HEIGHT, + top: searchBarHidden || config.featureToggles.singleTopNav ? 0 : TOP_BAR_LEVEL_HEIGHT, zIndex: theme.zIndex.modalBackdrop, [theme.breakpoints.up('md')]: { @@ -107,7 +107,7 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { // Needs to below navbar should we change the navbarFixed? add add a new level? zIndex: theme.zIndex.modal, position: 'fixed', - top: searchBarHidden ? 0 : TOP_BAR_LEVEL_HEIGHT, + top: searchBarHidden || config.featureToggles.singleTopNav ? 0 : TOP_BAR_LEVEL_HEIGHT, backgroundColor: theme.colors.background.primary, flex: '1 1 0', diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index d7deaaeb3cd..b2be2f189b1 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -13,6 +13,7 @@ import { setBookmark } from 'app/core/reducers/navBarTree'; import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index'; import { useDispatch, useSelector } from 'app/types'; +import { MegaMenuHeader } from './MegaMenuHeader'; import { MegaMenuItem } from './MegaMenuItem'; import { usePinnedItems } from './hooks'; import { enrichWithInteractionTracking, findByUrl, getActiveItem } from './utils'; @@ -62,6 +63,10 @@ export const MegaMenu = memo( const activeItem = getActiveItem(navItems, state.sectionNav.node, location.pathname); + const handleMegaMenu = () => { + chrome.setMegaMenuOpen(!state.megaMenuOpen); + }; + const handleDockedMenu = () => { chrome.setMegaMenuDocked(!state.megaMenuDocked); if (state.megaMenuDocked) { @@ -109,22 +114,26 @@ export const MegaMenu = memo( return (
-
- - -
+ {config.featureToggles.singleTopNav ? ( + + ) : ( +
+ + +
+ )}
diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index ec7296fcb73..ce83432e202 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -24,16 +24,7 @@ const charWidth = measureText('M', UPLOT_AXIS_FONT_SIZE).width; const toRads = Math.PI / 180; export const BarChartPanel = (props: PanelProps) => { - const { - data, - options, - fieldConfig, - width, - height, - timeZone, - id, - // replaceVariables - } = props; + const { data, options, fieldConfig, width, height, timeZone, id, replaceVariables } = props; // will need this if joining on time to re-create data links // const { dataLinkPostProcessor } = usePanelContext(); @@ -177,6 +168,7 @@ export const BarChartPanel = (props: PanelProps) => { sortOrder={options.tooltip.sort} isPinned={isPinned} maxHeight={options.tooltip.maxHeight} + replaceVariables={replaceVariables} /> ); }} diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 319b127b227..acddd0a5727 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -306,6 +306,7 @@ export const CandlestickPanel = ({ isPinned={isPinned} annotate={enableAnnotationCreation ? annotate : undefined} maxHeight={options.tooltip.maxHeight} + replaceVariables={replaceVariables} /> ); }} diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index a3a9896b251..06042a4080e 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -219,6 +219,7 @@ export const HeatmapPanel = ({ annotate={enableAnnotationCreation ? annotate : undefined} maxHeight={options.tooltip.maxHeight} maxWidth={options.tooltip.maxWidth} + replaceVariables={replaceVariables} /> ); }} diff --git a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx index 924c287fd23..010fbdfa09c 100644 --- a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx @@ -3,11 +3,13 @@ import * as React from 'react'; import uPlot from 'uplot'; import { + ActionModel, DataFrameType, Field, FieldType, formattedValueToString, getFieldDisplayName, + InterpolateFunction, LinkModel, PanelData, } from '@grafana/data'; @@ -16,14 +18,14 @@ import { TooltipDisplayMode, useTheme2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; +import { VizTooltipWrapper } from '@grafana/ui/src/components/VizTooltip/VizTooltipWrapper'; import { ColorIndicator, ColorPlacement, VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; -import { VizTooltipWrapper } from '../../../../../packages/grafana-ui/src/components/VizTooltip/VizTooltipWrapper'; -import { getDataLinks } from '../status-history/utils'; +import { getDataLinks, getFieldActions } from '../status-history/utils'; import { isTooltipScrollable } from '../timeseries/utils'; import { HeatmapData } from './fields'; @@ -43,6 +45,7 @@ interface HeatmapTooltipProps { annotate?: () => void; maxHeight?: number; maxWidth?: number; + replaceVariables: InterpolateFunction; } export const HeatmapTooltip = (props: HeatmapTooltipProps) => { @@ -73,6 +76,7 @@ const HeatmapHoverCell = ({ annotate, maxHeight, maxWidth, + replaceVariables, }: HeatmapTooltipProps) => { const index = dataIdxs[1]!; const data = dataRef.current; @@ -291,6 +295,7 @@ const HeatmapHoverCell = ({ if (isPinned) { let links: Array> = []; + let actions: Array> = []; const linksField = data.series?.fields[yValueIdx + 1]; @@ -301,9 +306,11 @@ const HeatmapHoverCell = ({ if (visible && hasLinks) { links = getDataLinks(linksField, xValueIdx); } + + actions = getFieldActions(data.series!, linksField, replaceVariables); } - footer = ; + footer = ; } let can = useRef(null); diff --git a/public/app/plugins/panel/histogram/HistogramTooltip.tsx b/public/app/plugins/panel/histogram/HistogramTooltip.tsx index 4fee3eb5c11..53bfd3bcfc6 100644 --- a/public/app/plugins/panel/histogram/HistogramTooltip.tsx +++ b/public/app/plugins/panel/histogram/HistogramTooltip.tsx @@ -5,10 +5,10 @@ import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/c import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; +import { VizTooltipWrapper } from '@grafana/ui/src/components/VizTooltip/VizTooltipWrapper'; import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; -import { VizTooltipWrapper } from '../../../../../packages/grafana-ui/src/components/VizTooltip/VizTooltipWrapper'; import { getDataLinks } from '../status-history/utils'; import { isTooltipScrollable } from '../timeseries/utils'; diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 202de0f1035..df2df2c811c 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -198,6 +198,7 @@ export const StateTimelinePanel = ({ annotate={enableAnnotationCreation ? annotate : undefined} withDuration={true} maxHeight={options.tooltip.maxHeight} + replaceVariables={replaceVariables} /> ); }} diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index efb1d4ff1db..b4ff1a07c21 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -6,12 +6,12 @@ import { TooltipDisplayMode } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; +import { VizTooltipWrapper } from '@grafana/ui/src/components/VizTooltip/VizTooltipWrapper'; import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils'; -import { VizTooltipWrapper } from '../../../../../packages/grafana-ui/src/components/VizTooltip/VizTooltipWrapper'; -import { getDataLinks } from '../status-history/utils'; +import { getDataLinks, getFieldActions } from '../status-history/utils'; import { TimeSeriesTooltipProps } from '../timeseries/TimeSeriesTooltip'; import { isTooltipScrollable } from '../timeseries/utils'; @@ -31,6 +31,7 @@ export const StateTimelineTooltip2 = ({ timeRange, withDuration, maxHeight, + replaceVariables, }: StateTimelineTooltip2Props) => { const xField = series.fields[0]; @@ -70,8 +71,9 @@ export const StateTimelineTooltip2 = ({ const field = series.fields[seriesIdx]; const dataIdx = dataIdxs[seriesIdx]!; const links = getDataLinks(field, dataIdx); + const actions = getFieldActions(series, field, replaceVariables!); - footer = ; + footer = ; } const headerItem: VizTooltipItem = { diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 475ad75556f..5f09c96ca52 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -130,6 +130,7 @@ export const StatusHistoryPanel = ({ annotate={enableAnnotationCreation ? annotate : undefined} withDuration={false} maxHeight={options.tooltip.maxHeight} + replaceVariables={replaceVariables} /> ); }} diff --git a/public/app/plugins/panel/status-history/utils.ts b/public/app/plugins/panel/status-history/utils.ts index f4775386ce1..b5807d0b2cb 100644 --- a/public/app/plugins/panel/status-history/utils.ts +++ b/public/app/plugins/panel/status-history/utils.ts @@ -1,4 +1,7 @@ -import { Field, LinkModel } from '@grafana/data'; +import { ActionModel, Field, InterpolateFunction, LinkModel } from '@grafana/data'; +import { DataFrame } from '@grafana/data/'; +import { config } from '@grafana/runtime'; +import { getActions } from 'app/features/actions/utils'; export const getDataLinks = (field: Field, rowIdx: number) => { const links: Array> = []; @@ -20,3 +23,31 @@ export const getDataLinks = (field: Field, rowIdx: number) => { return links; }; + +export const getFieldActions = (dataFrame: DataFrame, field: Field, replaceVars: InterpolateFunction) => { + if (!config.featureToggles?.vizActions) { + return []; + } + + const actions: Array> = []; + const actionLookup = new Set(); + + const actionsModel = getActions( + dataFrame, + field, + field.state!.scopedVars!, + replaceVars, + field.config.actions ?? [], + {} + ); + + actionsModel.forEach((action) => { + const key = `${action.title}`; + if (!actionLookup.has(key)) { + actions.push(action); + actionLookup.add(key); + } + }); + + return actions; +}; diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 32ec1c7b62a..fb9076f8a01 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -131,6 +131,7 @@ export const TimeSeriesPanel = ({ isPinned={isPinned} annotate={enableAnnotationCreation ? annotate : undefined} maxHeight={options.tooltip.maxHeight} + replaceVariables={replaceVariables} /> ); }} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index eb6faf49742..2c30eb643c8 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -1,16 +1,15 @@ -import { css } from '@emotion/css'; import { ReactNode } from 'react'; -import { DataFrame, Field, FieldType, formattedValueToString } from '@grafana/data'; +import { DataFrame, Field, FieldType, formattedValueToString, InterpolateFunction } from '@grafana/data'; import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; +import { VizTooltipWrapper } from '@grafana/ui/src/components/VizTooltip/VizTooltipWrapper'; import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; -import { VizTooltipWrapper } from '../../../../../packages/grafana-ui/src/components/VizTooltip/VizTooltipWrapper'; -import { getDataLinks } from '../status-history/utils'; +import { getDataLinks, getFieldActions } from '../status-history/utils'; import { fmt } from '../xychart/utils'; import { isTooltipScrollable } from './utils'; @@ -36,6 +35,8 @@ export interface TimeSeriesTooltipProps { annotate?: () => void; maxHeight?: number; + + replaceVariables?: InterpolateFunction; } export const TimeSeriesTooltip = ({ @@ -48,6 +49,7 @@ export const TimeSeriesTooltip = ({ isPinned, annotate, maxHeight, + replaceVariables, }: TimeSeriesTooltipProps) => { const xField = series.fields[0]; const xVal = formattedValueToString(xField.display!(xField.values[dataIdxs[0]!])); @@ -77,8 +79,9 @@ export const TimeSeriesTooltip = ({ const field = series.fields[seriesIdx]; const dataIdx = dataIdxs[seriesIdx]!; const links = getDataLinks(field, dataIdx); + const actions = getFieldActions(series, field, replaceVariables!); - footer = ; + footer = ; } const headerItem: VizTooltipItem | null = xField.config.custom?.hideFrom?.tooltip @@ -101,10 +104,3 @@ export const TimeSeriesTooltip = ({ ); }; - -export const getStyles = () => ({ - wrapper: css({ - display: 'flex', - flexDirection: 'column', - }), -}); diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 757789d7e87..0d7d398ab01 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -130,6 +130,7 @@ export const TrendPanel = ({ sortOrder={options.tooltip.sort} isPinned={isPinned} maxHeight={options.tooltip.maxHeight} + replaceVariables={replaceVariables} /> ); }} diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.tsx index 9ef55c0df9a..b8046935bb3 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.tsx @@ -5,9 +5,9 @@ import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; +import { VizTooltipWrapper } from '@grafana/ui/src/components/VizTooltip/VizTooltipWrapper'; import { ColorIndicator, VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; -import { VizTooltipWrapper } from '../../../../../packages/grafana-ui/src/components/VizTooltip/VizTooltipWrapper'; import { getDataLinks } from '../status-history/utils'; import { Options } from './panelcfg.gen'; diff --git a/public/app/plugins/panel/xychart/v2/XYChartPanel.tsx b/public/app/plugins/panel/xychart/v2/XYChartPanel.tsx index cbc2c80c656..f22fd8248e1 100644 --- a/public/app/plugins/panel/xychart/v2/XYChartPanel.tsx +++ b/public/app/plugins/panel/xychart/v2/XYChartPanel.tsx @@ -122,6 +122,7 @@ export const XYChartPanel2 = (props: Props2) => { dismiss={dismiss} isPinned={isPinned} seriesIdx={seriesIdx!} + replaceVariables={props.replaceVariables} /> ); }} diff --git a/public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx index a680b9d2a67..4a6f590c2f9 100644 --- a/public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx @@ -1,14 +1,14 @@ import { ReactNode } from 'react'; -import { DataFrame } from '@grafana/data'; +import { DataFrame, InterpolateFunction } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; +import { VizTooltipWrapper } from '@grafana/ui/src/components/VizTooltip/VizTooltipWrapper'; import { ColorIndicator, VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; -import { VizTooltipWrapper } from '../../../../../../packages/grafana-ui/src/components/VizTooltip/VizTooltipWrapper'; -import { getDataLinks } from '../../status-history/utils'; +import { getDataLinks, getFieldActions } from '../../status-history/utils'; import { XYSeries } from './types2'; import { fmt } from './utils'; @@ -20,6 +20,7 @@ export interface Props { dismiss: () => void; data: DataFrame[]; xySeries: XYSeries[]; + replaceVariables: InterpolateFunction; } function stripSeriesName(fieldName: string, seriesName: string) { @@ -30,7 +31,7 @@ function stripSeriesName(fieldName: string, seriesName: string) { return fieldName; } -export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, isPinned }: Props) => { +export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, isPinned, replaceVariables }: Props) => { const rowIndex = dataIdxs.find((idx) => idx !== null)!; const series = xySeries[seriesIdx! - 1]; @@ -94,8 +95,10 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, i if (isPinned && seriesIdx != null) { const links = getDataLinks(yField, rowIndex); + const yFieldFrame = data.find((frame) => frame.fields.includes(yField))!; + const actions = getFieldActions(yFieldFrame, yField, replaceVariables); - footer = ; + footer = ; } return ( From 47b51326cc53854ee283a8c63b794b30404ee4fc Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:33:10 -0600 Subject: [PATCH 053/174] VizTooltip: Update datalinks styling (#93950) --- .../components/VizTooltip/VizTooltipFooter.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index ca4ef351874..99428ce1d56 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data'; -import { Button, ButtonProps, DataLinkButton, Stack } from '..'; +import { Button, Stack, TextLink } from '..'; import { useStyles2 } from '../../themes'; import { ActionButton } from '../Actions/ActionButton'; @@ -15,14 +15,19 @@ interface VizTooltipFooterProps { export const ADD_ANNOTATION_ID = 'add-annotation-button'; const renderDataLinks = (dataLinks: LinkModel[]) => { - const buttonProps: ButtonProps = { - variant: 'secondary', - }; - return ( {dataLinks.map((link, i) => ( - + + {link.title} + ))} ); From e23ba32722d40930404b7e32012eb5ec3d875f44 Mon Sep 17 00:00:00 2001 From: jackyin Date: Sat, 28 Sep 2024 22:04:40 +0800 Subject: [PATCH 054/174] Transformations: Fix crash in Config from query results (#93427) --- .../grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx index a2a97b3ef8e..3efd06c618e 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx @@ -62,7 +62,7 @@ export const FieldValueMatcherEditor = ({ options, onChange }: Props) => { ); const opts = options ?? {}; - const isBool = isBooleanReducer(options.reducer); + const isBool = isBooleanReducer(opts.reducer); return (
From fb9e12c106fb7b38405f14b2ec20d1d0b9d32db9 Mon Sep 17 00:00:00 2001 From: Dai Nguyen <88277570+ej25a@users.noreply.github.com> Date: Mon, 30 Sep 2024 02:35:33 -0500 Subject: [PATCH 055/174] Update Okta SAML attributes documentation (#93966) Co-authored-by: Irene Rodriguez --- .../configure-authentication/saml/index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md index bddba3548cd..29ef9f04975 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md @@ -280,13 +280,13 @@ Grafana supports user authentication through Okta, which is useful when you want - In the **Single sign on URL** field, use the `/saml/acs` endpoint URL of your Grafana instance, for example, `https://grafana.example.com/saml/acs`. - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, for example, `https://grafana.example.com/saml/metadata`. - Leave the default values for **Name ID format** and **Application username**. - - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana, for example: + - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana. The attribute names in Okta need to match exactly what is defined within Grafana, for example: - | Attribute name (in Grafana) | Value (in Okta profile) | - | --------------------------- | -------------------------------------- | - | Login | `user.login` | - | Email | `user.email` | - | DisplayName | `user.firstName + " " + user.lastName` | + | Attribute name (in Grafana) | Name and value (in Okta profile) | + | --------------------------- | -------------------------------------------------- | + | Login | Login `user.login` | + | Email | Email `user.email` | + | DisplayName | DisplayName `user.firstName + " " + user.lastName` | - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`) and set filter to `Matches regex .*` to return all user groups. From 369a8a2b5fe87ecdef0973f1f734514be7eb62a1 Mon Sep 17 00:00:00 2001 From: Ryan Crutchfield <30603182+rjcrutch@users.noreply.github.com> Date: Mon, 30 Sep 2024 01:56:55 -0600 Subject: [PATCH 056/174] Docs: Add org mapping feature to generic OAuth (#91365) * Doc fix - Add org mapping feature to generic OAuth https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/generic-oauth/#org-roles-mapping-example * Reviewer correction Added org mapping for all OAuth providers with the exception of GCOM --- .../configure-authentication/_index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md index 72d7cd80eb9..c8590e8c111 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md @@ -22,13 +22,13 @@ The following table shows all supported authentication providers and the feature | Provider | Multi Org Mapping | Enforce Sync | Role Mapping | Grafana Admin Mapping | Team Sync | Allowed groups | Active Sync | Skip OrgRole mapping | Auto Login | Single Logout | | :---------------------------------------------------- | :---------------- | :----------- | :----------- | :-------------------- | :-------- | :------------- | :---------- | :------------------- | :--------- | :------------ | | [Auth Proxy]({{< relref "./auth-proxy" >}}) | no | yes | yes | no | yes | no | N/A | no | N/A | N/A | -| [Azure AD OAuth]({{< relref "./azuread" >}}) | no | yes | yes | yes | yes | yes | N/A | yes | yes | yes | -| [Generic OAuth]({{< relref "./generic-oauth" >}}) | no | yes | yes | yes | yes | no | N/A | yes | yes | yes | -| [GitHub OAuth]({{< relref "./github" >}}) | no | yes | yes | yes | yes | yes | N/A | yes | yes | yes | -| [GitLab OAuth]({{< relref "./gitlab" >}}) | no | yes | yes | yes | yes | yes | N/A | yes | yes | yes | -| [Google OAuth]({{< relref "./google" >}}) | no | no | no | no | yes | no | N/A | no | yes | yes | +| [Azure AD OAuth]({{< relref "./azuread" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [Generic OAuth]({{< relref "./generic-oauth" >}}) | yes | yes | yes | yes | yes | no | N/A | yes | yes | yes | +| [GitHub OAuth]({{< relref "./github" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [GitLab OAuth]({{< relref "./gitlab" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [Google OAuth]({{< relref "./google" >}}) | yes | no | no | no | yes | no | N/A | no | yes | yes | | [Grafana.com OAuth]({{< relref "./grafana-cloud" >}}) | no | no | yes | no | N/A | N/A | N/A | yes | yes | yes | -| [Okta OAuth]({{< relref "./okta" >}}) | no | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [Okta OAuth]({{< relref "./okta" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | | [SAML]({{< relref "./saml" >}}) (Enterprise only) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | | [LDAP]({{< relref "./ldap" >}}) | yes | yes | yes | yes | yes | yes | yes | no | N/A | N/A | | [JWT Proxy]({{< relref "./jwt" >}}) | no | yes | yes | yes | no | no | N/A | no | N/A | N/A | From 8b3615b57621c1540fe8e75e9683a36ce931d0e0 Mon Sep 17 00:00:00 2001 From: Dai Nguyen <88277570+ej25a@users.noreply.github.com> Date: Mon, 30 Sep 2024 02:58:52 -0500 Subject: [PATCH 057/174] Update Image Render Dependencies (#93959) --- .../image-rendering/troubleshooting/index.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md index ece628061d3..c5ceb9e07dd 100644 --- a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md +++ b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md @@ -97,6 +97,14 @@ On a minimal CentOS 8 installation, the following dependencies are required for libXcomposite libXdamage libXtst cups libXScrnSaver pango atk adwaita-cursor-theme adwaita-icon-theme at at-spi2-atk at-spi2-core cairo-gobject colord-libs dconf desktop-file-utils ed emacs-filesystem gdk-pixbuf2 glib-networking gnutls gsettings-desktop-schemas gtk-update-icon-cache gtk3 hicolor-icon-theme jasper-libs json-glib libappindicator-gtk3 libdbusmenu libdbusmenu-gtk3 libepoxy liberation-fonts liberation-narrow-fonts liberation-sans-fonts liberation-serif-fonts libgusb libindicator-gtk3 libmodman libproxy libsoup libwayland-cursor libwayland-egl libxkbcommon m4 mailx nettle patch psmisc redhat-lsb-core redhat-lsb-submod-security rest spax time trousers xdg-utils xkeyboard-config alsa-lib libX11-xcb ``` +**RHEL:** + +On a minimal RHEL 8 installation, the following dependencies are required for the image rendering to function: + +```bash +linux-vdso.so.1 libdl.so.2 libpthread.so.0 libgobject-2.0.so.0 libglib-2.0.so.0 libnss3.so libnssutil3.so libsmime3.so libnspr4.so libatk-1.0.so.0 libatk-bridge-2.0.so.0 libcups.so.2 libgio-2.0.so.0 libdrm.so.2 libdbus-1.so.3 libexpat.so.1 libxcb.so.1 libxkbcommon.so.0 libm.so.6 libX11.so.6 libXcomposite.so.1 libXdamage.so.1 libXext.so.6 libXfixes.so.3 libXrandr.so.2 libgbm.so.1 libpango-1.0.so.0 libcairo.so.2 libasound.so.2 libatspi.so.0 libgcc_s.so.1 libc.so.6 /lib64/ld-linux-x86-64.so.2 libgnutls.so.30 libpcre.so.1 libffi.so.6 libplc4.so libplds4.so librt.so.1 libgmodule-2.0.so.0 libgssapi_krb5.so.2 libkrb5.so.3 libk5crypto.so.3 libcom_err.so.2 libavahi-common.so.3 libavahi-client.so.3 libcrypt.so.1 libz.so.1 libselinux.so.1 libresolv.so.2 libmount.so.1 libsystemd.so.0 libXau.so.6 libXrender.so.1 libthai.so.0 libfribidi.so.0 libpixman-1.so.0 libfontconfig.so.1 libpng16.so.16 libxcb-render.so.0 libidn2.so.0 libunistring.so.2 libtasn1.so.6 libnettle.so.6 libhogweed.so.4 libgmp.so.10 libkrb5support.so.0 libkeyutils.so.1 libpcre2-8.so.0 libuuid.so.1 liblz4.so.1 libgcrypt.so.20 libbz2.so.1 +``` + ## Certificate signed by internal certificate authorities In many cases, Grafana runs on internal servers and uses certificates that have not been signed by a CA ([Certificate Authority](https://en.wikipedia.org/wiki/Certificate_authority)) known to Chrome, and therefore cannot be validated. Chrome internally uses NSS ([Network Security Services](https://en.wikipedia.org/wiki/Network_Security_Services)) for cryptographic operations such as the validation of certificates. From 1aed1d801736ce924cca6dbec0de0150e6c8f53d Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Mon, 30 Sep 2024 10:06:54 +0200 Subject: [PATCH 058/174] Chore: Bump webpack for e2e test plugins (#93831) chore(e2e-plugins): bump version of webpack to silence dependabot --- .../grafana-extensionstest-app/package.json | 2 +- yarn.lock | 52 ++----------------- 2 files changed, 4 insertions(+), 50 deletions(-) diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e/test-plugins/grafana-extensionstest-app/package.json index 0da20ae736b..f7577e54dd5 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e/test-plugins/grafana-extensionstest-app/package.json @@ -23,7 +23,7 @@ "glob": "10.4.1", "ts-node": "10.9.2", "typescript": "5.5.4", - "webpack": "5.91.0", + "webpack": "5.95.0", "webpack-merge": "5.10.0" }, "engines": { diff --git a/yarn.lock b/yarn.lock index 886d48af577..f7d85e7980c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9204,7 +9204,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.6.3" typescript: "npm:5.5.4" - webpack: "npm:5.91.0" + webpack: "npm:5.95.0" webpack-merge: "npm:5.10.0" peerDependencies: "@grafana/runtime": "*" @@ -9897,7 +9897,7 @@ __metadata: languageName: node linkType: hard -"@types/eslint-scope@npm:^3.7.3, @types/eslint-scope@npm:^3.7.7": +"@types/eslint-scope@npm:^3.7.7": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" dependencies: @@ -11746,15 +11746,6 @@ __metadata: languageName: node linkType: hard -"acorn-import-assertions@npm:^1.9.0": - version: 1.9.0 - resolution: "acorn-import-assertions@npm:1.9.0" - peerDependencies: - acorn: ^8 - checksum: 10/af8dd58f6b0c6a43e85849744534b99f2133835c6fcdabda9eea27d0a0da625a0d323c4793ba7cb25cf4507609d0f747c210ccc2fc9b5866de04b0e59c9c5617 - languageName: node - linkType: hard - "acorn-import-attributes@npm:^1.9.5": version: 1.9.5 resolution: "acorn-import-attributes@npm:1.9.5" @@ -16239,7 +16230,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.16.0, enhanced-resolve@npm:^5.17.1": +"enhanced-resolve@npm:^5.17.1": version: 5.17.1 resolution: "enhanced-resolve@npm:5.17.1" dependencies: @@ -33059,43 +33050,6 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5.91.0": - version: 5.91.0 - resolution: "webpack@npm:5.91.0" - dependencies: - "@types/eslint-scope": "npm:^3.7.3" - "@types/estree": "npm:^1.0.5" - "@webassemblyjs/ast": "npm:^1.12.1" - "@webassemblyjs/wasm-edit": "npm:^1.12.1" - "@webassemblyjs/wasm-parser": "npm:^1.12.1" - acorn: "npm:^8.7.1" - acorn-import-assertions: "npm:^1.9.0" - browserslist: "npm:^4.21.10" - chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.16.0" - es-module-lexer: "npm:^1.2.1" - eslint-scope: "npm:5.1.1" - events: "npm:^3.2.0" - glob-to-regexp: "npm:^0.4.1" - graceful-fs: "npm:^4.2.11" - json-parse-even-better-errors: "npm:^2.3.1" - loader-runner: "npm:^4.2.0" - mime-types: "npm:^2.1.27" - neo-async: "npm:^2.6.2" - schema-utils: "npm:^3.2.0" - tapable: "npm:^2.1.1" - terser-webpack-plugin: "npm:^5.3.10" - watchpack: "npm:^2.4.1" - webpack-sources: "npm:^3.2.3" - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 10/647ca53c15fe0fa1af4396a7257d7a93cbea648d2685e565a11cc822a9e3ea9316345250987d75f02c0b45dae118814f094ec81908d1032e77a33cd6470b289e - languageName: node - linkType: hard - "webpackbar@npm:^6.0.0": version: 6.0.1 resolution: "webpackbar@npm:6.0.1" From e4698d9c52b427da08172c0b7acf42620ea4156e Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Mon, 30 Sep 2024 09:19:01 +0100 Subject: [PATCH 059/174] Alerting: Fix eval interval not being saved when creating a new group (#93821) --- .../unified/RuleEditorExisting.test.tsx | 103 ++++++++++-------- .../components/rule-editor/FolderAndGroup.tsx | 41 +++---- .../rule-editor/GrafanaEvaluationBehavior.tsx | 19 ++-- .../alert-rule-form/AlertRuleForm.tsx | 7 +- .../SimplifiedRuleEditor.test.tsx | 4 +- .../SimplifiedRuleEditor.test.tsx.snap | 44 +++++++- .../ruleGroup/useUpsertRuleFromRuleGroup.ts | 10 +- .../unified/mocks/server/configure.ts | 8 ++ .../unified/mocks/server/handlers/search.ts | 4 +- 9 files changed, 148 insertions(+), 92 deletions(-) diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx index a3aeec38bdc..6f46fdb4ab1 100644 --- a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx @@ -3,44 +3,25 @@ import { ui } from 'test/helpers/alertingRuleEditor'; import { render, screen } from 'test/test-utils'; import { contextSrv } from 'app/core/services/context_srv'; -import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types'; +import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/configure'; +import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; +import { DashboardSearchItemType } from 'app/features/search/types'; -import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions'; -import { backendSrv } from '../../../core/services/backend_srv'; import { AccessControlAction } from '../../../types'; import RuleEditor from './RuleEditor'; -import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; import { setupMswServer } from './mockApi'; import { grantUserPermissions, mockDataSource, mockFolder } from './mocks'; import { grafanaRulerRule } from './mocks/grafanaRulerApi'; import { setupDataSources } from './testSetup/datasources'; import { Annotation } from './utils/constants'; -jest.mock('./components/rule-editor/ExpressionEditor', () => ({ - ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( - onChange(e.target.value)} /> - ), -})); - jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, })); -jest.mock('../../../../app/features/manage-dashboards/state/actions'); - -// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. -// lets just skip it -jest.mock('app/features/query/components/QueryEditorRow', () => ({ - QueryEditorRow: () =>

hi

, -})); - jest.setTimeout(60 * 1000); -const mocks = { - searchFolders: jest.mocked(searchFolders), -}; - setupMswServer(); function renderRuleEditor(identifier: string) { @@ -55,6 +36,24 @@ function renderRuleEditor(identifier: string) { } describe('RuleEditor grafana managed rules', () => { + const folder = { + title: 'Folder A', + uid: grafanaRulerRule.grafana_alert.namespace_uid, + id: 1, + type: DashboardSearchItemType.DashDB, + accessControl: { + [AccessControlAction.AlertingRuleUpdate]: true, + }, + }; + + const slashedFolder = { + title: 'Folder with /', + uid: 'abcde', + id: 2, + accessControl: { + [AccessControlAction.AlertingRuleUpdate]: true, + }, + }; beforeEach(() => { jest.clearAllMocks(); contextSrv.isEditor = true; @@ -73,21 +72,6 @@ describe('RuleEditor grafana managed rules', () => { AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleExternalWrite, ]); - }); - - it('can edit grafana managed rule', async () => { - const folder = { - title: 'Folder A', - uid: grafanaRulerRule.grafana_alert.namespace_uid, - id: 1, - type: DashboardSearchItemType.DashDB, - }; - - const slashedFolder = { - title: 'Folder with /', - uid: 'abcde', - id: 2, - }; const dataSources = { default: mockDataSource( @@ -99,18 +83,12 @@ describe('RuleEditor grafana managed rules', () => { { alerting: false } ), }; - - jest.spyOn(backendSrv, 'getFolderByUid').mockResolvedValue({ - ...mockFolder(), - accessControl: { - [AccessControlAction.AlertingRuleUpdate]: true, - }, - }); - setupDataSources(dataSources.default); + setFolderResponse(mockFolder(folder)); + setFolderResponse(mockFolder(slashedFolder)); + }); - // mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); - mocks.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]); + it('can edit grafana managed rule', async () => { const { user } = renderRuleEditor(grafanaRulerRule.grafana_alert.uid); // check that it's filled in @@ -141,7 +119,36 @@ describe('RuleEditor grafana managed rules', () => { // save and check what was sent to backend await user.click(ui.buttons.save.get()); - mocks.searchFolders.mockResolvedValue([] as DashboardSearchHit[]); expect(screen.getByText('New folder')).toBeInTheDocument(); }); + + it('saves evaluation interval correctly', async () => { + const { user } = renderRuleEditor(grafanaRulerRule.grafana_alert.uid); + + await user.click(await screen.findByRole('button', { name: /new evaluation group/i })); + await screen.findByRole('dialog'); + + await user.type(screen.getByLabelText(/evaluation group name/i), 'new group'); + const evalInterval = screen.getByLabelText(/^evaluation interval/i); + + await user.clear(evalInterval); + await user.type(evalInterval, '12m'); + await user.click(screen.getByRole('button', { name: /create/i })); + + // Update the pending period as well, otherwise we'll get a form validation error + // and the rule won't try and save + await user.type(screen.getByLabelText(/pending period/i), '12m'); + + const capture = captureRequests( + (req) => req.method === 'POST' && req.url.includes('/api/ruler/grafana/api/v1/rules/uuid020c61ef') + ); + + await user.click(ui.buttons.save.get()); + + const [request] = await capture; + const postBody = await request.json(); + + expect(postBody.name).toBe('new group'); + expect(postBody.interval).toBe('12m'); + }); }); diff --git a/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx b/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx index 6dcf9d49fdc..aea869c59bc 100644 --- a/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx @@ -211,6 +211,7 @@ export function FolderAndGroup({ className={styles.formInput} error={errors.group?.message} invalid={!!errors.group?.message} + htmlFor="group" > ( @@ -356,6 +357,7 @@ function EvaluationGroupCreationModal({ const { watch } = useFormContext(); const evaluateEveryId = 'eval-every-input'; + const evaluationGroupNameId = 'new-eval-group-name'; const [groupName, folderName] = watch(['group', 'folder.title']); const groupRules = @@ -392,8 +394,11 @@ function EvaluationGroupCreationModal({ onSubmit())}> - Evaluation group + } error={formState.errors.group?.message} @@ -403,7 +408,7 @@ function EvaluationGroupCreationModal({ data-testid={selectors.components.AlertRules.newEvaluationGroupName} className={styles.formInput} autoFocus={true} - id={'group'} + id={evaluationGroupNameId} placeholder="Enter a name" {...register('group', { required: { value: true, message: 'Required.' } })} /> @@ -411,29 +416,27 @@ function EvaluationGroupCreationModal({ Evaluation interval } + invalid={Boolean(formState.errors.evaluateEvery)} > - - (groupRules) - )} - /> - - - - + (groupRules) + )} + /> + + + {isOpen && ( - - - + +
+ +
+
)}
); From 66b881ae2f6c469145d9532f10207376035a48f3 Mon Sep 17 00:00:00 2001 From: antonio <45235678+tonypowa@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:30:55 +0200 Subject: [PATCH 066/174] tutorials: alerting > evaluation (#93981) --- docs/sources/tutorials/alerting-get-started/index.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/sources/tutorials/alerting-get-started/index.md b/docs/sources/tutorials/alerting-get-started/index.md index 1b7ea42a167..169b1634b65 100644 --- a/docs/sources/tutorials/alerting-get-started/index.md +++ b/docs/sources/tutorials/alerting-get-started/index.md @@ -223,10 +223,11 @@ In this section, we define queries, expressions (used to manipulate the data), a ### Set evaluation behavior -An [evaluation group](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/) defines when an alert rule fires, and it’s based on two settings: +The [alert rule evaluation](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/) defines the conditions under which an alert rule triggers, based on the following settings: -- **Evaluation group**: how frequently the alert rule is evaluated. -- **Evaluation interval**: how long the condition must be met to start firing. This allows your data time to stabilize before triggering an alert, helping to reduce the frequency of unnecessary notifications. +- **Evaluation group**: every alert rule is assigned to an evaluation group. You can assign the alert rule to an existing evaluation group or create a new one. +- **Evaluation interval**: determines how frequently the alert rule is checked. For instance, the evaluation may occur every 10s, 30s, 1m, 10m, etc. +- **Pending period**: how long the condition must be met to trigger the alert rule. To set up the evaluation: From 405887eebf024aa503d68440c85ca1437e87a23c Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:31:18 +0200 Subject: [PATCH 067/174] Alerting Docs: Update the introduction to Templates (#93935) * Intro/Templates: update Intro and Template annotations sections * Template labels section + adjustements * Template notifications * Use diagram for `meta_image` --- .../templating-labels-annotations.md | 114 +++++----- .../fundamentals/notifications/templates.md | 200 ++++++++++-------- 2 files changed, 169 insertions(+), 145 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/templating-labels-annotations.md b/docs/sources/alerting/alerting-rules/templating-labels-annotations.md index 321d8bfd720..d25fa40daec 100644 --- a/docs/sources/alerting/alerting-rules/templating-labels-annotations.md +++ b/docs/sources/alerting/alerting-rules/templating-labels-annotations.md @@ -36,6 +36,64 @@ Each template is evaluated whenever the alert rule is evaluated, and is evaluate Extra whitespace in label templates can break matches with notification policies. {{% /admonition %}} +## Variables + +In Grafana templating, the `$` and `.` symbols are used to reference variables and their properties. You can reference variables directly in your alert rule definitions using the `$` symbol followed by the variable name. Similarly, you can access properties of variables using the dot (`.`) notation within alert rule definitions. + +The following variables are available to you when templating labels and annotations: + +### The labels variable + +The `$labels` variable contains all labels from the query. For example, suppose you have a query that returns CPU usage for all of your servers, and you have an alert rule that fires when any of your servers have exceeded 80% CPU usage for the last 5 minutes. You want to add a summary annotation to the alert that tells you which server is experiencing high CPU usage. With the `$labels` variable you can write a template that prints a human-readable sentence such as: + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes +``` + +> If you are using a classic condition then `$labels` will not contain any labels from the query. Classic conditions discard these labels in order to enforce uni-dimensional behavior (at most one alert per alert rule). If you want to use labels from the query in your template then use the example [here](#print-all-labels-from-a-classic-condition). + +### The value variable + +The `$value` variable is a string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule. It does not contain the results of range queries, as these can return anywhere from 10s to 10,000s of rows or metrics. If it did, for especially large queries a single alert could use 10s of MBs of memory and Grafana would run out of memory very quickly. + +To print the `$value` variable in the summary you would write something like this: + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ $value }} +``` + +And would look something like this: + +``` +CPU usage for instance1 has exceeded 80% for the last 5 minutes: [ var='A' labels={instance=instance1} value=81.234 ] +``` + +Here `var='A'` refers to the instant query with Ref ID A, `labels={instance=instance1}` refers to the labels, and `value=81.234` refers to the average CPU usage over the last 5 minutes. + +If you want to print just some of the string instead of the full string then use the `$values` variable. It contains the same information as `$value`, but in a structured table, and is much easier to use then writing a regular expression to match just the text you want. + +### The values variable + +The `$values` variable is a table containing the labels and floating point values of all instant queries and expressions, indexed by their Ref IDs. + +To print the value of the instant query with Ref ID A: + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "A" }} +``` + +For example, given an alert with the labels `instance=server1` and an instant query with the value `81.2345`, this would print: + +``` +CPU usage for instance1 has exceeded 80% for the last 5 minutes: 81.2345 +``` + +If the query in Ref ID A is a range query rather than an instant query then add a reduce expression with Ref ID B and replace `(index $values "A")` with `(index $values "B")`: + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "B" }} +``` + ## Examples The following examples attempt to show the most common use-cases we have seen for templates. You can use these examples verbatim, or adapt them as necessary for your use case. For more information on how to write text/template refer see [the beginner's guide to alert notification templates in Grafana](https://grafana.com/blog/2023/04/05/grafana-alerting-a-beginners-guide-to-templating-alert-notifications/). @@ -216,62 +274,6 @@ B2: 84.5678 B3: 95.6789 ``` -## Variables - -The following variables are available to you when templating labels and annotations: - -### The labels variable - -The `$labels` variable contains all labels from the query. For example, suppose you have a query that returns CPU usage for all of your servers, and you have an alert rule that fires when any of your servers have exceeded 80% CPU usage for the last 5 minutes. You want to add a summary annotation to the alert that tells you which server is experiencing high CPU usage. With the `$labels` variable you can write a template that prints a human-readable sentence such as: - -``` -CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes -``` - -> If you are using a classic condition then `$labels` will not contain any labels from the query. Classic conditions discard these labels in order to enforce uni-dimensional behavior (at most one alert per alert rule). If you want to use labels from the query in your template then use the example [here](#print-all-labels-from-a-classic-condition). - -### The value variable - -The `$value` variable is a string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule. It does not contain the results of range queries, as these can return anywhere from 10s to 10,000s of rows or metrics. If it did, for especially large queries a single alert could use 10s of MBs of memory and Grafana would run out of memory very quickly. - -To print the `$value` variable in the summary you would write something like this: - -``` -CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ $value }} -``` - -And would look something like this: - -``` -CPU usage for instance1 has exceeded 80% for the last 5 minutes: [ var='A' labels={instance=instance1} value=81.234 ] -``` - -Here `var='A'` refers to the instant query with Ref ID A, `labels={instance=instance1}` refers to the labels, and `value=81.234` refers to the average CPU usage over the last 5 minutes. - -If you want to print just some of the string instead of the full string then use the `$values` variable. It contains the same information as `$value`, but in a structured table, and is much easier to use then writing a regular expression to match just the text you want. - -### The values variable - -The `$values` variable is a table containing the labels and floating point values of all instant queries and expressions, indexed by their Ref IDs. - -To print the value of the instant query with Ref ID A: - -``` -CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "A" }} -``` - -For example, given an alert with the labels `instance=server1` and an instant query with the value `81.2345`, this would print: - -``` -CPU usage for instance1 has exceeded 80% for the last 5 minutes: 81.2345 -``` - -If the query in Ref ID A is a range query rather than an instant query then add a reduce expression with Ref ID B and replace `(index $values "A")` with `(index $values "B")`: - -``` -CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "B" }} -``` - ## Functions The following functions are available to you when templating labels and annotations: diff --git a/docs/sources/alerting/fundamentals/notifications/templates.md b/docs/sources/alerting/fundamentals/notifications/templates.md index d0ac6b2775b..2154b82ceef 100644 --- a/docs/sources/alerting/fundamentals/notifications/templates.md +++ b/docs/sources/alerting/fundamentals/notifications/templates.md @@ -17,148 +17,170 @@ labels: - enterprise - oss title: Templates +meta_image: /media/docs/alerting/how-notification-templates-works.png weight: 115 refs: - variables-label-annotation: + labels: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/alert-rules/annotation-label/#labels + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#labels + annotations: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/alert-rules/annotation-label/#annotations + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#annotations + templating-labels-annotations: - pattern: /docs/grafana/ destination: /docs/grafana//alerting/alerting-rules/templating-labels-annotations/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templating-labels-annotations/ + notification-message-reference: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/template-notifications/reference/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference/ + notification-messages: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/template-notifications/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/ + create-notification-templates: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/template-notifications/create-notification-templates/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/create-notification-templates/ --- # Templates Use templating to customize, format, and reuse alert notification messages. Create more flexible and informative alert notification messages by incorporating dynamic content, such as metric values, labels, and other contextual information. -In Grafana, there are two ways to template your alert notification messages: +In Grafana, you have various options to template your alert notification messages: -1. Labels and annotations +1. [Alert rule annotations](#template-annotations) - - Template labels and annotations in alert rules. - - Labels and annotations contain information about an alert. - - Labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert. + - Annotations add extra information, like `summary` and `description`, to alert instances for notification messages. + - Template annotations to display query values that are meaningful to the alert, for example, the server name or the threshold query value. -2. Notification templates +1. [Alert rule labels](#template-labels) - - Template notifications in contact points. - - Add notification templates to contact points for reuse and consistent messaging in your notifications. - - Use notification templates to change the title, message, and format of the message in your notifications. + - Labels are used to differentiate an alert instance from all other alert instances. + - Template labels to add an additional label based on a query value, or when the labels from the query are incomplete or not descriptive enough. -This diagram illustrates the entire process of templating, from the creation of labels and annotations in alert rules or notification templates in contact points, to what they look like when exported and applied in your alert notification messages. +1. [Notification templates](#template-notifications) + - Notification templates are used by contact points for consistent messaging in notification titles and descriptions. + - Template notifications when you want to customize the appearance and information of your notifications. + - Avoid using notification templates to add extra information to alert instances—use annotations instead. -{{< figure src="/media/docs/alerting/grafana-templating-diagram-2.jpg" max-width="1200px" caption="How Templating works" >}} +This diagram illustrates the entire templating process, from querying labels and templating the alert summary and notification to the final alert notification message. + +{{< figure src="/media/docs/alerting/how-notification-templates-works.png" max-width="1200px" caption="How templating works" >}} In this diagram: -- **Monitored Application**: A web server, database, or any other service generating metrics. For example, it could be an NGINX server providing metrics about request rates, response times, and so on. -- **Prometheus**: Prometheus collects metrics from the monitored application. For example, it might scrape metrics from the NGINX server, including labels like instance (the server hostname) and job (the service name). -- **Grafana**: Grafana queries Prometheus to retrieve metrics data. For example, you might create an alert rule to monitor NGINX request rates over time, and template labels or annotations based on the instance label. -- **Alertmanager**: Part of the Prometheus ecosystem, Alertmanager handles alert notifications. For example, if the request rate exceeds a certain threshold on a particular NGINX server, Alertmanager can send an alert notification to, for example, Slack or email, including the server name and the exceeded threshold (the instance label will be interpolated, and the actual server name will appear in the alert notification). -- **Alert notification**: When an alert rule condition is met, Alertmanager sends a notification to various channels such as Slack, Grafana OnCall, etc. These notifications can include information from the labels associated with the alerting rule. For example, if an alert triggers due to high CPU usage on a specific server, the notification message can include details like server name (instance label), disk usage percentage, and the threshold that was exceeded. +1. The alert rule query returns `12345`, along with the values of the `instance` and `job` labels. +1. This query result breaches the alert rule condition, firing the alert instance. +1. The alert instance generates an annotation summary, defined by the template used in the alert rule summary. In this case, it displays the value of the `instance` label: `server1`. +1. The Alertmanager receives the firing alert instance, including the final annotation summary, and determines the contact point that will process the alert. +1. The Alertmanager uses the contact point's notification template to format the message, then sends the notification to the configured destination(s)—an email address. -## Labels and annotations +## Template annotations -Labels and annotations contain information about an alert. Labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert. +[Annotations](ref:annotations) can be defined in the alert rule to add extra information to alert instances. -### Template labels +When creating an alert rule, Grafana suggests several optional annotations, such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`, which help identify and respond to alerts. You can also create custom annotations. -Label templates are applied in the alert rule itself (i.e. in the Configure labels and notifications section of an alert). +Annotations are key-value pairs, and their values can contain a combination of text and template code that is evaluated when the alert fires. -{{}} -Think about templating labels when you need to improve or change how alerts are uniquely identified. This is especially helpful if the labels you get from your query aren't detailed enough. Keep in mind that it's better to keep long sentences for summaries and descriptions. Also, avoid using the query's value in labels because it may result in the creation of many alerts when you actually only need one. -{{}} +Annotations can contain plain text, but you should template annotations if you need to display query values that are relevant to the alert, for example: -Templating can be applied by using variables and functions. These variables can represent dynamic values retrieved from your data queries. +- Show the query value that triggers the alert. +- Include labels returned by the query that identify the alert. +- Format the annotation message depending on a query value. -{{}} -In Grafana templating, the $ and . symbols are used to reference variables and their properties. You can reference variables directly in your alert rule definitions using the $ symbol followed by the variable name. Similarly, you can access properties of variables using the dot (.) notation within alert rule definitions. -{{}} +Here’s an example of templating an annotation, which explains where and why the alert was triggered. In this case, the alert triggers when CPU usage exceeds a threshold, and the `summary` annotation provides the relevant details. -Here are some commonly used built-in [variables](ref:variables-label-annotation) to interact with the name and value of labels in Grafana alerting: +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% ({{ index $values "A" }}) for the last 5 minutes. +``` -- The `$labels` variable, which contains all labels from the query. +The outcome of this template would be: - For example, let's say you have an alert rule that triggers when the CPU usage exceeds a certain threshold. You want to create annotations that provide additional context when this alert is triggered, such as including the specific server that experienced the high CPU usage. +``` +CPU usage for Instance 1 has exceeded 80% (81.2345) for the last 5 minutes. +``` - The host {{ index $labels "instance" }} has exceeded 80% CPU usage for the last 5 minutes +Implement annotations that provide meaningful information to respond to your alerts. Annotations are displayed in the Grafana alert detail view and are included by default in notifications. - The outcome of this template would print: +For more details on how to template annotations, refer to [Template annotations and labels](ref:templating-labels-annotations). - The host instance 1 has exceeded 80% CPU usage for the last 5 minutes +## Template labels -- The `$value` variable, which is a string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule. +[Labels](ref:labels) are used to differentiate one alert instance from all other alert instances, as the set of labels uniquely identifies an alert instance. Notification policies and silences use labels to handle alert instances. - In the context of the previous example, $value variable would write something like this: +Template labels when you need to improve or change how alerts are uniquely identified. This is helpful if the labels you get from your query aren't detailed enough. - CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ $value }} +Here’s an example of templating a `severity` label based on the query value: - The outcome of this template would print: +``` +{{ if (gt $values.A.Value 90.0) -}} +critical +{{ else if (gt $values.A.Value 80.0) -}} +high +{{ else if (gt $values.A.Value 60.0) -}} +medium +{{ else -}} +low +{{- end }} +``` - CPU usage for instance1 has exceeded 80% for the last 5 minutes: [ var='A' labels={instance=instance1} value=81.234 ] +Avoid using query values in labels, as this may result in the creation of numerous alerts when only one is needed. Use annotation to inform about the query value instead. -- The `$values` variable is a table containing the labels and floating point values of all instant queries and expressions, indexed by their Ref IDs (i.e. the id that identifies the query or expression. By default the Red ID of the query is “A”). +For more details on how to template labels, refer to [Template annotations and labels](ref:templating-labels-annotations). - Given an alert with the labels instance=server1 and an instant query with the value 81.2345, would write like this: +## Template notifications - CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "A" }} +[Notification templates](ref:notification-messages) allow you to customize the content of your notifications, such as the subject of an email or the body of a Slack message. - And it would print: +Notification templates differ from templating annotations and labels in the following ways: - CPU usage for instance1 has exceeded 80% for the last 5 minutes: 81.2345 +- Notification templates are assigned to the **Contact point**, rather than the alert rule. +- If not specified, the contact point uses a default template that includes relevant alert information. +- You can create reusable notification templates and reference them in other templates. +- The same template can be shared across multiple contact points, making it easier to maintain and ensuring consistency. +- While both annotation/label templates and notification templates use the same templating language, the available variables and functions differ. For more details, refer to the [notification template reference](ref:notification-message-reference) and [annotation/label template reference](ref:templating-labels-annotations). +- Notification templates should not be used to add additional information to individual alerts—use annotations for that purpose. -{{% admonition type="caution" %}} -Extra whitespace in label templates can break matches with notification policies. -{{% /admonition %}} +Here is an example of a notification template that summarizes all firing and resolved alerts in a notification group: -### Template annotations - -Both labels and annotations have the same structure: a set of named values; however their intended uses are different. The purpose of annotations is to add additional information to existing alerts. - -There are a number of suggested annotations in Grafana such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`. Like labels, annotations must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. - -Here is an example of templating an annotation in the context of an alert rule. The text/template is added into the Add annotations section. - - CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes - -The outcome of this template would print - - CPU usage for Instance 1 has exceeded 80% for the last 5 minutes - -### Template notifications - -Notification templates represent the alternative approach to templating designed for reusing templates. Notifications are messages to inform users about events or conditions triggered by alerts. You can create reusable notification templates to customize the content and format of alert notifications. Variables, labels, or other context-specific details can be added to the templates to dynamically insert information like metric values. - -Here is an example of a notification template: - -```go +``` {{ define "alerts.message" -}} -{{ if .Alerts.Firing -}} -{{ len .Alerts.Firing }} firing alert(s) -{{ template "alerts.summarize" .Alerts.Firing }} -{{- end }} -{{- if .Alerts.Resolved -}} -{{ len .Alerts.Resolved }} resolved alert(s) -{{ template "alerts.summarize" .Alerts.Resolved }} -{{- end }} + {{ if .Alerts.Firing -}} + {{ len .Alerts.Firing }} firing alert(s) + {{ template "alerts.summarize" .Alerts.Firing }} + {{- end }} + {{- if .Alerts.Resolved -}} + {{ len .Alerts.Resolved }} resolved alert(s) + {{ template "alerts.summarize" .Alerts.Resolved }} + {{- end }} {{- end }} {{ define "alerts.summarize" -}} -{{ range . -}} -- {{ index .Annotations "summary" }} -{{ end }} + {{ range . -}} + - {{ index .Annotations "summary" }} + {{ end }} {{ end }} ``` -This is the message you would receive in your contact point: +The notification message to the contact point would look like this: - 1 firing alert(s) - - The database server db1 has exceeded 75% of available disk space. Disk space used is 76%, please resize the disk size within the next 24 hours +``` +1 firing alert(s) +- The database server db1 has exceeded 75% of available disk space. Disk space used is 76%, please resize the disk size within the next 24 hours. - 1 resolved alert(s) - - The web server web1 has been responding to 5% of HTTP requests with 5xx errors for the last 5 minutes +1 resolved alert(s) +- The web server web1 has been responding to 5% of HTTP requests with 5xx errors for the last 5 minutes. +``` -Once the template is created, you need to make reference to it in your **Contact point** (in the Optional `[contact point]` settings) . - -{{}} -It's not recommended to include individual alert information within notification templates. Instead, it's more effective to incorporate such details within the rule using labels and annotations. -{{}} +For instructions on creating and using notification templates, refer to [Create notification templates.](ref:create-notification-templates) From 42f1fcaf2cc0e8e9ad071f2b1c025fbaee0c5811 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:44:15 +0100 Subject: [PATCH 068/174] Restore Dashboards: Add e2e tests (again) (#93214) --- e2e/dashboards-suite/dashboard-browse.spec.ts | 8 + .../dashboard-restore.spec.ts | 82 +++++++ e2e/dashboards/TestRestoreDashboard.json | 209 ++++++++++++++++++ e2e/utils/flows/deleteDashboard.ts | 8 + .../src/selectors/pages.ts | 8 + .../dashboard/components/DashNav/DashNav.tsx | 1 + .../page/components/SearchResultsTable.tsx | 17 +- scripts/grafana-server/custom.ini | 2 +- 8 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 e2e/dashboards-suite/dashboard-restore.spec.ts create mode 100644 e2e/dashboards/TestRestoreDashboard.json diff --git a/e2e/dashboards-suite/dashboard-browse.spec.ts b/e2e/dashboards-suite/dashboard-browse.spec.ts index 4e9728b7a65..a39c0ca373d 100644 --- a/e2e/dashboards-suite/dashboard-browse.spec.ts +++ b/e2e/dashboards-suite/dashboard-browse.spec.ts @@ -49,4 +49,12 @@ describe('Dashboard browse', () => { e2e.flows.confirmDelete(); e2e.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard').should('not.exist'); }); + + afterEach(() => { + // Permanently delete dashboard + e2e.pages.RecentlyDeleted.visit(); + e2e.pages.Search.table.row('E2E Test - Import Dashboard').find('[type="checkbox"]').click({ force: true }); + cy.contains('button', 'Delete permanently').click(); + e2e.flows.confirmDelete(); + }); }); diff --git a/e2e/dashboards-suite/dashboard-restore.spec.ts b/e2e/dashboards-suite/dashboard-restore.spec.ts new file mode 100644 index 00000000000..270b9794e31 --- /dev/null +++ b/e2e/dashboards-suite/dashboard-restore.spec.ts @@ -0,0 +1,82 @@ +import testDashboard from '../dashboards/TestRestoreDashboard.json'; +import { e2e } from '../utils'; + +describe('Dashboard restore', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('Should delete, restore and permanently delete from the Dashboards page', () => { + e2e.flows.importDashboard(testDashboard, 1000, true); + + e2e.pages.Dashboards.visit(); + + // Delete dashboard + e2e.pages.BrowseDashboards.table + .row('E2E Test - Restore Dashboard') + .find('[type="checkbox"]') + .click({ force: true }); + deleteDashboard('Delete'); + + // Dashboard should appear in Recently Deleted + e2e.pages.RecentlyDeleted.visit(); + e2e.pages.Search.table.row('E2E Test - Restore Dashboard').should('exist'); + + // Restore dashboard + e2e.pages.Search.table.row('E2E Test - Restore Dashboard').find('[type="checkbox"]').click({ force: true }); + cy.contains('button', 'Restore').click(); + cy.contains('p', 'This action will restore 1 dashboard.').should('be.visible'); + e2e.pages.ConfirmModal.delete().click(); + e2e.components.Alert.alertV2('success').contains('Dashboard E2E Test - Restore Dashboard restored').should('exist'); + + // Dashboard should appear in Browse + e2e.pages.Dashboards.visit(); + e2e.pages.BrowseDashboards.table.row('E2E Test - Restore Dashboard').should('exist'); + + // Delete dashboard + e2e.pages.BrowseDashboards.table + .row('E2E Test - Restore Dashboard') + .find('[type="checkbox"]') + .click({ force: true }); + deleteDashboard('Delete'); + + // Permanently delete dashboard + permanentlyDeleteDashboard(); + }); + + it('Should delete, restore and permanently delete from the Dashboard settings', () => { + e2e.flows.importDashboard(testDashboard, 1000, true); + + e2e.flows.openDashboard({ uid: '355ac6c2-8a12-4469-8b99-4750eb8d0966' }); + e2e.pages.Dashboard.DashNav.settingsButton().click(); + deleteDashboard('Delete dashboard'); + + // Permanently delete dashboard + permanentlyDeleteDashboard(); + }); +}); + +const deleteDashboard = (buttonName: string) => { + cy.contains('button', buttonName).click(); + e2e.flows.confirmDelete(); + e2e.components.Alert.alertV2('success') + .contains('Dashboard E2E Test - Restore Dashboard moved to Recently deleted') + .should('exist'); + e2e.pages.BrowseDashboards.table.row('E2E Test - Restore Dashboard').should('not.exist'); +}; + +const permanentlyDeleteDashboard = () => { + // Permanently delete dashboard + e2e.pages.RecentlyDeleted.visit(); + e2e.pages.Search.table.row('E2E Test - Restore Dashboard').find('[type="checkbox"]').click({ force: true }); + cy.contains('button', 'Delete permanently').click(); + cy.contains('p', 'This action will delete 1 dashboard.').should('be.visible'); + e2e.flows.confirmDelete(); + e2e.components.Alert.alertV2('success').contains('Dashboard E2E Test - Restore Dashboard deleted').should('exist'); + + // Dashboard should not appear in Recently Deleted or Browse + e2e.pages.Search.table.row('E2E Test - Restore Dashboard').should('not.exist'); + + e2e.pages.Dashboards.visit(); + e2e.pages.BrowseDashboards.table.row('E2E Test - Restore Dashboard').should('not.exist'); +}; diff --git a/e2e/dashboards/TestRestoreDashboard.json b/e2e/dashboards/TestRestoreDashboard.json new file mode 100644 index 00000000000..b34ff4c0c05 --- /dev/null +++ b/e2e/dashboards/TestRestoreDashboard.json @@ -0,0 +1,209 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 322, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "text": {} + }, + "pluginVersion": "8.3.0-pre", + "title": "Gauge Example", + "type": "gauge" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.3.0-pre", + "title": "Stat", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "title": "Time series example", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 31, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2021-09-01T04:00:00.000Z", + "to": "2021-09-15T04:00:00.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "E2E Test - Restore Dashboard", + "uid": "355ac6c2-8a12-4469-8b99-4750eb8d0966", + "version": 4 +} diff --git a/e2e/utils/flows/deleteDashboard.ts b/e2e/utils/flows/deleteDashboard.ts index de492012c2f..7eb294f6758 100644 --- a/e2e/utils/flows/deleteDashboard.ts +++ b/e2e/utils/flows/deleteDashboard.ts @@ -29,6 +29,14 @@ export const deleteDashboard = ({ quick = false, title, uid }: DeleteDashboardCo const quickDelete = (uid: string) => { cy.request('DELETE', fromBaseUrl(`/api/dashboards/uid/${uid}`)); + cy.window().then((win: Cypress.AUTWindow) => { + if ( + win.grafanaBootData.settings.featureToggles.dashboardRestore && + win.grafanaBootData.settings.featureToggles.dashboardRestoreUI + ) { + cy.request('DELETE', fromBaseUrl(`/api/dashboards/uid/${uid}/trash`)); + } + }); }; const uiDelete = (uid: string, title: string) => { diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index be04b0e8296..2b835699614 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -57,6 +57,7 @@ export const Pages = { navV2: 'data-testid Dashboard navigation', publicDashboardTag: 'data-testid public dashboard tag', shareButton: 'data-testid share-button', + settingsButton: 'data-testid settings-button', scrollContainer: 'data-testid Dashboard canvas scroll container', newShareButton: { container: 'data-testid new share button', @@ -239,6 +240,9 @@ export const Pages = { */ dashboards: (title: string) => `Dashboard search item ${title}`, }, + RecentlyDeleted: { + url: '/dashboard/recently-deleted', + }, SaveDashboardAsModal: { newName: 'Save dashboard title field', save: 'Save dashboard button', @@ -403,6 +407,10 @@ export const Pages = { FolderView: { url: '/?search=open&layout=folders', }, + table: { + body: 'data-testid search-table', + row: (name: string) => `data-testid search row ${name}`, + }, }, PublicDashboards: { ListItem: { diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 86a3986be31..b0bb9796b16 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -326,6 +326,7 @@ export const DashNav = memo((props) => { diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 20ae3c292ff..471bdeb0108 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -7,6 +7,7 @@ import InfiniteLoader from 'react-window-infinite-loader'; import { Observable } from 'rxjs'; import { Field, GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { TableCellHeight } from '@grafana/schema'; import { useStyles2, useTheme2 } from '@grafana/ui'; import { TableCell } from '@grafana/ui/src/components/Table/TableCell'; @@ -137,9 +138,13 @@ export const SearchResultsTable = React.memo( className += ' ' + styles.selectedRow; } const { key, ...rowProps } = row.getRowProps({ style }); - return ( -
+
{row.cells.map((cell: Cell, index: number) => { return ( +
{headerGroups.map((headerGroup) => { const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({ style: { width }, diff --git a/scripts/grafana-server/custom.ini b/scripts/grafana-server/custom.ini index 5d09d40ac63..d1751079a18 100644 --- a/scripts/grafana-server/custom.ini +++ b/scripts/grafana-server/custom.ini @@ -4,7 +4,7 @@ content_security_policy = true content_security_policy_template = """require-trusted-types-for 'script'; script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';""" [feature_toggles] -enable = publicDashboards +enable = publicDashboards, dashboardRestore, dashboardRestoreUI [plugins] allow_loading_unsigned_plugins=grafana-extensionstest-app,grafana-extensionexample1-app,grafana-extensionexample2-app, From 7bca69849f08ac9d0ea96fe04016a11420f3acf3 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Mon, 30 Sep 2024 11:49:02 +0200 Subject: [PATCH 069/174] Dashboards: Enable scenes by default (#93818) * Mark Scenes feature toggles as GA * Move old arch e2e to a new folder * Run E2E on scenes by default * Upgrade e2e-selectors to ensure the tests in Playwright works --- .betterer.results | 4 +- .drone.yml | 34 +++--- .github/CODEOWNERS | 1 - .github/workflows/run-scenes-e2e.yml | 47 -------- .../feature-toggles/index.md | 8 +- e2e/cypress/support/e2e.js | 6 +- .../Repeating_a_panel_horizontally.spec.ts | 8 +- .../Repeating_a_panel_vertically.spec.ts | 8 +- .../Repeating_an_empty_row.spec.ts | 9 +- e2e/dashboards-suite/dashboard-browse.spec.ts | 4 +- .../dashboard-export-json.spec.ts | 2 +- .../dashboard-keybindings.spec.ts | 0 .../dashboard-live-streaming.spec.ts | 3 +- .../dashboard-public-create.spec.ts | 10 +- .../dashboard-public-templating.spec.ts | 4 +- .../dashboard-share-externally-create.spec.ts | 4 +- .../dashboard-share-internally.spec.ts | 4 +- .../dashboard-share-snapshot-create.spec.ts | 6 +- .../dashboard-templating.spec.ts | 2 +- .../dashboard-time-zone.spec.ts | 4 +- .../general-dashboards.spec.ts | 4 +- .../load-options-from-url.spec.ts | 79 ++++++++----- .../new-constant-variable.spec.ts | 8 +- .../new-custom-variable.spec.ts | 19 ++- .../new-datasource-variable.spec.ts | 30 ++--- .../new-interval-variable.spec.ts | 11 +- .../new-query-variable.spec.ts | 55 +++++---- .../new-text-box-variable.spec.ts | 15 ++- .../set-options-from-ui.spec.ts | 111 +++++++++++------- .../dashboards-suite/snapshot-create.spec.ts | 0 ...ting-dashboard-links-and-variables.spec.ts | 16 ++- .../Repeating_a_panel_horizontally.spec.ts | 8 +- .../Repeating_a_panel_vertically.spec.ts | 8 +- .../Repeating_an_empty_row.spec.ts | 9 +- .../dashboard-browse-nested.spec.ts | 0 .../dashboards-suite/dashboard-browse.spec.ts | 4 +- .../dashboard-live-streaming.spec.ts | 3 +- .../dashboard-panel-attention.spec.ts | 0 .../dashboard-public-create.spec.ts | 10 +- .../dashboard-public-templating.spec.ts | 4 +- .../dashboard-templating.spec.ts | 2 +- .../dashboard-time-zone.spec.ts | 4 +- .../dashboard-timepicker.spec.ts | 0 .../embedded-dashboard.spec.ts | 0 .../general-dashboards.spec.ts | 4 +- .../dashboards-suite/import-dashboard.spec.ts | 0 .../load-options-from-url.spec.ts | 79 +++++-------- .../new-constant-variable.spec.ts | 8 +- .../new-custom-variable.spec.ts | 19 +-- .../new-datasource-variable.spec.ts | 30 +++-- .../new-interval-variable.spec.ts | 11 +- .../new-query-variable.spec.ts | 55 ++++----- .../new-text-box-variable.spec.ts | 15 +-- .../set-options-from-ui.spec.ts | 97 ++++++--------- ...ting-dashboard-links-and-variables.spec.ts | 16 +-- .../textbox-variables.spec.ts | 0 .../dashboards-suite/utils/makeDashboard.ts | 0 .../dashboards/DashboardLiveTest.json | 0 .../dashboards/DashboardSearchTest.json | 0 .../dashboards/PanelSandboxDashboard.json | 0 .../dashboards/TestDashboard.json | 0 .../panels-suite/dashlist.spec.ts | 8 +- .../panels-suite/datagrid-data-change.spec.ts | 2 +- .../datagrid-editing-features.spec.ts | 2 +- .../frontend-sandbox-panel.spec.ts | 8 +- .../panels-suite/geomap-layer-types.spec.ts | 14 +-- .../panels-suite/geomap-map-controls.spec.ts | 2 +- ...eomap-spatial-operations-transform.spec.ts | 10 +- .../panels-suite/panelEdit_base.spec.ts | 39 +++--- .../panels-suite/panelEdit_queries.spec.ts | 2 +- .../panels-suite/panelEdit_transforms.spec.ts | 6 +- .../shared/smokeTestScenario.ts | 19 +-- .../smoke-tests-suite/1-smoketests.spec.ts | 0 .../panels_smokescreen.spec.ts | 16 ++- .../utils/flows/addDashboard.ts | 6 +- .../utils/flows/addDataSource.ts | 0 .../utils/flows/addPanel.ts | 0 .../utils/flows/assertSuccessNotification.ts | 0 .../utils/flows/configurePanel.ts | 10 +- .../utils/flows/confirmModal.ts | 0 .../utils/flows/deleteDashboard.ts | 0 .../utils/flows/deleteDataSource.ts | 0 .../utils/flows/editPanel.ts | 0 .../utils/flows/importDashboard.ts | 4 +- .../utils/flows/importDashboards.ts | 0 e2e/{scenes => old-arch}/utils/flows/index.ts | 0 e2e/{scenes => old-arch}/utils/flows/login.ts | 0 .../utils/flows/openDashboard.ts | 0 .../utils/flows/openPanelMenuItem.ts | 0 .../utils/flows/revertAllChanges.ts | 0 .../utils/flows/saveDashboard.ts | 0 .../utils/flows/selectOption.ts | 0 .../utils/flows/setDashboardTimeRange.ts | 0 .../utils/flows/setTimeRange.ts | 0 .../utils/flows/userPreferences.ts | 0 e2e/{scenes => old-arch}/utils/index.ts | 0 .../utils/support/benchmark.ts | 0 e2e/old-arch/utils/support/clipboard.ts | 29 +++++ .../utils/support/index.ts | 0 .../utils/support/localStorage.ts | 0 .../utils/support/monaco.ts | 0 .../utils/support/scenarioContext.ts | 0 .../utils/support/selector.ts | 0 .../utils/support/types.ts | 0 e2e/{scenes => old-arch}/utils/support/url.ts | 2 - .../utils/typings/index.ts | 0 .../utils/typings/undo.ts | 0 .../various-suite/bar-gauge.spec.ts | 4 +- .../various-suite/exemplars.spec.ts | 6 +- .../various-suite/explore.spec.ts | 0 .../various-suite/filter-annotations.spec.ts | 37 ++---- .../frontend-sandbox-app.spec.ts | 0 .../frontend-sandbox-datasource.spec.ts | 6 +- .../various-suite/gauge.spec.ts | 0 .../various-suite/graph-auto-migrate.spec.ts | 0 .../helpers/prometheus-helpers.ts | 0 .../various-suite/inspect-drawer.spec.ts | 0 .../various-suite/keybinds.spec.ts | 7 +- .../various-suite/loki-editor.spec.ts | 0 .../various-suite/loki-query-builder.spec.ts | 0 .../loki-table-explore-to-dash.spec.ts | 8 +- .../various-suite/navigation.spec.ts | 0 .../various-suite/pie-chart.spec.ts | 7 +- .../prometheus-annotations.spec.ts | 5 +- .../various-suite/prometheus-config.spec.ts | 0 .../various-suite/prometheus-editor.spec.ts | 10 +- .../prometheus-variable-editor.spec.ts | 9 +- .../various-suite/query-editor.spec.ts | 0 .../various-suite/return-to-previous.spec.ts | 0 .../various-suite/select-focus.spec.ts | 0 .../various-suite/solo-route.spec.ts | 0 .../trace-view-scrolling.spec.ts | 0 .../visualization-suggestions.spec.ts | 0 e2e/panels-suite/dashlist.spec.ts | 6 +- .../frontend-sandbox-panel.spec.ts | 4 +- e2e/panels-suite/geomap-layer-types.spec.ts | 12 +- ...eomap-spatial-operations-transform.spec.ts | 8 +- e2e/panels-suite/panelEdit_base.spec.ts | 37 +++--- e2e/panels-suite/panelEdit_transforms.spec.ts | 4 +- .../as-viewer-user/permissions.spec.ts | 5 +- e2e/run-suite | 12 +- e2e/shared/smokeTestScenario.ts | 13 +- .../panels_smokescreen.spec.ts | 16 +-- e2e/utils/flows/addDashboard.ts | 6 +- e2e/utils/flows/configurePanel.ts | 10 +- e2e/utils/flows/importDashboard.ts | 4 +- e2e/utils/support/url.ts | 2 + e2e/various-suite/bar-gauge.spec.ts | 4 +- e2e/various-suite/exemplars.spec.ts | 6 +- e2e/various-suite/filter-annotations.spec.ts | 37 ++++-- .../frontend-sandbox-datasource.spec.ts | 6 +- e2e/various-suite/keybinds.spec.ts | 7 +- .../loki-table-explore-to-dash.spec.ts | 3 +- e2e/various-suite/pie-chart.spec.ts | 7 +- .../prometheus-annotations.spec.ts | 5 +- e2e/various-suite/prometheus-editor.spec.ts | 4 +- .../prometheus-variable-editor.spec.ts | 9 +- package.json | 2 +- pkg/services/featuremgmt/registry.go | 12 +- pkg/services/featuremgmt/toggles_gen.csv | 8 +- pkg/services/featuremgmt/toggles_gen.json | 48 +++++--- scripts/drone/pipelines/build.star | 8 +- yarn.lock | 10 +- 163 files changed, 748 insertions(+), 747 deletions(-) delete mode 100644 .github/workflows/run-scenes-e2e.yml rename e2e/{scenes => }/dashboards-suite/dashboard-export-json.spec.ts (98%) rename e2e/{scenes => }/dashboards-suite/dashboard-keybindings.spec.ts (100%) rename e2e/{scenes => }/dashboards-suite/dashboard-share-externally-create.spec.ts (97%) rename e2e/{scenes => }/dashboards-suite/dashboard-share-internally.spec.ts (98%) rename e2e/{scenes => }/dashboards-suite/dashboard-share-snapshot-create.spec.ts (93%) rename e2e/{scenes => }/dashboards-suite/snapshot-create.spec.ts (100%) rename e2e/{scenes => old-arch}/dashboards-suite/Repeating_a_panel_horizontally.spec.ts (95%) rename e2e/{scenes => old-arch}/dashboards-suite/Repeating_a_panel_vertically.spec.ts (95%) rename e2e/{scenes => old-arch}/dashboards-suite/Repeating_an_empty_row.spec.ts (94%) rename e2e/{scenes => old-arch}/dashboards-suite/dashboard-browse-nested.spec.ts (100%) rename e2e/{scenes => old-arch}/dashboards-suite/dashboard-browse.spec.ts (93%) rename e2e/{scenes => old-arch}/dashboards-suite/dashboard-live-streaming.spec.ts (75%) rename e2e/{ => old-arch}/dashboards-suite/dashboard-panel-attention.spec.ts (100%) rename e2e/{scenes => old-arch}/dashboards-suite/dashboard-public-create.spec.ts (95%) rename e2e/{scenes => old-arch}/dashboards-suite/dashboard-public-templating.spec.ts (91%) rename e2e/{scenes => old-arch}/dashboards-suite/dashboard-templating.spec.ts (95%) rename e2e/{scenes => old-arch}/dashboards-suite/dashboard-time-zone.spec.ts (98%) rename e2e/{scenes => old-arch}/dashboards-suite/dashboard-timepicker.spec.ts (100%) rename e2e/{scenes => old-arch}/dashboards-suite/embedded-dashboard.spec.ts (100%) rename e2e/{scenes => old-arch}/dashboards-suite/general-dashboards.spec.ts (87%) rename e2e/{scenes => old-arch}/dashboards-suite/import-dashboard.spec.ts (100%) rename e2e/{scenes => old-arch}/dashboards-suite/load-options-from-url.spec.ts (75%) rename e2e/{scenes => old-arch}/dashboards-suite/new-constant-variable.spec.ts (83%) rename e2e/{scenes => old-arch}/dashboards-suite/new-custom-variable.spec.ts (83%) rename e2e/{scenes => old-arch}/dashboards-suite/new-datasource-variable.spec.ts (61%) rename e2e/{scenes => old-arch}/dashboards-suite/new-interval-variable.spec.ts (82%) rename e2e/{scenes => old-arch}/dashboards-suite/new-query-variable.spec.ts (80%) rename e2e/{scenes => old-arch}/dashboards-suite/new-text-box-variable.spec.ts (75%) rename e2e/{scenes => old-arch}/dashboards-suite/set-options-from-ui.spec.ts (73%) rename e2e/{scenes => old-arch}/dashboards-suite/templating-dashboard-links-and-variables.spec.ts (85%) rename e2e/{scenes => old-arch}/dashboards-suite/textbox-variables.spec.ts (100%) rename e2e/{scenes => old-arch}/dashboards-suite/utils/makeDashboard.ts (100%) rename e2e/{scenes => old-arch}/dashboards/DashboardLiveTest.json (100%) rename e2e/{scenes => old-arch}/dashboards/DashboardSearchTest.json (100%) rename e2e/{scenes => old-arch}/dashboards/PanelSandboxDashboard.json (100%) rename e2e/{scenes => old-arch}/dashboards/TestDashboard.json (100%) rename e2e/{scenes => old-arch}/panels-suite/dashlist.spec.ts (88%) rename e2e/{scenes => old-arch}/panels-suite/datagrid-data-change.spec.ts (97%) rename e2e/{scenes => old-arch}/panels-suite/datagrid-editing-features.spec.ts (99%) rename e2e/{scenes => old-arch}/panels-suite/frontend-sandbox-panel.spec.ts (93%) rename e2e/{scenes => old-arch}/panels-suite/geomap-layer-types.spec.ts (91%) rename e2e/{scenes => old-arch}/panels-suite/geomap-map-controls.spec.ts (97%) rename e2e/{scenes => old-arch}/panels-suite/geomap-spatial-operations-transform.spec.ts (93%) rename e2e/{scenes => old-arch}/panels-suite/panelEdit_base.spec.ts (75%) rename e2e/{scenes => old-arch}/panels-suite/panelEdit_queries.spec.ts (99%) rename e2e/{scenes => old-arch}/panels-suite/panelEdit_transforms.spec.ts (86%) rename e2e/{scenes => old-arch}/shared/smokeTestScenario.ts (70%) rename e2e/{scenes => old-arch}/smoke-tests-suite/1-smoketests.spec.ts (100%) rename e2e/{scenes => old-arch}/smoke-tests-suite/panels_smokescreen.spec.ts (66%) rename e2e/{scenes => old-arch}/utils/flows/addDashboard.ts (97%) rename e2e/{scenes => old-arch}/utils/flows/addDataSource.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/addPanel.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/assertSuccessNotification.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/configurePanel.ts (93%) rename e2e/{scenes => old-arch}/utils/flows/confirmModal.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/deleteDashboard.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/deleteDataSource.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/editPanel.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/importDashboard.ts (95%) rename e2e/{scenes => old-arch}/utils/flows/importDashboards.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/index.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/login.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/openDashboard.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/openPanelMenuItem.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/revertAllChanges.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/saveDashboard.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/selectOption.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/setDashboardTimeRange.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/setTimeRange.ts (100%) rename e2e/{scenes => old-arch}/utils/flows/userPreferences.ts (100%) rename e2e/{scenes => old-arch}/utils/index.ts (100%) rename e2e/{scenes => old-arch}/utils/support/benchmark.ts (100%) create mode 100644 e2e/old-arch/utils/support/clipboard.ts rename e2e/{scenes => old-arch}/utils/support/index.ts (100%) rename e2e/{scenes => old-arch}/utils/support/localStorage.ts (100%) rename e2e/{scenes => old-arch}/utils/support/monaco.ts (100%) rename e2e/{scenes => old-arch}/utils/support/scenarioContext.ts (100%) rename e2e/{scenes => old-arch}/utils/support/selector.ts (100%) rename e2e/{scenes => old-arch}/utils/support/types.ts (100%) rename e2e/{scenes => old-arch}/utils/support/url.ts (92%) rename e2e/{scenes => old-arch}/utils/typings/index.ts (100%) rename e2e/{scenes => old-arch}/utils/typings/undo.ts (100%) rename e2e/{scenes => old-arch}/various-suite/bar-gauge.spec.ts (76%) rename e2e/{scenes => old-arch}/various-suite/exemplars.spec.ts (93%) rename e2e/{scenes => old-arch}/various-suite/explore.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/filter-annotations.spec.ts (61%) rename e2e/{scenes => old-arch}/various-suite/frontend-sandbox-app.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/frontend-sandbox-datasource.spec.ts (97%) rename e2e/{scenes => old-arch}/various-suite/gauge.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/graph-auto-migrate.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/helpers/prometheus-helpers.ts (100%) rename e2e/{scenes => old-arch}/various-suite/inspect-drawer.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/keybinds.spec.ts (88%) rename e2e/{scenes => old-arch}/various-suite/loki-editor.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/loki-query-builder.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/loki-table-explore-to-dash.spec.ts (96%) rename e2e/{scenes => old-arch}/various-suite/navigation.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/pie-chart.spec.ts (68%) rename e2e/{scenes => old-arch}/various-suite/prometheus-annotations.spec.ts (90%) rename e2e/{scenes => old-arch}/various-suite/prometheus-config.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/prometheus-editor.spec.ts (93%) rename e2e/{scenes => old-arch}/various-suite/prometheus-variable-editor.spec.ts (90%) rename e2e/{scenes => old-arch}/various-suite/query-editor.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/return-to-previous.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/select-focus.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/solo-route.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/trace-view-scrolling.spec.ts (100%) rename e2e/{scenes => old-arch}/various-suite/visualization-suggestions.spec.ts (100%) diff --git a/.betterer.results b/.betterer.results index 98a4edf52b4..0f98445256b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5,7 +5,7 @@ // exports[`better eslint`] = { value: `{ - "e2e/scenes/utils/support/types.ts:5381": [ + "e2e/old-arch/utils/support/types.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "e2e/utils/support/types.ts:5381": [ @@ -7230,7 +7230,7 @@ exports[`no undocumented stories`] = { exports[`no gf-form usage`] = { value: `{ - "e2e/scenes/utils/flows/addDataSource.ts:5381": [ + "e2e/old-arch/utils/flows/addDataSource.ts:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], "e2e/utils/flows/addDataSource.ts:5381": [ diff --git a/.drone.yml b/.drone.yml index ba023a80be6..03989a5e0e4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -660,14 +660,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-dashboards-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/dashboards-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/dashboards-suite + name: end-to-end-tests-old-arch/dashboards-suite - commands: - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite depends_on: @@ -678,14 +678,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-smoke-tests-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/smoke-tests-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/smoke-tests-suite + name: end-to-end-tests-old-arch/smoke-tests-suite - commands: - ./bin/build e2e-tests --port 3001 --suite panels-suite depends_on: @@ -696,14 +696,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-panels-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/panels-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/panels-suite + name: end-to-end-tests-old-arch/panels-suite - commands: - ./bin/build e2e-tests --port 3001 --suite various-suite depends_on: @@ -714,14 +714,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-various-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/various-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/various-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/various-suite + name: end-to-end-tests-old-arch/various-suite - commands: - cd / - ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH} @@ -2084,14 +2084,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-dashboards-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/dashboards-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/dashboards-suite + name: end-to-end-tests-old-arch/dashboards-suite - commands: - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite depends_on: @@ -2102,14 +2102,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-smoke-tests-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/smoke-tests-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/smoke-tests-suite + name: end-to-end-tests-old-arch/smoke-tests-suite - commands: - ./bin/build e2e-tests --port 3001 --suite panels-suite depends_on: @@ -2120,14 +2120,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-panels-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/panels-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/panels-suite + name: end-to-end-tests-old-arch/panels-suite - commands: - ./bin/build e2e-tests --port 3001 --suite various-suite depends_on: @@ -2138,14 +2138,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-various-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/various-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/various-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/various-suite + name: end-to-end-tests-old-arch/various-suite - commands: - cd / - ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH} @@ -6151,6 +6151,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 495b2466a038f0e208edc8cf65c78edc4795a380d2f1c1ff31d10259e4338431 +hmac: e35ebf7a31abb198c576ca8f623b63fb2bd9d84de2a6111e28b2415587d5377b ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6f45dea46d8..ca06153fe2d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -736,7 +736,6 @@ embed.go @grafana/grafana-as-code /.github/workflows/i18n-crowdin-download.yml @grafana/grafana-frontend-platform /.github/workflows/pr-go-workspace-check.yml @grafana/grafana-app-platform-squad /.github/workflows/pr-k8s-codegen-check.yml @grafana/grafana-app-platform-squad -/.github/workflows/run-scenes-e2e.yml @grafana/dashboards-squad /.github/workflows/go_lint.yml @grafana/grafana-backend-services-squad /.github/workflows/trivy-scan.yml @grafana/grafana-backend-services-squad /.github/workflows/changelog.yml @zserge diff --git a/.github/workflows/run-scenes-e2e.yml b/.github/workflows/run-scenes-e2e.yml deleted file mode 100644 index 53425680492..00000000000 --- a/.github/workflows/run-scenes-e2e.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Run dashboard scenes e2e - -on: - schedule: - - cron: "0 8 * * 1-5" # every day at 08:00UTC on weekdays - # push # uncomment for test run during PR - -env: - ARCH: linux-amd64 - -jobs: - dashboard-scenes-e2e: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Pin Go version to mod file - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - run: go version - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - run: yarn install --immutable - - name: Build grafana - run: make build - - name: Install Cypress dependencies - uses: cypress-io/github-action@v6 - with: - runTests: false - - name: Run dashboard scenes e2e - run: yarn e2e:scenes - - name: "Send Slack notification" - if: ${{ failure() }} - uses: slackapi/slack-github-action@v1.26.0 - with: - payload: > - { - "icon_emoji": ":this-is-fine-fire:", - "username": "Dashboard scenes e2e tests failed", - "text": "Link to run: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}", - "channel": "#grafana-dashboards-squad" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 3156876f8cf..2b3b25d5a08 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -25,6 +25,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | | `publicDashboards` | [Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version. | Yes | +| `publicDashboardsScene` | Enables public dashboard rendering using scenes | Yes | | `featureHighlights` | Highlight Grafana Enterprise features | | | `correlations` | Correlations page | Yes | | `autoMigrateXYChartPanel` | Migrate old XYChart panel to new XYChart2 model | Yes | @@ -56,6 +57,9 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `managedPluginsInstall` | Install managed plugins directly from plugins catalog | 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 | +| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes | +| `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | | `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes | | `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | | `exploreMetrics` | Enables the new Explore Metrics core app | Yes | @@ -117,7 +121,6 @@ Experimental features might be changed or removed without prior notice. | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | | `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | -| `publicDashboardsScene` | Enables public dashboard rendering using scenes | | `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | | `storage` | Configurable storage for dashboards, datasources, and resources | | `canvasPanelNesting` | Allow elements nesting | @@ -171,9 +174,6 @@ Experimental features might be changed or removed without prior notice. | `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | | `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | | `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | -| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | -| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | -| `dashboardScene` | Enables dashboard rendering using scenes for all roles | | `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | | `tableSharedCrosshair` | Enables shared crosshair in table panel | | `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | diff --git a/e2e/cypress/support/e2e.js b/e2e/cypress/support/e2e.js index 39bab3d3c21..3e03d0f4344 100644 --- a/e2e/cypress/support/e2e.js +++ b/e2e/cypress/support/e2e.js @@ -46,8 +46,8 @@ Cypress.on('uncaught:exception', (err) => { // beforeEach(() => { - if (Cypress.env('SCENES')) { - cy.logToConsole('enabling dashboardScene feature toggle in localstorage'); - cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=true'); + if (Cypress.env('DISABLE_SCENES')) { + cy.logToConsole('disabling dashboardScene feature toggle in localstorage'); + cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=false'); } }); diff --git a/e2e/dashboards-suite/Repeating_a_panel_horizontally.spec.ts b/e2e/dashboards-suite/Repeating_a_panel_horizontally.spec.ts index e34acc89d81..9b3ec4806e0 100644 --- a/e2e/dashboards-suite/Repeating_a_panel_horizontally.spec.ts +++ b/e2e/dashboards-suite/Repeating_a_panel_horizontally.spec.ts @@ -37,9 +37,15 @@ describe('Repeating a panel horizontally', () => { }); // Change to only show panels 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal') + .parent() + .within(() => { + cy.get('input').click(); + }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); + // blur the dropdown cy.get('body').click(); diff --git a/e2e/dashboards-suite/Repeating_a_panel_vertically.spec.ts b/e2e/dashboards-suite/Repeating_a_panel_vertically.spec.ts index 2b6a51f0515..fbf0f5bf9d2 100644 --- a/e2e/dashboards-suite/Repeating_a_panel_vertically.spec.ts +++ b/e2e/dashboards-suite/Repeating_a_panel_vertically.spec.ts @@ -38,9 +38,15 @@ describe('Repeating a panel vertically', () => { }); // Change to only show panels 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical') + .parent() + .within(() => { + cy.get('input').click(); + }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); + // blur the dropdown cy.get('body').click(); diff --git a/e2e/dashboards-suite/Repeating_an_empty_row.spec.ts b/e2e/dashboards-suite/Repeating_an_empty_row.spec.ts index 8777ea3e065..2bf52250bad 100644 --- a/e2e/dashboards-suite/Repeating_an_empty_row.spec.ts +++ b/e2e/dashboards-suite/Repeating_an_empty_row.spec.ts @@ -32,10 +32,15 @@ describe('Repeating empty rows', () => { e2e.components.DashboardRow.title(title).should('be.visible'); }); - // Change to only show rows 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('row').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('row') + .parent() + .within(() => { + cy.get('input').click(); + }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); + // blur the dropdown cy.get('body').click(); diff --git a/e2e/dashboards-suite/dashboard-browse.spec.ts b/e2e/dashboards-suite/dashboard-browse.spec.ts index a39c0ca373d..3bd04f4e338 100644 --- a/e2e/dashboards-suite/dashboard-browse.spec.ts +++ b/e2e/dashboards-suite/dashboard-browse.spec.ts @@ -1,7 +1,7 @@ import testDashboard from '../dashboards/TestDashboard.json'; import { e2e } from '../utils'; - -describe('Dashboard browse', () => { +// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-browse.spec.ts +describe.skip('Dashboard browse', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); diff --git a/e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts b/e2e/dashboards-suite/dashboard-export-json.spec.ts similarity index 98% rename from e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts rename to e2e/dashboards-suite/dashboard-export-json.spec.ts index 6795c5680a7..34abe6cec05 100644 --- a/e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts +++ b/e2e/dashboards-suite/dashboard-export-json.spec.ts @@ -1,5 +1,5 @@ import { e2e } from '../utils'; -import '../../utils/support/clipboard'; +import '../utils/support/clipboard'; describe('Export as JSON', () => { beforeEach(() => { diff --git a/e2e/scenes/dashboards-suite/dashboard-keybindings.spec.ts b/e2e/dashboards-suite/dashboard-keybindings.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/dashboard-keybindings.spec.ts rename to e2e/dashboards-suite/dashboard-keybindings.spec.ts diff --git a/e2e/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e/dashboards-suite/dashboard-live-streaming.spec.ts index b91ccf5c270..089b3ec4beb 100644 --- a/e2e/dashboards-suite/dashboard-live-streaming.spec.ts +++ b/e2e/dashboards-suite/dashboard-live-streaming.spec.ts @@ -1,7 +1,8 @@ import testDashboard from '../dashboards/DashboardLiveTest.json'; import { e2e } from '../utils'; -describe('Dashboard Live streaming support', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/dashboard-live-streaming.spec.ts +describe.skip('Dashboard Live streaming support', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.importDashboard(testDashboard, 1000); diff --git a/e2e/dashboards-suite/dashboard-public-create.spec.ts b/e2e/dashboards-suite/dashboard-public-create.spec.ts index 6fa08c988e5..993bb82c702 100644 --- a/e2e/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-public-create.spec.ts @@ -1,6 +1,6 @@ import { e2e } from '../utils'; - -describe('Public dashboards', () => { +// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-public-create.spec.ts +describe.skip('Public dashboards', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -14,7 +14,7 @@ describe('Public dashboards', () => { cy.wait('@query'); // Open sharing modal - e2e.pages.Dashboard.DashNav.shareButton().click(); + e2e.components.NavToolbar.shareDashboard().click(); // Select public dashboards tab e2e.components.Tab.title('Public dashboard').click(); @@ -74,7 +74,7 @@ describe('Public dashboards', () => { e2e.pages.Dashboard.DashNav.publicDashboardTag().should('exist'); // Open sharing modal - e2e.pages.Dashboard.DashNav.shareButton().click(); + e2e.components.NavToolbar.shareDashboard().click(); // Select public dashboards tab cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); @@ -114,7 +114,7 @@ describe('Public dashboards', () => { cy.wait('@query'); // Open sharing modal - e2e.pages.Dashboard.DashNav.shareButton().click(); + e2e.components.NavToolbar.shareDashboard().click(); // Select public dashboards tab cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); diff --git a/e2e/dashboards-suite/dashboard-public-templating.spec.ts b/e2e/dashboards-suite/dashboard-public-templating.spec.ts index fa3a4a91242..4a4e660d44e 100644 --- a/e2e/dashboards-suite/dashboard-public-templating.spec.ts +++ b/e2e/dashboards-suite/dashboard-public-templating.spec.ts @@ -10,10 +10,10 @@ describe('Create a public dashboard with template variables shows a template var e2e.flows.openDashboard({ uid: 'HYaGDGIMk' }); // Open sharing modal - e2e.pages.Dashboard.DashNav.shareButton().click(); + e2e.components.NavToolbar.shareDashboard().click(); // Select public dashboards tab - e2e.components.Tab.title('Public dashboard').click(); + e2e.components.Tab.title('Public Dashboard').click(); // Warning Alert dashboard cannot be made public because it has template variables e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e/dashboards-suite/dashboard-share-externally-create.spec.ts similarity index 97% rename from e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts rename to e2e/dashboards-suite/dashboard-share-externally-create.spec.ts index 60642f9e0ef..39e01c4e60f 100644 --- a/e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -1,6 +1,6 @@ -import { PublicDashboard } from '../../../public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; +import { PublicDashboard } from '../../public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { e2e } from '../utils'; -import '../../utils/support/clipboard'; +import '../utils/support/clipboard'; describe('Shared dashboards', () => { beforeEach(() => { diff --git a/e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts b/e2e/dashboards-suite/dashboard-share-internally.spec.ts similarity index 98% rename from e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts rename to e2e/dashboards-suite/dashboard-share-internally.spec.ts index 96b7291d40f..76773fa7f71 100644 --- a/e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts +++ b/e2e/dashboards-suite/dashboard-share-internally.spec.ts @@ -1,6 +1,6 @@ -import { ShareLinkConfiguration } from '../../../public/app/features/dashboard-scene/sharing/ShareButton/utils'; +import { ShareLinkConfiguration } from '../../public/app/features/dashboard-scene/sharing/ShareButton/utils'; import { e2e } from '../utils'; -import '../../utils/support/clipboard'; +import '../utils/support/clipboard'; describe('Share internally', () => { beforeEach(() => { diff --git a/e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts b/e2e/dashboards-suite/dashboard-share-snapshot-create.spec.ts similarity index 93% rename from e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts rename to e2e/dashboards-suite/dashboard-share-snapshot-create.spec.ts index 808ebc69ee8..6661750718e 100644 --- a/e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-share-snapshot-create.spec.ts @@ -1,7 +1,7 @@ -import { SnapshotCreateResponse } from '../../../public/app/features/dashboard/services/SnapshotSrv'; -import { fromBaseUrl } from '../../utils/support/url'; +import { SnapshotCreateResponse } from '../../public/app/features/dashboard/services/SnapshotSrv'; import { e2e } from '../utils'; -import '../../utils/support/clipboard'; +import { fromBaseUrl } from '../utils/support/url'; +import '../utils/support/clipboard'; describe('Snapshots', () => { beforeEach(() => { diff --git a/e2e/dashboards-suite/dashboard-templating.spec.ts b/e2e/dashboards-suite/dashboard-templating.spec.ts index 71b9fcbf7db..4e885df1258 100644 --- a/e2e/dashboards-suite/dashboard-templating.spec.ts +++ b/e2e/dashboards-suite/dashboard-templating.spec.ts @@ -34,7 +34,7 @@ describe('Dashboard templating', () => { `Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`, `Server:date = NaN`, `Server:text = All`, - `Server:queryparam = var-Server=All`, + `Server:queryparam = var-Server=A%27A%22A&var-Server=BB%5CB&var-Server=CCC`, `1 < 2`, `Example: from=now-6h&to=now`, ]; diff --git a/e2e/dashboards-suite/dashboard-time-zone.spec.ts b/e2e/dashboards-suite/dashboard-time-zone.spec.ts index 3c2844188e3..0361e84a236 100644 --- a/e2e/dashboards-suite/dashboard-time-zone.spec.ts +++ b/e2e/dashboards-suite/dashboard-time-zone.spec.ts @@ -81,7 +81,8 @@ describe('Dashboard time zone support', () => { } }); - it('Tests relative timezone support and overrides', () => { + // TODO: remove skip once https://github.com/grafana/grafana/issues/86420 is done + it.skip('Tests relative timezone support and overrides', () => { // Open dashboard e2e.flows.openDashboard({ uid: 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8', @@ -123,7 +124,6 @@ describe('Dashboard time zone support', () => { .within(() => { cy.contains('[role="row"]', '00:00:00').should('be.visible'); }); - // Test UTC timezone e2e.flows.setTimeRange({ from: 'now-6h', diff --git a/e2e/dashboards-suite/general-dashboards.spec.ts b/e2e/dashboards-suite/general-dashboards.spec.ts index 1d01c82267c..bc1b2a306be 100644 --- a/e2e/dashboards-suite/general-dashboards.spec.ts +++ b/e2e/dashboards-suite/general-dashboards.spec.ts @@ -22,9 +22,9 @@ describe('Dashboards', () => { // Then we open and close the panel editor e2e.components.Panels.Panel.menu('Panel #50').click({ force: true }); // it only shows on hover e2e.components.Panels.Panel.menuItems('Edit').click(); - e2e.components.PanelEditor.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); - // And the last panel should still be visible! + // The last panel should still be visible! e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); }); }); diff --git a/e2e/dashboards-suite/load-options-from-url.spec.ts b/e2e/dashboards-suite/load-options-from-url.spec.ts index c1678a0aa40..d674ae2a385 100644 --- a/e2e/dashboards-suite/load-options-from-url.spec.ts +++ b/e2e/dashboards-suite/load-options-from-url.spec.ts @@ -16,40 +16,50 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB').should('be.visible'); @@ -65,40 +75,50 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); @@ -125,24 +145,25 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + cy.get('body').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') .should('be.visible') - .within(() => { - e2e.components.Variables.variableOption().should('have.length', 65); - }); + .should('have.length', 2); }); }); diff --git a/e2e/dashboards-suite/new-constant-variable.spec.ts b/e2e/dashboards-suite/new-constant-variable.spec.ts index 5cb43e21359..42693acb668 100644 --- a/e2e/dashboards-suite/new-constant-variable.spec.ts +++ b/e2e/dashboards-suite/new-constant-variable.spec.ts @@ -9,7 +9,7 @@ describe('Variables - Constant', () => { }); it('can add a new constant variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Constant" variable @@ -22,11 +22,11 @@ describe('Variables - Constant', () => { e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type('pesto').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto'); + // e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto'); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.components.RefreshPicker.runButtonV2().click(); // Assert it was rendered diff --git a/e2e/dashboards-suite/new-custom-variable.spec.ts b/e2e/dashboards-suite/new-custom-variable.spec.ts index 7b75622aff7..bda748fbdf2 100644 --- a/e2e/dashboards-suite/new-custom-variable.spec.ts +++ b/e2e/dashboards-suite/new-custom-variable.spec.ts @@ -25,7 +25,7 @@ describe('Variables - Custom', () => { }); it('can add a custom template variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Custom" variable @@ -34,17 +34,16 @@ describe('Variables - Custom', () => { assertPreviewValues(['one', 'two', 'three']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('two').click(); - + e2e.components.Select.option().contains('two').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: two'); }); it('can add a custom template variable with labels', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Custom" variable @@ -58,10 +57,10 @@ describe('Variables - Custom', () => { assertPreviewValues(['One', 'Two', 'Three']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('One').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('Two').click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1').click(); + e2e.components.Select.option().contains('Two').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 2'); diff --git a/e2e/dashboards-suite/new-datasource-variable.spec.ts b/e2e/dashboards-suite/new-datasource-variable.spec.ts index 40260a76520..b56e458567c 100644 --- a/e2e/dashboards-suite/new-datasource-variable.spec.ts +++ b/e2e/dashboards-suite/new-datasource-variable.spec.ts @@ -3,16 +3,14 @@ import { e2e } from '../utils'; const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; const DASHBOARD_NAME = 'Test variable output'; -const gdev_mysql = 'gdev-mysql'; -const gdev_mysql_ds_tests = 'gdev-mysql-ds-tests'; - -describe('Variables - Datasource', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/new-datasource-variable.spec.ts +describe.skip('Variables - Datasource', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); it('can add a new datasource variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Datasource" variable @@ -23,26 +21,30 @@ describe('Variables - Datasource', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); - // If this is failing, but sure to check there are MySQL datasources named "gdev-mysql" and "gdev-mysql-ds-tests" + // If this is failing, but sure to check there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus" // Or, just update is to match some gdev datasources to test with :) e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().within(() => { - cy.get('input').type('MySQL{enter}'); + cy.get('input').type('Prometheus{enter}'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('contain.text', gdev_mysql); e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should( 'contain.text', - gdev_mysql_ds_tests + 'gdev-prometheus' + ); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should( + 'contain.text', + 'gdev-slow-prometheus' ); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.components.RefreshPicker.runButtonV2().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(gdev_mysql).click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(gdev_mysql_ds_tests).click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus').click(); + e2e.components.Select.option().contains('gdev-slow-prometheus').click(); // Assert it was rendered - cy.get('.markdown-html').should('include.text', `VariableUnderTestText: ${gdev_mysql_ds_tests}`); + cy.get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus-uid'); + cy.get('.markdown-html').should('include.text', 'VariableUnderTestText: gdev-slow-prometheus'); }); }); diff --git a/e2e/dashboards-suite/new-interval-variable.spec.ts b/e2e/dashboards-suite/new-interval-variable.spec.ts index f99c7a2d8b2..88e1052ce5e 100644 --- a/e2e/dashboards-suite/new-interval-variable.spec.ts +++ b/e2e/dashboards-suite/new-interval-variable.spec.ts @@ -16,7 +16,7 @@ describe('Variables - Interval', () => { }); it('can add a new interval variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Interval" variable @@ -35,12 +35,13 @@ describe('Variables - Interval', () => { assertPreviewValues(['10s', '10m', '60m', '90m', '1h30m']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.components.RefreshPicker.runButtonV2().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('10s').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1h30m').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test').next().should('have.text', `10s`).click(); + e2e.components.Select.option().contains('1h30m').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 1h30m'); diff --git a/e2e/dashboards-suite/new-query-variable.spec.ts b/e2e/dashboards-suite/new-query-variable.spec.ts index c5edb437147..d6ead2937c4 100644 --- a/e2e/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/dashboards-suite/new-query-variable.spec.ts @@ -1,3 +1,5 @@ +import { selectors } from '@grafana/e2e-selectors'; + import { e2e } from '../utils'; const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; @@ -9,10 +11,12 @@ describe('Variables - Query - Add variable', () => { }); it('query variable should be default and default fields should be correct', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); + cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) + .should('be.visible') + .click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2() .should('be.visible') @@ -68,15 +72,17 @@ describe('Variables - Query - Add variable', () => { cy.get('input[type="checkbox"]').should('not.be.checked'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.have.text'); e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist'); }); it('adding a single value query variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); + cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) + .should('be.visible') + .click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') @@ -101,29 +107,27 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItem() .should('have.length', 4) .eq(3) .within(() => { - e2e.components.Variables.variableLinkWrapper().should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() - .should('be.visible') - .within(() => { - e2e.components.Variables.variableOption().should('have.length', 1); - }); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); + cy.get('input').click(); }); + + e2e.components.Select.option().should('have.length', 1); + e2e.components.Select.option().contains('C'); }); it('adding a multi value query variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); + cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) + .should('be.visible') + .click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') @@ -161,22 +165,21 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItem() .should('have.length', 4) .eq(3) .within(() => { - e2e.components.Variables.variableLinkWrapper().should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() - .should('be.visible') - .within(() => { - e2e.components.Variables.variableOption().should('have.length', 2); - }); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); + cy.get('input').click(); }); + + e2e.components.Select.option().should('have.length', 3); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + + e2e.components.Select.option().contains('All'); + e2e.components.Select.option().contains('C'); }); }); diff --git a/e2e/dashboards-suite/new-text-box-variable.spec.ts b/e2e/dashboards-suite/new-text-box-variable.spec.ts index b6cbabceef8..7da9afa32bf 100644 --- a/e2e/dashboards-suite/new-text-box-variable.spec.ts +++ b/e2e/dashboards-suite/new-text-box-variable.spec.ts @@ -9,26 +9,25 @@ describe('Variables - Text box', () => { }); it('can add a new text box variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "text box" variable e2e.components.CallToActionCard.buttonV2('Add variable').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => { - cy.get('input').type('Text box{enter}'); + cy.get('input').type('Textbox{enter}'); }); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2().type('cat-dog').blur(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'cat-dog'); - // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); - cy.get('#var-VariableUnderTest').clear().type('dog-cat').blur(); - + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.SubMenu.submenuItem().within(() => { + cy.get('input').clear().type('dog-cat').blur(); + }); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: dog-cat'); }); diff --git a/e2e/dashboards-suite/set-options-from-ui.spec.ts b/e2e/dashboards-suite/set-options-from-ui.spec.ts index d7ec950487b..bdcc5c6b516 100644 --- a/e2e/dashboards-suite/set-options-from-ui.spec.ts +++ b/e2e/dashboards-suite/set-options-from-ui.spec.ts @@ -1,3 +1,5 @@ +import { selectors } from '@grafana/e2e-selectors'; + import { e2e } from '../utils'; const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; @@ -10,39 +12,46 @@ describe('Variables - Set options from ui', () => { it('clicking a value that is not part of dependents options should change these to All', () => { e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` }); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + .should('be.visible') + .within(() => { + cy.get('input').click(); + }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); - e2e.components.NavToolbar.container().click(); + cy.get('body').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') .should('have.length', 2) .eq(0) - .should('be.visible') - .click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); + cy.get('body').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() - .should('be.visible') + e2e.pages.Dashboard.SubMenu.submenuItemLabels('pod') + .parent() .within(() => { - e2e.components.Variables.variableOption().should('have.length', 65); + cy.get('input').click(); }); + // length is 11 because of virtualized select options + e2e.components.Select.option().parent().should('have.length', 11); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAB').should('be.visible'); @@ -60,41 +69,47 @@ describe('Variables - Set options from ui', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + .should('be.visible') + .within(() => { + cy.get('input').click(); + }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); - e2e.components.NavToolbar.container().click(); + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (2)'); + + cy.get('body').click(); cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').scrollIntoView().should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B').scrollIntoView().should('be.visible'); - e2e.components.LoadingIndicator.icon().should('have.length', 0); + cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 17); + cy.get('input').click(); }); + e2e.components.Select.option().should('have.length', 11); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AD').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA').should('be.visible').click(); + cy.get('body').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().should('have.length', 10); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB').should('be.visible'); @@ -108,38 +123,46 @@ describe('Variables - Set options from ui', () => { cy.intercept({ pathname: '/api/ds/query' }).as('query'); cy.wait('@query'); + cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); - - e2e.components.NavToolbar.container().click(); - - cy.wait('@query'); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); - - e2e.components.LoadingIndicator.icon().should('have.length', 0); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); + + cy.get('body').click(); + + cy.wait('@query'); + cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').should('be.visible'); + + cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') + .should('be.visible') + .within(() => { + cy.get('input').click(); + }); + + e2e.components.Select.option().should('have.length', 10); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().should('have.length', 10); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/snapshot-create.spec.ts b/e2e/dashboards-suite/snapshot-create.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/snapshot-create.spec.ts rename to e2e/dashboards-suite/snapshot-create.spec.ts diff --git a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index 06e13002db4..829303aab41 100644 --- a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -27,21 +27,27 @@ describe('Templating', () => { expect(links).to.have.length.greaterThan(13); for (let index = 0; index < links.length; index++) { - expect(Cypress.$(links[index]).attr('href')).contains(`var-custom=${variableValue}`); + expect(Cypress.$(links[index]).attr('href')).contains(variableValue); } }); }; e2e.components.DashboardLinks.dropDown().should('be.visible').click().wait('@tagsTemplatingSearch'); - // verify all links, should have All value - verifyLinks('All'); + verifyLinks('var-custom=p1&var-custom=p2&var-custom=p3'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); + cy.get('body').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + .should('be.visible') + .within(() => { + cy.get('input').click(); + }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2').should('be.visible').click(); - e2e.components.NavToolbar.container().click(); + cy.get('body').click(); + e2e.components.DashboardLinks.dropDown() .scrollIntoView() .should('be.visible') diff --git a/e2e/scenes/dashboards-suite/Repeating_a_panel_horizontally.spec.ts b/e2e/old-arch/dashboards-suite/Repeating_a_panel_horizontally.spec.ts similarity index 95% rename from e2e/scenes/dashboards-suite/Repeating_a_panel_horizontally.spec.ts rename to e2e/old-arch/dashboards-suite/Repeating_a_panel_horizontally.spec.ts index 9b3ec4806e0..e34acc89d81 100644 --- a/e2e/scenes/dashboards-suite/Repeating_a_panel_horizontally.spec.ts +++ b/e2e/old-arch/dashboards-suite/Repeating_a_panel_horizontally.spec.ts @@ -37,15 +37,9 @@ describe('Repeating a panel horizontally', () => { }); // Change to only show panels 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal') - .parent() - .within(() => { - cy.get('input').click(); - }); - + e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); - // blur the dropdown cy.get('body').click(); diff --git a/e2e/scenes/dashboards-suite/Repeating_a_panel_vertically.spec.ts b/e2e/old-arch/dashboards-suite/Repeating_a_panel_vertically.spec.ts similarity index 95% rename from e2e/scenes/dashboards-suite/Repeating_a_panel_vertically.spec.ts rename to e2e/old-arch/dashboards-suite/Repeating_a_panel_vertically.spec.ts index fbf0f5bf9d2..2b6a51f0515 100644 --- a/e2e/scenes/dashboards-suite/Repeating_a_panel_vertically.spec.ts +++ b/e2e/old-arch/dashboards-suite/Repeating_a_panel_vertically.spec.ts @@ -38,15 +38,9 @@ describe('Repeating a panel vertically', () => { }); // Change to only show panels 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical') - .parent() - .within(() => { - cy.get('input').click(); - }); - + e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); - // blur the dropdown cy.get('body').click(); diff --git a/e2e/scenes/dashboards-suite/Repeating_an_empty_row.spec.ts b/e2e/old-arch/dashboards-suite/Repeating_an_empty_row.spec.ts similarity index 94% rename from e2e/scenes/dashboards-suite/Repeating_an_empty_row.spec.ts rename to e2e/old-arch/dashboards-suite/Repeating_an_empty_row.spec.ts index 2bf52250bad..8777ea3e065 100644 --- a/e2e/scenes/dashboards-suite/Repeating_an_empty_row.spec.ts +++ b/e2e/old-arch/dashboards-suite/Repeating_an_empty_row.spec.ts @@ -32,15 +32,10 @@ describe('Repeating empty rows', () => { e2e.components.DashboardRow.title(title).should('be.visible'); }); - e2e.pages.Dashboard.SubMenu.submenuItemLabels('row') - .parent() - .within(() => { - cy.get('input').click(); - }); - + // Change to only show rows 1 + 3 + e2e.pages.Dashboard.SubMenu.submenuItemLabels('row').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); - // blur the dropdown cy.get('body').click(); diff --git a/e2e/scenes/dashboards-suite/dashboard-browse-nested.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-browse-nested.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/dashboard-browse-nested.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-browse-nested.spec.ts diff --git a/e2e/scenes/dashboards-suite/dashboard-browse.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-browse.spec.ts similarity index 93% rename from e2e/scenes/dashboards-suite/dashboard-browse.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-browse.spec.ts index a5dc3cfc5ca..4e9728b7a65 100644 --- a/e2e/scenes/dashboards-suite/dashboard-browse.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-browse.spec.ts @@ -1,7 +1,7 @@ import testDashboard from '../dashboards/TestDashboard.json'; import { e2e } from '../utils'; -// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-browse.spec.ts -describe.skip('Dashboard browse', () => { + +describe('Dashboard browse', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); diff --git a/e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-live-streaming.spec.ts similarity index 75% rename from e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-live-streaming.spec.ts index 089b3ec4beb..b91ccf5c270 100644 --- a/e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-live-streaming.spec.ts @@ -1,8 +1,7 @@ import testDashboard from '../dashboards/DashboardLiveTest.json'; import { e2e } from '../utils'; -// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/dashboard-live-streaming.spec.ts -describe.skip('Dashboard Live streaming support', () => { +describe('Dashboard Live streaming support', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.importDashboard(testDashboard, 1000); diff --git a/e2e/dashboards-suite/dashboard-panel-attention.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-panel-attention.spec.ts similarity index 100% rename from e2e/dashboards-suite/dashboard-panel-attention.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-panel-attention.spec.ts diff --git a/e2e/scenes/dashboards-suite/dashboard-public-create.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-public-create.spec.ts similarity index 95% rename from e2e/scenes/dashboards-suite/dashboard-public-create.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-public-create.spec.ts index 993bb82c702..6fa08c988e5 100644 --- a/e2e/scenes/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-public-create.spec.ts @@ -1,6 +1,6 @@ import { e2e } from '../utils'; -// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-public-create.spec.ts -describe.skip('Public dashboards', () => { + +describe('Public dashboards', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -14,7 +14,7 @@ describe.skip('Public dashboards', () => { cy.wait('@query'); // Open sharing modal - e2e.components.NavToolbar.shareDashboard().click(); + e2e.pages.Dashboard.DashNav.shareButton().click(); // Select public dashboards tab e2e.components.Tab.title('Public dashboard').click(); @@ -74,7 +74,7 @@ describe.skip('Public dashboards', () => { e2e.pages.Dashboard.DashNav.publicDashboardTag().should('exist'); // Open sharing modal - e2e.components.NavToolbar.shareDashboard().click(); + e2e.pages.Dashboard.DashNav.shareButton().click(); // Select public dashboards tab cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); @@ -114,7 +114,7 @@ describe.skip('Public dashboards', () => { cy.wait('@query'); // Open sharing modal - e2e.components.NavToolbar.shareDashboard().click(); + e2e.pages.Dashboard.DashNav.shareButton().click(); // Select public dashboards tab cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); diff --git a/e2e/scenes/dashboards-suite/dashboard-public-templating.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-public-templating.spec.ts similarity index 91% rename from e2e/scenes/dashboards-suite/dashboard-public-templating.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-public-templating.spec.ts index 4a4e660d44e..fa3a4a91242 100644 --- a/e2e/scenes/dashboards-suite/dashboard-public-templating.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-public-templating.spec.ts @@ -10,10 +10,10 @@ describe('Create a public dashboard with template variables shows a template var e2e.flows.openDashboard({ uid: 'HYaGDGIMk' }); // Open sharing modal - e2e.components.NavToolbar.shareDashboard().click(); + e2e.pages.Dashboard.DashNav.shareButton().click(); // Select public dashboards tab - e2e.components.Tab.title('Public Dashboard').click(); + e2e.components.Tab.title('Public dashboard').click(); // Warning Alert dashboard cannot be made public because it has template variables e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/dashboard-templating.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-templating.spec.ts similarity index 95% rename from e2e/scenes/dashboards-suite/dashboard-templating.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-templating.spec.ts index 4e885df1258..71b9fcbf7db 100644 --- a/e2e/scenes/dashboards-suite/dashboard-templating.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-templating.spec.ts @@ -34,7 +34,7 @@ describe('Dashboard templating', () => { `Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`, `Server:date = NaN`, `Server:text = All`, - `Server:queryparam = var-Server=A%27A%22A&var-Server=BB%5CB&var-Server=CCC`, + `Server:queryparam = var-Server=All`, `1 < 2`, `Example: from=now-6h&to=now`, ]; diff --git a/e2e/scenes/dashboards-suite/dashboard-time-zone.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-time-zone.spec.ts similarity index 98% rename from e2e/scenes/dashboards-suite/dashboard-time-zone.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-time-zone.spec.ts index 0361e84a236..3c2844188e3 100644 --- a/e2e/scenes/dashboards-suite/dashboard-time-zone.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-time-zone.spec.ts @@ -81,8 +81,7 @@ describe('Dashboard time zone support', () => { } }); - // TODO: remove skip once https://github.com/grafana/grafana/issues/86420 is done - it.skip('Tests relative timezone support and overrides', () => { + it('Tests relative timezone support and overrides', () => { // Open dashboard e2e.flows.openDashboard({ uid: 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8', @@ -124,6 +123,7 @@ describe('Dashboard time zone support', () => { .within(() => { cy.contains('[role="row"]', '00:00:00').should('be.visible'); }); + // Test UTC timezone e2e.flows.setTimeRange({ from: 'now-6h', diff --git a/e2e/scenes/dashboards-suite/dashboard-timepicker.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-timepicker.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/dashboard-timepicker.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-timepicker.spec.ts diff --git a/e2e/scenes/dashboards-suite/embedded-dashboard.spec.ts b/e2e/old-arch/dashboards-suite/embedded-dashboard.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/embedded-dashboard.spec.ts rename to e2e/old-arch/dashboards-suite/embedded-dashboard.spec.ts diff --git a/e2e/scenes/dashboards-suite/general-dashboards.spec.ts b/e2e/old-arch/dashboards-suite/general-dashboards.spec.ts similarity index 87% rename from e2e/scenes/dashboards-suite/general-dashboards.spec.ts rename to e2e/old-arch/dashboards-suite/general-dashboards.spec.ts index bc1b2a306be..1d01c82267c 100644 --- a/e2e/scenes/dashboards-suite/general-dashboards.spec.ts +++ b/e2e/old-arch/dashboards-suite/general-dashboards.spec.ts @@ -22,9 +22,9 @@ describe('Dashboards', () => { // Then we open and close the panel editor e2e.components.Panels.Panel.menu('Panel #50').click({ force: true }); // it only shows on hover e2e.components.Panels.Panel.menuItems('Edit').click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.components.PanelEditor.applyButton().click(); - // The last panel should still be visible! + // And the last panel should still be visible! e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); }); }); diff --git a/e2e/scenes/dashboards-suite/import-dashboard.spec.ts b/e2e/old-arch/dashboards-suite/import-dashboard.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/import-dashboard.spec.ts rename to e2e/old-arch/dashboards-suite/import-dashboard.spec.ts diff --git a/e2e/scenes/dashboards-suite/load-options-from-url.spec.ts b/e2e/old-arch/dashboards-suite/load-options-from-url.spec.ts similarity index 75% rename from e2e/scenes/dashboards-suite/load-options-from-url.spec.ts rename to e2e/old-arch/dashboards-suite/load-options-from-url.spec.ts index d674ae2a385..c1678a0aa40 100644 --- a/e2e/scenes/dashboards-suite/load-options-from-url.spec.ts +++ b/e2e/old-arch/dashboards-suite/load-options-from-url.spec.ts @@ -16,50 +16,40 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB').should('be.visible'); @@ -75,50 +65,40 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); @@ -145,25 +125,24 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - cy.get('body').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') - .should('have.length', 2); + .within(() => { + e2e.components.Variables.variableOption().should('have.length', 65); + }); }); }); diff --git a/e2e/scenes/dashboards-suite/new-constant-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-constant-variable.spec.ts similarity index 83% rename from e2e/scenes/dashboards-suite/new-constant-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-constant-variable.spec.ts index 42693acb668..5cb43e21359 100644 --- a/e2e/scenes/dashboards-suite/new-constant-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-constant-variable.spec.ts @@ -9,7 +9,7 @@ describe('Variables - Constant', () => { }); it('can add a new constant variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Constant" variable @@ -22,11 +22,11 @@ describe('Variables - Constant', () => { e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type('pesto').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); - // e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto'); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.components.RefreshPicker.runButtonV2().click(); // Assert it was rendered diff --git a/e2e/scenes/dashboards-suite/new-custom-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-custom-variable.spec.ts similarity index 83% rename from e2e/scenes/dashboards-suite/new-custom-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-custom-variable.spec.ts index bda748fbdf2..7b75622aff7 100644 --- a/e2e/scenes/dashboards-suite/new-custom-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-custom-variable.spec.ts @@ -25,7 +25,7 @@ describe('Variables - Custom', () => { }); it('can add a custom template variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Custom" variable @@ -34,16 +34,17 @@ describe('Variables - Custom', () => { assertPreviewValues(['one', 'two', 'three']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one').click(); - e2e.components.Select.option().contains('two').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('two').click(); + // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: two'); }); it('can add a custom template variable with labels', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Custom" variable @@ -57,10 +58,10 @@ describe('Variables - Custom', () => { assertPreviewValues(['One', 'Two', 'Three']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1').click(); - e2e.components.Select.option().contains('Two').click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('One').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('Two').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 2'); diff --git a/e2e/scenes/dashboards-suite/new-datasource-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-datasource-variable.spec.ts similarity index 61% rename from e2e/scenes/dashboards-suite/new-datasource-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-datasource-variable.spec.ts index b56e458567c..40260a76520 100644 --- a/e2e/scenes/dashboards-suite/new-datasource-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-datasource-variable.spec.ts @@ -3,14 +3,16 @@ import { e2e } from '../utils'; const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; const DASHBOARD_NAME = 'Test variable output'; -// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/new-datasource-variable.spec.ts -describe.skip('Variables - Datasource', () => { +const gdev_mysql = 'gdev-mysql'; +const gdev_mysql_ds_tests = 'gdev-mysql-ds-tests'; + +describe('Variables - Datasource', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); it('can add a new datasource variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Datasource" variable @@ -21,30 +23,26 @@ describe.skip('Variables - Datasource', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); - // If this is failing, but sure to check there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus" + // If this is failing, but sure to check there are MySQL datasources named "gdev-mysql" and "gdev-mysql-ds-tests" // Or, just update is to match some gdev datasources to test with :) e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().within(() => { - cy.get('input').type('Prometheus{enter}'); + cy.get('input').type('MySQL{enter}'); }); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('contain.text', gdev_mysql); e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should( 'contain.text', - 'gdev-prometheus' - ); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should( - 'contain.text', - 'gdev-slow-prometheus' + gdev_mysql_ds_tests ); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.components.RefreshPicker.runButtonV2().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus').click(); - e2e.components.Select.option().contains('gdev-slow-prometheus').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(gdev_mysql).click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(gdev_mysql_ds_tests).click(); // Assert it was rendered - cy.get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus-uid'); - cy.get('.markdown-html').should('include.text', 'VariableUnderTestText: gdev-slow-prometheus'); + cy.get('.markdown-html').should('include.text', `VariableUnderTestText: ${gdev_mysql_ds_tests}`); }); }); diff --git a/e2e/scenes/dashboards-suite/new-interval-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-interval-variable.spec.ts similarity index 82% rename from e2e/scenes/dashboards-suite/new-interval-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-interval-variable.spec.ts index 88e1052ce5e..f99c7a2d8b2 100644 --- a/e2e/scenes/dashboards-suite/new-interval-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-interval-variable.spec.ts @@ -16,7 +16,7 @@ describe('Variables - Interval', () => { }); it('can add a new interval variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Interval" variable @@ -35,13 +35,12 @@ describe('Variables - Interval', () => { assertPreviewValues(['10s', '10m', '60m', '90m', '1h30m']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); - + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.components.RefreshPicker.runButtonV2().click(); - e2e.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test').next().should('have.text', `10s`).click(); - e2e.components.Select.option().contains('1h30m').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('10s').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1h30m').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 1h30m'); diff --git a/e2e/scenes/dashboards-suite/new-query-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-query-variable.spec.ts similarity index 80% rename from e2e/scenes/dashboards-suite/new-query-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-query-variable.spec.ts index d6ead2937c4..c5edb437147 100644 --- a/e2e/scenes/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-query-variable.spec.ts @@ -1,5 +1,3 @@ -import { selectors } from '@grafana/e2e-selectors'; - import { e2e } from '../utils'; const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; @@ -11,12 +9,10 @@ describe('Variables - Query - Add variable', () => { }); it('query variable should be default and default fields should be correct', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) - .should('be.visible') - .click(); + e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2() .should('be.visible') @@ -72,17 +68,15 @@ describe('Variables - Query - Add variable', () => { cy.get('input[type="checkbox"]').should('not.be.checked'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.have.text'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist'); e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist'); }); it('adding a single value query variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) - .should('be.visible') - .click(); + e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') @@ -107,27 +101,29 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItem() .should('have.length', 4) .eq(3) .within(() => { - cy.get('input').click(); - }); + e2e.components.Variables.variableLinkWrapper().should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + .should('be.visible') + .within(() => { + e2e.components.Variables.variableOption().should('have.length', 1); + }); - e2e.components.Select.option().should('have.length', 1); - e2e.components.Select.option().contains('C'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); + }); }); it('adding a multi value query variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) - .should('be.visible') - .click(); + e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') @@ -165,21 +161,22 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItem() .should('have.length', 4) .eq(3) .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableLinkWrapper().should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + .should('be.visible') + .within(() => { + e2e.components.Variables.variableOption().should('have.length', 2); + }); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); }); - - e2e.components.Select.option().should('have.length', 3); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - - e2e.components.Select.option().contains('All'); - e2e.components.Select.option().contains('C'); }); }); diff --git a/e2e/scenes/dashboards-suite/new-text-box-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-text-box-variable.spec.ts similarity index 75% rename from e2e/scenes/dashboards-suite/new-text-box-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-text-box-variable.spec.ts index 7da9afa32bf..b6cbabceef8 100644 --- a/e2e/scenes/dashboards-suite/new-text-box-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-text-box-variable.spec.ts @@ -9,25 +9,26 @@ describe('Variables - Text box', () => { }); it('can add a new text box variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "text box" variable e2e.components.CallToActionCard.buttonV2('Add variable').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => { - cy.get('input').type('Textbox{enter}'); + cy.get('input').type('Text box{enter}'); }); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2().type('cat-dog').blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'cat-dog'); + // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); - e2e.pages.Dashboard.SubMenu.submenuItem().within(() => { - cy.get('input').clear().type('dog-cat').blur(); - }); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); + cy.get('#var-VariableUnderTest').clear().type('dog-cat').blur(); + // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: dog-cat'); }); diff --git a/e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts b/e2e/old-arch/dashboards-suite/set-options-from-ui.spec.ts similarity index 73% rename from e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts rename to e2e/old-arch/dashboards-suite/set-options-from-ui.spec.ts index bdcc5c6b516..d7ec950487b 100644 --- a/e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts +++ b/e2e/old-arch/dashboards-suite/set-options-from-ui.spec.ts @@ -1,5 +1,3 @@ -import { selectors } from '@grafana/e2e-selectors'; - import { e2e } from '../utils'; const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; @@ -12,46 +10,39 @@ describe('Variables - Set options from ui', () => { it('clicking a value that is not part of dependents options should change these to All', () => { e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` }); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') - .should('be.visible') - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); - cy.get('body').click(); + e2e.components.NavToolbar.container().click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') .should('have.length', 2) .eq(0) + .should('be.visible') + .click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - cy.get('body').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemLabels('pod') - .parent() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 65); }); - // length is 11 because of virtualized select options - e2e.components.Select.option().parent().should('have.length', 11); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAB').should('be.visible'); @@ -69,47 +60,41 @@ describe('Variables - Set options from ui', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') - .should('be.visible') - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (2)'); - - cy.get('body').click(); + e2e.components.NavToolbar.container().click(); cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B').scrollIntoView().should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').scrollIntoView().should('be.visible'); - cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); + e2e.components.LoadingIndicator.icon().should('have.length', 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 17); }); - e2e.components.Select.option().should('have.length', 11); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AD').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - cy.get('body').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().should('have.length', 10); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB').should('be.visible'); @@ -123,46 +108,38 @@ describe('Variables - Set options from ui', () => { cy.intercept({ pathname: '/api/ds/query' }).as('query'); cy.wait('@query'); - cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B') - .should('be.visible') - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); - cy.get('body').click(); + e2e.components.NavToolbar.container().click(); cy.wait('@query'); - cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); - cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); + e2e.components.LoadingIndicator.icon().should('have.length', 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().should('have.length', 10); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().should('have.length', 10); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e/old-arch/dashboards-suite/templating-dashboard-links-and-variables.spec.ts similarity index 85% rename from e2e/scenes/dashboards-suite/templating-dashboard-links-and-variables.spec.ts rename to e2e/old-arch/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index 829303aab41..06e13002db4 100644 --- a/e2e/scenes/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e/old-arch/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -27,27 +27,21 @@ describe('Templating', () => { expect(links).to.have.length.greaterThan(13); for (let index = 0; index < links.length; index++) { - expect(Cypress.$(links[index]).attr('href')).contains(variableValue); + expect(Cypress.$(links[index]).attr('href')).contains(`var-custom=${variableValue}`); } }); }; e2e.components.DashboardLinks.dropDown().should('be.visible').click().wait('@tagsTemplatingSearch'); - verifyLinks('var-custom=p1&var-custom=p2&var-custom=p3'); + // verify all links, should have All value + verifyLinks('All'); - cy.get('body').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') - .should('be.visible') - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2').should('be.visible').click(); - cy.get('body').click(); - + e2e.components.NavToolbar.container().click(); e2e.components.DashboardLinks.dropDown() .scrollIntoView() .should('be.visible') diff --git a/e2e/scenes/dashboards-suite/textbox-variables.spec.ts b/e2e/old-arch/dashboards-suite/textbox-variables.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/textbox-variables.spec.ts rename to e2e/old-arch/dashboards-suite/textbox-variables.spec.ts diff --git a/e2e/scenes/dashboards-suite/utils/makeDashboard.ts b/e2e/old-arch/dashboards-suite/utils/makeDashboard.ts similarity index 100% rename from e2e/scenes/dashboards-suite/utils/makeDashboard.ts rename to e2e/old-arch/dashboards-suite/utils/makeDashboard.ts diff --git a/e2e/scenes/dashboards/DashboardLiveTest.json b/e2e/old-arch/dashboards/DashboardLiveTest.json similarity index 100% rename from e2e/scenes/dashboards/DashboardLiveTest.json rename to e2e/old-arch/dashboards/DashboardLiveTest.json diff --git a/e2e/scenes/dashboards/DashboardSearchTest.json b/e2e/old-arch/dashboards/DashboardSearchTest.json similarity index 100% rename from e2e/scenes/dashboards/DashboardSearchTest.json rename to e2e/old-arch/dashboards/DashboardSearchTest.json diff --git a/e2e/scenes/dashboards/PanelSandboxDashboard.json b/e2e/old-arch/dashboards/PanelSandboxDashboard.json similarity index 100% rename from e2e/scenes/dashboards/PanelSandboxDashboard.json rename to e2e/old-arch/dashboards/PanelSandboxDashboard.json diff --git a/e2e/scenes/dashboards/TestDashboard.json b/e2e/old-arch/dashboards/TestDashboard.json similarity index 100% rename from e2e/scenes/dashboards/TestDashboard.json rename to e2e/old-arch/dashboards/TestDashboard.json diff --git a/e2e/scenes/panels-suite/dashlist.spec.ts b/e2e/old-arch/panels-suite/dashlist.spec.ts similarity index 88% rename from e2e/scenes/panels-suite/dashlist.spec.ts rename to e2e/old-arch/panels-suite/dashlist.spec.ts index 7ad25207910..ec053d3e5e4 100644 --- a/e2e/scenes/panels-suite/dashlist.spec.ts +++ b/e2e/old-arch/panels-suite/dashlist.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const PAGE_UNDER_TEST = 'a6801696-cc53-4196-b1f9-2403e3909185/panel-tests-dashlist-variables'; describe('DashList panel', () => { @@ -20,11 +20,7 @@ describe('DashList panel', () => { }); // update variable to b - e2e.pages.Dashboard.SubMenu.submenuItemLabels('server') - .parent() - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('server').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click(); // blur the dropdown cy.get('body').click(); diff --git a/e2e/scenes/panels-suite/datagrid-data-change.spec.ts b/e2e/old-arch/panels-suite/datagrid-data-change.spec.ts similarity index 97% rename from e2e/scenes/panels-suite/datagrid-data-change.spec.ts rename to e2e/old-arch/panels-suite/datagrid-data-change.spec.ts index e7950030659..583f30e267f 100644 --- a/e2e/scenes/panels-suite/datagrid-data-change.spec.ts +++ b/e2e/old-arch/panels-suite/datagrid-data-change.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; const DATAGRID_SELECT_SERIES = 'Datagrid Select series'; diff --git a/e2e/scenes/panels-suite/datagrid-editing-features.spec.ts b/e2e/old-arch/panels-suite/datagrid-editing-features.spec.ts similarity index 99% rename from e2e/scenes/panels-suite/datagrid-editing-features.spec.ts rename to e2e/old-arch/panels-suite/datagrid-editing-features.spec.ts index 71e69b21022..e3592f60388 100644 --- a/e2e/scenes/panels-suite/datagrid-editing-features.spec.ts +++ b/e2e/old-arch/panels-suite/datagrid-editing-features.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; const DATAGRID_CANVAS = 'data-grid-canvas'; diff --git a/e2e/scenes/panels-suite/frontend-sandbox-panel.spec.ts b/e2e/old-arch/panels-suite/frontend-sandbox-panel.spec.ts similarity index 93% rename from e2e/scenes/panels-suite/frontend-sandbox-panel.spec.ts rename to e2e/old-arch/panels-suite/frontend-sandbox-panel.spec.ts index 582d6ba6497..bab59b99866 100644 --- a/e2e/scenes/panels-suite/frontend-sandbox-panel.spec.ts +++ b/e2e/old-arch/panels-suite/frontend-sandbox-panel.spec.ts @@ -1,9 +1,9 @@ -import panelSandboxDashboard from '../../dashboards/PanelSandboxDashboard.json'; -import { e2e } from '../../utils'; +import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; -// Skipping due to race conditions with same old arch test e2e/panels-suite/frontend-sandbox-panel.spec.ts -describe.skip('Panel sandbox', () => { + +describe('Panel sandbox', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true); diff --git a/e2e/scenes/panels-suite/geomap-layer-types.spec.ts b/e2e/old-arch/panels-suite/geomap-layer-types.spec.ts similarity index 91% rename from e2e/scenes/panels-suite/geomap-layer-types.spec.ts rename to e2e/old-arch/panels-suite/geomap-layer-types.spec.ts index 2e60e88f3f5..17779dde1c9 100644 --- a/e2e/scenes/panels-suite/geomap-layer-types.spec.ts +++ b/e2e/old-arch/panels-suite/geomap-layer-types.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'P2jR04WVk'; @@ -21,41 +21,41 @@ describe('Geomap layer types', () => { e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Heatmap{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('heatmap'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // GeoJSON e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('GeoJSON{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('geojson'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // Open Street Map e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Open Street Map{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('osm-standard'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('not.exist'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // CARTO basemap e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('CARTO basemap{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('carto'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // ArcGIS MapServer e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('ArcGIS MapServer{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('esri-xyz'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // XYZ Tile layer e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('XYZ Tile layer{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('xyz'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); }); it.skip('Tests changing the layer type (alpha)', () => { diff --git a/e2e/scenes/panels-suite/geomap-map-controls.spec.ts b/e2e/old-arch/panels-suite/geomap-map-controls.spec.ts similarity index 97% rename from e2e/scenes/panels-suite/geomap-map-controls.spec.ts rename to e2e/old-arch/panels-suite/geomap-map-controls.spec.ts index 21ba2f73c8b..fbbc1c2c516 100644 --- a/e2e/scenes/panels-suite/geomap-map-controls.spec.ts +++ b/e2e/old-arch/panels-suite/geomap-map-controls.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'P2jR04WVk'; describe('Geomap layer controls options', () => { diff --git a/e2e/scenes/panels-suite/geomap-spatial-operations-transform.spec.ts b/e2e/old-arch/panels-suite/geomap-spatial-operations-transform.spec.ts similarity index 93% rename from e2e/scenes/panels-suite/geomap-spatial-operations-transform.spec.ts rename to e2e/old-arch/panels-suite/geomap-spatial-operations-transform.spec.ts index 66fb1f102e0..52ca7ed0fac 100644 --- a/e2e/scenes/panels-suite/geomap-spatial-operations-transform.spec.ts +++ b/e2e/old-arch/panels-suite/geomap-spatial-operations-transform.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'P2jR04WVk'; @@ -9,7 +9,7 @@ describe('Geomap spatial operations', () => { it('Tests location auto option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -27,7 +27,7 @@ describe('Geomap spatial operations', () => { it('Tests location coords option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -51,7 +51,7 @@ describe('Geomap spatial operations', () => { it('Tests geoshash field column appears in table view', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -74,7 +74,7 @@ describe('Geomap spatial operations', () => { it('Tests location lookup option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); diff --git a/e2e/scenes/panels-suite/panelEdit_base.spec.ts b/e2e/old-arch/panels-suite/panelEdit_base.spec.ts similarity index 75% rename from e2e/scenes/panels-suite/panelEdit_base.spec.ts rename to e2e/old-arch/panels-suite/panelEdit_base.spec.ts index e8d0e301aff..744c41ed8ce 100644 --- a/e2e/scenes/panels-suite/panelEdit_base.spec.ts +++ b/e2e/old-arch/panels-suite/panelEdit_base.spec.ts @@ -1,6 +1,4 @@ -import { selectors } from '@grafana/e2e-selectors'; - -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const PANEL_UNDER_TEST = 'Lines 500 data points'; @@ -18,53 +16,62 @@ describe('Panel edit tests', () => { e2e.flows.openPanelMenuItem(e2e.flows.PanelMenuItems.Edit, PANEL_UNDER_TEST); - // // New panel editor opens when navigating from Panel menu + // New panel editor opens when navigating from Panel menu e2e.components.PanelEditor.General.content().should('be.visible'); // Queries tab is rendered and open by default e2e.components.PanelEditor.DataPane.content() - .scrollIntoView() .should('be.visible') .within(() => { - e2e.components.Tab.title('Queries').should('be.visible'); + e2e.components.Tab.title('Query').should('be.visible'); // data should be the active tab e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Queries1'); // there's already a query so therefore Query + 1 + expect(li.text()).equals('Query1'); // there's already a query so therefore Query + 1 }); - // cy.get('[data-testid]="query-editor-rows"').should('be.visible'); - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('be.visible'); + e2e.components.QueryTab.content().should('be.visible'); e2e.components.TransformTab.content().should('not.exist'); e2e.components.AlertTab.content().should('not.exist'); e2e.components.PanelAlertTabContent.content().should('not.exist'); // Bottom pane tabs // Can change to Transform tab - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Transformations0'); // there's no transform so therefore Transform + 0 + expect(li.text()).equals('Transform data0'); // there's no transform so therefore Transform + 0 }); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible'); - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); + e2e.components.QueryTab.content().should('not.exist'); e2e.components.AlertTab.content().should('not.exist'); e2e.components.PanelAlertTabContent.content().should('not.exist'); // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) - e2e.components.Tab.title('Alert').scrollIntoView().should('be.visible').click(); + e2e.components.Tab.title('Alert').should('be.visible').click(); e2e.components.Tab.active().should('have.text', 'Alert0'); // there's no alert so therefore Alert + 0 // Needs to be disabled until Grafana EE turns unified alerting on by default // e2e.components.AlertTab.content().should('not.exist'); - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); + e2e.components.QueryTab.content().should('not.exist'); e2e.components.TransformTab.content().should('not.exist'); // Needs to be disabled until Grafana EE turns unified alerting on by default // e2e.components.PanelAlertTabContent.content().should('exist'); // e2e.components.PanelAlertTabContent.content().should('be.visible'); - e2e.components.Tab.title('Queries').should('be.visible').click(); + e2e.components.Tab.title('Query').should('be.visible').click(); }); + // Panel sidebar is rendered open by default + e2e.components.PanelEditor.OptionsPane.content().should('be.visible'); + + // close options pane + e2e.components.PanelEditor.toggleVizOptions().click(); + e2e.components.PanelEditor.OptionsPane.content().should('not.exist'); + + // open options pane + e2e.components.PanelEditor.toggleVizOptions().should('be.visible').click(); + e2e.components.PanelEditor.OptionsPane.content().should('be.visible'); + // Check that Time series is chosen e2e.components.PanelEditor.toggleVizPicker().click(); e2e.components.PluginVisualization.item('Time series').should('be.visible'); @@ -95,8 +102,6 @@ describe('Panel edit tests', () => { e2e.components.PanelEditor.DataPane.content().should('be.visible'); // Field & Overrides tabs (need to switch to React based vis, i.e. Table) - e2e.components.PanelEditor.toggleTableView().click({ force: true }).click({ force: true }); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible'); }); diff --git a/e2e/scenes/panels-suite/panelEdit_queries.spec.ts b/e2e/old-arch/panels-suite/panelEdit_queries.spec.ts similarity index 99% rename from e2e/scenes/panels-suite/panelEdit_queries.spec.ts rename to e2e/old-arch/panels-suite/panelEdit_queries.spec.ts index a06b05a1668..1700a00ea9f 100644 --- a/e2e/scenes/panels-suite/panelEdit_queries.spec.ts +++ b/e2e/old-arch/panels-suite/panelEdit_queries.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const flakyTimeout = 10000; diff --git a/e2e/scenes/panels-suite/panelEdit_transforms.spec.ts b/e2e/old-arch/panels-suite/panelEdit_transforms.spec.ts similarity index 86% rename from e2e/scenes/panels-suite/panelEdit_transforms.spec.ts rename to e2e/old-arch/panels-suite/panelEdit_transforms.spec.ts index ef14d6e55a5..eb3029b8d98 100644 --- a/e2e/scenes/panels-suite/panelEdit_transforms.spec.ts +++ b/e2e/old-arch/panels-suite/panelEdit_transforms.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; describe('Panel edit tests - transformations', () => { beforeEach(() => { @@ -8,7 +8,7 @@ describe('Panel edit tests - transformations', () => { it('Tests transformations editor', () => { e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); e2e.components.Transforms.Reduce.calculationsLabel().scrollIntoView().should('be.visible'); @@ -18,7 +18,7 @@ describe('Panel edit tests - transformations', () => { it('Tests case where transformations can be disabled and not clear out panel data', () => { e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); e2e.components.Transforms.disableTransformationButton().should('be.visible').click(); diff --git a/e2e/scenes/shared/smokeTestScenario.ts b/e2e/old-arch/shared/smokeTestScenario.ts similarity index 70% rename from e2e/scenes/shared/smokeTestScenario.ts rename to e2e/old-arch/shared/smokeTestScenario.ts index 3e66419680f..537bd0bb23c 100644 --- a/e2e/scenes/shared/smokeTestScenario.ts +++ b/e2e/old-arch/shared/smokeTestScenario.ts @@ -3,17 +3,12 @@ import { e2e } from '../utils'; export const smokeTestScenario = () => describe('Smoke tests', () => { before(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false); - cy.logToConsole('enabling dashboardScene feature toggle in localstorage'); - cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=true'); + cy.logToConsole('disabling dashboardScene feature toggle in localstorage'); + cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=false'); cy.reload(); + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false); e2e.flows.addDataSource(); e2e.flows.addDashboard(); - e2e.flows.addPanel({ - dataSourceName: 'gdev-testdata', - visitDashboardAtStart: false, - timeout: 10000, - }); }); after(() => { @@ -21,6 +16,11 @@ export const smokeTestScenario = () => }); it('Login scenario, create test data source, dashboard, panel, and export scenario', () => { + // wait for time to be set to account for any layout shift + cy.contains('2020-01-01 00:00:00 to 2020-01-01 06:00:00').should('be.visible'); + e2e.components.PageToolbar.itemButton('Add button').click(); + e2e.components.PageToolbar.itemButton('Add new visualization menu item').click(); + e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer() .should('be.visible') .within(() => { @@ -32,7 +32,8 @@ export const smokeTestScenario = () => // Make sure the graph renders via checking legend e2e.components.VizLegend.seriesName('A-series').should('be.visible'); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + // Expand options section + e2e.components.PanelEditor.applyButton(); // Make sure panel is & visualization is added to dashboard e2e.components.VizLegend.seriesName('A-series').should('be.visible'); diff --git a/e2e/scenes/smoke-tests-suite/1-smoketests.spec.ts b/e2e/old-arch/smoke-tests-suite/1-smoketests.spec.ts similarity index 100% rename from e2e/scenes/smoke-tests-suite/1-smoketests.spec.ts rename to e2e/old-arch/smoke-tests-suite/1-smoketests.spec.ts diff --git a/e2e/scenes/smoke-tests-suite/panels_smokescreen.spec.ts b/e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts similarity index 66% rename from e2e/scenes/smoke-tests-suite/panels_smokescreen.spec.ts rename to e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts index ca63dadba19..5ba784d72b2 100644 --- a/e2e/scenes/smoke-tests-suite/panels_smokescreen.spec.ts +++ b/e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts @@ -14,11 +14,17 @@ describe('Panels smokescreen', () => { it('Tests each panel type in the panel edit view to ensure no crash', () => { e2e.flows.addDashboard(); - e2e.flows.addPanel({ - dataSourceName: 'gdev-testdata', - timeout: 10000, - visitDashboardAtStart: false, - }); + // TODO: Try and use e2e.flows.addPanel() instead of block below + try { + e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); + e2e.components.PageToolbar.itemButton('Add button').click(); + } catch (e) { + // Depending on the screen size, the "Add panel" button might be hidden + e2e.components.PageToolbar.item('Show more items').click(); + e2e.components.PageToolbar.item('Add button').last().click(); + } + e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); + e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); cy.window().then((win: Cypress.AUTWindow & { grafanaBootData: GrafanaBootConfig['bootData'] }) => { // Loop through every panel type and ensure no crash diff --git a/e2e/scenes/utils/flows/addDashboard.ts b/e2e/old-arch/utils/flows/addDashboard.ts similarity index 97% rename from e2e/scenes/utils/flows/addDashboard.ts rename to e2e/old-arch/utils/flows/addDashboard.ts index 77dd07e9c9b..cc8e7548032 100644 --- a/e2e/scenes/utils/flows/addDashboard.ts +++ b/e2e/old-arch/utils/flows/addDashboard.ts @@ -139,9 +139,9 @@ export const addDashboard = (config?: Partial) => { setDashboardTimeRange(timeRange); - e2e.components.NavToolbar.editDashboard.saveButton().click(); - e2e.components.Drawer.DashboardSaveDrawer.saveAsTitleInput().clear().type(title, { force: true }); - e2e.components.Drawer.DashboardSaveDrawer.saveButton().click(); + e2e.components.PageToolbar.item('Save dashboard').click(); + e2e.pages.SaveDashboardAsModal.newName().clear().type(title, { force: true }); + e2e.pages.SaveDashboardAsModal.save().click(); e2e.flows.assertSuccessNotification(); e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible'); diff --git a/e2e/scenes/utils/flows/addDataSource.ts b/e2e/old-arch/utils/flows/addDataSource.ts similarity index 100% rename from e2e/scenes/utils/flows/addDataSource.ts rename to e2e/old-arch/utils/flows/addDataSource.ts diff --git a/e2e/scenes/utils/flows/addPanel.ts b/e2e/old-arch/utils/flows/addPanel.ts similarity index 100% rename from e2e/scenes/utils/flows/addPanel.ts rename to e2e/old-arch/utils/flows/addPanel.ts diff --git a/e2e/scenes/utils/flows/assertSuccessNotification.ts b/e2e/old-arch/utils/flows/assertSuccessNotification.ts similarity index 100% rename from e2e/scenes/utils/flows/assertSuccessNotification.ts rename to e2e/old-arch/utils/flows/assertSuccessNotification.ts diff --git a/e2e/scenes/utils/flows/configurePanel.ts b/e2e/old-arch/utils/flows/configurePanel.ts similarity index 93% rename from e2e/scenes/utils/flows/configurePanel.ts rename to e2e/old-arch/utils/flows/configurePanel.ts index 7a881a47c17..4aa6a6cc5ab 100644 --- a/e2e/scenes/utils/flows/configurePanel.ts +++ b/e2e/old-arch/utils/flows/configurePanel.ts @@ -85,17 +85,15 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC e2e.components.Panels.Panel.headerItems('Edit').click(); } else { try { - //Enter edit mode - e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); - e2e.components.PageToolbar.itemButton('Add button').should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.addVisualizationButton().should('be.visible').click(); + e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); + e2e.components.PageToolbar.itemButton('Add button').click(); } catch (e) { // Depending on the screen size, the "Add" button might be hidden e2e.components.PageToolbar.item('Show more items').click(); e2e.components.PageToolbar.item('Add button').last().click(); } - // e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); - // e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); + e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); + e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); } if (timeRange) { diff --git a/e2e/scenes/utils/flows/confirmModal.ts b/e2e/old-arch/utils/flows/confirmModal.ts similarity index 100% rename from e2e/scenes/utils/flows/confirmModal.ts rename to e2e/old-arch/utils/flows/confirmModal.ts diff --git a/e2e/scenes/utils/flows/deleteDashboard.ts b/e2e/old-arch/utils/flows/deleteDashboard.ts similarity index 100% rename from e2e/scenes/utils/flows/deleteDashboard.ts rename to e2e/old-arch/utils/flows/deleteDashboard.ts diff --git a/e2e/scenes/utils/flows/deleteDataSource.ts b/e2e/old-arch/utils/flows/deleteDataSource.ts similarity index 100% rename from e2e/scenes/utils/flows/deleteDataSource.ts rename to e2e/old-arch/utils/flows/deleteDataSource.ts diff --git a/e2e/scenes/utils/flows/editPanel.ts b/e2e/old-arch/utils/flows/editPanel.ts similarity index 100% rename from e2e/scenes/utils/flows/editPanel.ts rename to e2e/old-arch/utils/flows/editPanel.ts diff --git a/e2e/scenes/utils/flows/importDashboard.ts b/e2e/old-arch/utils/flows/importDashboard.ts similarity index 95% rename from e2e/scenes/utils/flows/importDashboard.ts rename to e2e/old-arch/utils/flows/importDashboard.ts index d3c6b7e23b5..4e7b8ade4ad 100644 --- a/e2e/scenes/utils/flows/importDashboard.ts +++ b/e2e/old-arch/utils/flows/importDashboard.ts @@ -51,9 +51,7 @@ export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: num e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click(); e2e.components.Tab.title('JSON').should('be.visible').click(); - e2e.components.PanelInspector.Json.content().should('be.visible'); - e2e.components.ReactMonacoEditor.editorLazy().should('be.visible'); - cy.contains('Panel JSON').click({ force: true }); + e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click({ force: true }); e2e.components.Select.option().should('be.visible').contains('Panel data').click(); // ensures that panel has loaded without knowingly hitting an error diff --git a/e2e/scenes/utils/flows/importDashboards.ts b/e2e/old-arch/utils/flows/importDashboards.ts similarity index 100% rename from e2e/scenes/utils/flows/importDashboards.ts rename to e2e/old-arch/utils/flows/importDashboards.ts diff --git a/e2e/scenes/utils/flows/index.ts b/e2e/old-arch/utils/flows/index.ts similarity index 100% rename from e2e/scenes/utils/flows/index.ts rename to e2e/old-arch/utils/flows/index.ts diff --git a/e2e/scenes/utils/flows/login.ts b/e2e/old-arch/utils/flows/login.ts similarity index 100% rename from e2e/scenes/utils/flows/login.ts rename to e2e/old-arch/utils/flows/login.ts diff --git a/e2e/scenes/utils/flows/openDashboard.ts b/e2e/old-arch/utils/flows/openDashboard.ts similarity index 100% rename from e2e/scenes/utils/flows/openDashboard.ts rename to e2e/old-arch/utils/flows/openDashboard.ts diff --git a/e2e/scenes/utils/flows/openPanelMenuItem.ts b/e2e/old-arch/utils/flows/openPanelMenuItem.ts similarity index 100% rename from e2e/scenes/utils/flows/openPanelMenuItem.ts rename to e2e/old-arch/utils/flows/openPanelMenuItem.ts diff --git a/e2e/scenes/utils/flows/revertAllChanges.ts b/e2e/old-arch/utils/flows/revertAllChanges.ts similarity index 100% rename from e2e/scenes/utils/flows/revertAllChanges.ts rename to e2e/old-arch/utils/flows/revertAllChanges.ts diff --git a/e2e/scenes/utils/flows/saveDashboard.ts b/e2e/old-arch/utils/flows/saveDashboard.ts similarity index 100% rename from e2e/scenes/utils/flows/saveDashboard.ts rename to e2e/old-arch/utils/flows/saveDashboard.ts diff --git a/e2e/scenes/utils/flows/selectOption.ts b/e2e/old-arch/utils/flows/selectOption.ts similarity index 100% rename from e2e/scenes/utils/flows/selectOption.ts rename to e2e/old-arch/utils/flows/selectOption.ts diff --git a/e2e/scenes/utils/flows/setDashboardTimeRange.ts b/e2e/old-arch/utils/flows/setDashboardTimeRange.ts similarity index 100% rename from e2e/scenes/utils/flows/setDashboardTimeRange.ts rename to e2e/old-arch/utils/flows/setDashboardTimeRange.ts diff --git a/e2e/scenes/utils/flows/setTimeRange.ts b/e2e/old-arch/utils/flows/setTimeRange.ts similarity index 100% rename from e2e/scenes/utils/flows/setTimeRange.ts rename to e2e/old-arch/utils/flows/setTimeRange.ts diff --git a/e2e/scenes/utils/flows/userPreferences.ts b/e2e/old-arch/utils/flows/userPreferences.ts similarity index 100% rename from e2e/scenes/utils/flows/userPreferences.ts rename to e2e/old-arch/utils/flows/userPreferences.ts diff --git a/e2e/scenes/utils/index.ts b/e2e/old-arch/utils/index.ts similarity index 100% rename from e2e/scenes/utils/index.ts rename to e2e/old-arch/utils/index.ts diff --git a/e2e/scenes/utils/support/benchmark.ts b/e2e/old-arch/utils/support/benchmark.ts similarity index 100% rename from e2e/scenes/utils/support/benchmark.ts rename to e2e/old-arch/utils/support/benchmark.ts diff --git a/e2e/old-arch/utils/support/clipboard.ts b/e2e/old-arch/utils/support/clipboard.ts new file mode 100644 index 00000000000..aa5841ea2d6 --- /dev/null +++ b/e2e/old-arch/utils/support/clipboard.ts @@ -0,0 +1,29 @@ +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + copyToClipboard(): Chainable; + copyFromClipboard(): Chainable; + } + } +} + +Cypress.Commands.add('copyFromClipboard', () => { + return cy.window().then((win) => { + return cy.wrap(win.navigator.clipboard.readText()); + }); +}); + +Cypress.Commands.add( + 'copyToClipboard', + { + prevSubject: [], + }, + (subject: string) => { + return cy.window().then((win) => { + return cy.wrap(win.navigator.clipboard.writeText(subject)); + }); + } +); + +export {}; diff --git a/e2e/scenes/utils/support/index.ts b/e2e/old-arch/utils/support/index.ts similarity index 100% rename from e2e/scenes/utils/support/index.ts rename to e2e/old-arch/utils/support/index.ts diff --git a/e2e/scenes/utils/support/localStorage.ts b/e2e/old-arch/utils/support/localStorage.ts similarity index 100% rename from e2e/scenes/utils/support/localStorage.ts rename to e2e/old-arch/utils/support/localStorage.ts diff --git a/e2e/scenes/utils/support/monaco.ts b/e2e/old-arch/utils/support/monaco.ts similarity index 100% rename from e2e/scenes/utils/support/monaco.ts rename to e2e/old-arch/utils/support/monaco.ts diff --git a/e2e/scenes/utils/support/scenarioContext.ts b/e2e/old-arch/utils/support/scenarioContext.ts similarity index 100% rename from e2e/scenes/utils/support/scenarioContext.ts rename to e2e/old-arch/utils/support/scenarioContext.ts diff --git a/e2e/scenes/utils/support/selector.ts b/e2e/old-arch/utils/support/selector.ts similarity index 100% rename from e2e/scenes/utils/support/selector.ts rename to e2e/old-arch/utils/support/selector.ts diff --git a/e2e/scenes/utils/support/types.ts b/e2e/old-arch/utils/support/types.ts similarity index 100% rename from e2e/scenes/utils/support/types.ts rename to e2e/old-arch/utils/support/types.ts diff --git a/e2e/scenes/utils/support/url.ts b/e2e/old-arch/utils/support/url.ts similarity index 92% rename from e2e/scenes/utils/support/url.ts rename to e2e/old-arch/utils/support/url.ts index 34620974b06..94e42827940 100644 --- a/e2e/scenes/utils/support/url.ts +++ b/e2e/old-arch/utils/support/url.ts @@ -1,5 +1,3 @@ -import { e2e } from '../index'; - const getBaseUrl = () => Cypress.env('BASE_URL') || Cypress.config().baseUrl || 'http://localhost:3000'; export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href; diff --git a/e2e/scenes/utils/typings/index.ts b/e2e/old-arch/utils/typings/index.ts similarity index 100% rename from e2e/scenes/utils/typings/index.ts rename to e2e/old-arch/utils/typings/index.ts diff --git a/e2e/scenes/utils/typings/undo.ts b/e2e/old-arch/utils/typings/undo.ts similarity index 100% rename from e2e/scenes/utils/typings/undo.ts rename to e2e/old-arch/utils/typings/undo.ts diff --git a/e2e/scenes/various-suite/bar-gauge.spec.ts b/e2e/old-arch/various-suite/bar-gauge.spec.ts similarity index 76% rename from e2e/scenes/various-suite/bar-gauge.spec.ts rename to e2e/old-arch/various-suite/bar-gauge.spec.ts index 7c28c48a146..5f8506b8050 100644 --- a/e2e/scenes/various-suite/bar-gauge.spec.ts +++ b/e2e/old-arch/various-suite/bar-gauge.spec.ts @@ -11,9 +11,7 @@ describe('Bar Gauge Panel', () => { // open Panel Tests - Bar Gauge e2e.flows.openDashboard({ uid: 'O6f11TZWk' }); - cy.get( - `[data-viz-panel-key="panel-6"] [data-testid^="${selectors.components.Panels.Visualization.BarGauge.valueV2}"]` - ) + cy.get(`[data-panelid=6] [data-testid^="${selectors.components.Panels.Visualization.BarGauge.valueV2}"]`) .should('have.css', 'color', 'rgb(242, 73, 92)') .contains('100'); }); diff --git a/e2e/scenes/various-suite/exemplars.spec.ts b/e2e/old-arch/various-suite/exemplars.spec.ts similarity index 93% rename from e2e/scenes/various-suite/exemplars.spec.ts rename to e2e/old-arch/various-suite/exemplars.spec.ts index 675b83039ba..b3c422c1294 100644 --- a/e2e/scenes/various-suite/exemplars.spec.ts +++ b/e2e/old-arch/various-suite/exemplars.spec.ts @@ -17,8 +17,8 @@ const addDataSource = () => { }, }); }; -// Skipping due to race conditions with same old arch test e2e/various-suite/exemplars.spec.ts -describe.skip('Exemplars', () => { + +describe('Exemplars', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); @@ -69,7 +69,7 @@ describe.skip('Exemplars', () => { cy.get(`[data-testid="time-series-zoom-to-data"]`).click(); - e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove', { force: true }); + e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove'); cy.contains('Query with gdev-tempo').click(); e2e.components.TraceViewer.spanBar().should('have.length', 11); }); diff --git a/e2e/scenes/various-suite/explore.spec.ts b/e2e/old-arch/various-suite/explore.spec.ts similarity index 100% rename from e2e/scenes/various-suite/explore.spec.ts rename to e2e/old-arch/various-suite/explore.spec.ts diff --git a/e2e/scenes/various-suite/filter-annotations.spec.ts b/e2e/old-arch/various-suite/filter-annotations.spec.ts similarity index 61% rename from e2e/scenes/various-suite/filter-annotations.spec.ts rename to e2e/old-arch/various-suite/filter-annotations.spec.ts index 35319ee1beb..26cf83f00e2 100644 --- a/e2e/scenes/various-suite/filter-annotations.spec.ts +++ b/e2e/old-arch/various-suite/filter-annotations.spec.ts @@ -1,8 +1,7 @@ import { e2e } from '../utils'; const DASHBOARD_ID = 'ed155665'; -// Skipping due to race conditions with same old arch test e2e/various-suite/filter-annotations.spec.ts -describe.skip('Annotations filtering', () => { +describe('Annotations filtering', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -10,9 +9,7 @@ describe.skip('Annotations filtering', () => { it('Tests switching filter type updates the UI accordingly', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID }); - e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); - + e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Annotations').click(); cy.contains('New query').click(); e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type('Red - Panel two'); @@ -38,30 +35,20 @@ describe.skip('Annotations filtering', () => { .type('Panel two{enter}', { force: true }); }); - cy.get('body').click(); + e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click({ force: true }); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); - - e2e.pages.Dashboard.Controls() + e2e.pages.Dashboard.SubMenu.Annotations.annotationsWrapper() .should('be.visible') .within(() => { - e2e.pages.Dashboard.SubMenu.submenuItemLabels('Red - Panel two') - .should('be.visible') - .parent() - .within((el) => { - cy.get('input') - .should('be.checked') - .uncheck({ force: true }) - .should('not.be.checked') - .check({ force: true }); - }); + e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red - Panel two').should('be.visible'); + e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red - Panel two') + .should('be.checked') + .uncheck({ force: true }) + .should('not.be.checked') + .check({ force: true }); - e2e.pages.Dashboard.SubMenu.submenuItemLabels('Red, only panel 1') - .should('be.visible') - .parent() - .within((el) => { - cy.get('input').should('be.checked'); - }); + e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red, only panel 1').should('be.visible'); + e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red, only panel 1').should('be.checked'); }); e2e.components.Panels.Panel.title('Panel one') diff --git a/e2e/scenes/various-suite/frontend-sandbox-app.spec.ts b/e2e/old-arch/various-suite/frontend-sandbox-app.spec.ts similarity index 100% rename from e2e/scenes/various-suite/frontend-sandbox-app.spec.ts rename to e2e/old-arch/various-suite/frontend-sandbox-app.spec.ts diff --git a/e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts b/e2e/old-arch/various-suite/frontend-sandbox-datasource.spec.ts similarity index 97% rename from e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts rename to e2e/old-arch/various-suite/frontend-sandbox-datasource.spec.ts index 651ba170db9..bfc3fd3fdc3 100644 --- a/e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts +++ b/e2e/old-arch/various-suite/frontend-sandbox-datasource.spec.ts @@ -6,8 +6,7 @@ const DATASOURCE_ID = 'sandbox-test-datasource'; let DATASOURCE_CONNECTION_ID = ''; const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance'; -// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts -describe.skip('Datasource sandbox', () => { +describe('Datasource sandbox', () => { before(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); @@ -86,7 +85,6 @@ describe.skip('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); - // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); @@ -98,6 +96,7 @@ describe.skip('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); + // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); @@ -130,6 +129,7 @@ describe.skip('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); + // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); diff --git a/e2e/scenes/various-suite/gauge.spec.ts b/e2e/old-arch/various-suite/gauge.spec.ts similarity index 100% rename from e2e/scenes/various-suite/gauge.spec.ts rename to e2e/old-arch/various-suite/gauge.spec.ts diff --git a/e2e/scenes/various-suite/graph-auto-migrate.spec.ts b/e2e/old-arch/various-suite/graph-auto-migrate.spec.ts similarity index 100% rename from e2e/scenes/various-suite/graph-auto-migrate.spec.ts rename to e2e/old-arch/various-suite/graph-auto-migrate.spec.ts diff --git a/e2e/scenes/various-suite/helpers/prometheus-helpers.ts b/e2e/old-arch/various-suite/helpers/prometheus-helpers.ts similarity index 100% rename from e2e/scenes/various-suite/helpers/prometheus-helpers.ts rename to e2e/old-arch/various-suite/helpers/prometheus-helpers.ts diff --git a/e2e/scenes/various-suite/inspect-drawer.spec.ts b/e2e/old-arch/various-suite/inspect-drawer.spec.ts similarity index 100% rename from e2e/scenes/various-suite/inspect-drawer.spec.ts rename to e2e/old-arch/various-suite/inspect-drawer.spec.ts diff --git a/e2e/scenes/various-suite/keybinds.spec.ts b/e2e/old-arch/various-suite/keybinds.spec.ts similarity index 88% rename from e2e/scenes/various-suite/keybinds.spec.ts rename to e2e/old-arch/various-suite/keybinds.spec.ts index c3a8b71359d..2f12aee7586 100644 --- a/e2e/scenes/various-suite/keybinds.spec.ts +++ b/e2e/old-arch/various-suite/keybinds.spec.ts @@ -1,8 +1,7 @@ import { e2e } from '../utils'; import { fromBaseUrl } from '../utils/support/url'; -// Skipping due to race conditions with same old arch test e2e/various-suite/keybinds.spec.ts -describe.skip('Keyboard shortcuts', () => { +describe('Keyboard shortcuts', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); @@ -34,10 +33,12 @@ describe.skip('Keyboard shortcuts', () => { zone: 'Browser', }); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); + let expectedRange = `Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00`; + e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); cy.get('body').type('{ctrl}z'); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - let expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`; + expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`; e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); }); diff --git a/e2e/scenes/various-suite/loki-editor.spec.ts b/e2e/old-arch/various-suite/loki-editor.spec.ts similarity index 100% rename from e2e/scenes/various-suite/loki-editor.spec.ts rename to e2e/old-arch/various-suite/loki-editor.spec.ts diff --git a/e2e/scenes/various-suite/loki-query-builder.spec.ts b/e2e/old-arch/various-suite/loki-query-builder.spec.ts similarity index 100% rename from e2e/scenes/various-suite/loki-query-builder.spec.ts rename to e2e/old-arch/various-suite/loki-query-builder.spec.ts diff --git a/e2e/scenes/various-suite/loki-table-explore-to-dash.spec.ts b/e2e/old-arch/various-suite/loki-table-explore-to-dash.spec.ts similarity index 96% rename from e2e/scenes/various-suite/loki-table-explore-to-dash.spec.ts rename to e2e/old-arch/various-suite/loki-table-explore-to-dash.spec.ts index 088745c7fca..c3834e7e7b9 100644 --- a/e2e/scenes/various-suite/loki-table-explore-to-dash.spec.ts +++ b/e2e/old-arch/various-suite/loki-table-explore-to-dash.spec.ts @@ -1,6 +1,6 @@ import { e2e } from '../utils'; -const dataSourceName = 'LokiEditor'; +const dataSourceName = 'LokiEditor' + Date.now(); const addDataSource = () => { e2e.flows.addDataSource({ type: 'Loki', @@ -110,8 +110,7 @@ const lokiQueryResult = { }, }; -// Skipping due to race conditions with same old arch test e2e/various-suite/loki-table-explore-to-dash.spec.ts -describe.skip('Loki Query Editor', () => { +describe('Loki Query Editor', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -122,7 +121,8 @@ describe.skip('Loki Query Editor', () => { beforeEach(() => { cy.window().then((win) => { - win.localStorage.setItem('grafana.featureToggles', 'logsExploreTableVisualisation=1'); + cy.setLocalStorage('grafana.featureToggles', 'logsExploreTableVisualisation=1'); + cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=false'); }); }); it('Should be able to add explore table to dashboard', () => { diff --git a/e2e/scenes/various-suite/navigation.spec.ts b/e2e/old-arch/various-suite/navigation.spec.ts similarity index 100% rename from e2e/scenes/various-suite/navigation.spec.ts rename to e2e/old-arch/various-suite/navigation.spec.ts diff --git a/e2e/scenes/various-suite/pie-chart.spec.ts b/e2e/old-arch/various-suite/pie-chart.spec.ts similarity index 68% rename from e2e/scenes/various-suite/pie-chart.spec.ts rename to e2e/old-arch/various-suite/pie-chart.spec.ts index c6d537a40a2..2030aed53fe 100644 --- a/e2e/scenes/various-suite/pie-chart.spec.ts +++ b/e2e/old-arch/various-suite/pie-chart.spec.ts @@ -11,8 +11,9 @@ describe('Pie Chart Panel', () => { // open Panel Tests - Pie Chart e2e.flows.openDashboard({ uid: 'lVE-2YFMz' }); - cy.get( - `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` - ).should('have.length', 5); + cy.get(`[data-panelid=11] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]`).should( + 'have.length', + 5 + ); }); }); diff --git a/e2e/scenes/various-suite/prometheus-annotations.spec.ts b/e2e/old-arch/various-suite/prometheus-annotations.spec.ts similarity index 90% rename from e2e/scenes/various-suite/prometheus-annotations.spec.ts rename to e2e/old-arch/various-suite/prometheus-annotations.spec.ts index 91392a145a1..b32620dda70 100644 --- a/e2e/scenes/various-suite/prometheus-annotations.spec.ts +++ b/e2e/old-arch/various-suite/prometheus-annotations.spec.ts @@ -14,8 +14,7 @@ const DATASOURCE_NAME = 'aprometheusAnnotationDS'; * */ function navigateToAnnotations() { - e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); + e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Annotations').click(); } @@ -68,7 +67,7 @@ describe('Prometheus annotations', () => { // series value as timestamp e2e.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp().scrollIntoView().should('exist'); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); + e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click(); // check that annotation exists cy.get('body').contains(annotationName); diff --git a/e2e/scenes/various-suite/prometheus-config.spec.ts b/e2e/old-arch/various-suite/prometheus-config.spec.ts similarity index 100% rename from e2e/scenes/various-suite/prometheus-config.spec.ts rename to e2e/old-arch/various-suite/prometheus-config.spec.ts diff --git a/e2e/scenes/various-suite/prometheus-editor.spec.ts b/e2e/old-arch/various-suite/prometheus-editor.spec.ts similarity index 93% rename from e2e/scenes/various-suite/prometheus-editor.spec.ts rename to e2e/old-arch/various-suite/prometheus-editor.spec.ts index ffe8eee9894..d04d502a706 100644 --- a/e2e/scenes/various-suite/prometheus-editor.spec.ts +++ b/e2e/old-arch/various-suite/prometheus-editor.spec.ts @@ -1,5 +1,3 @@ -import { selectors } from '@grafana/e2e-selectors'; - import { e2e } from '../utils'; import { getResources } from './helpers/prometheus-helpers'; @@ -43,8 +41,8 @@ function navigateToEditor(editorType: editorType, name: string): void { e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(name).scrollIntoView().should('be.visible').click(); } -// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/prometheus-editor.spec.ts -describe.skip('Prometheus query editor', () => { + +describe('Prometheus query editor', () => { it('should have a kickstart component', () => { navigateToEditor('Code', 'prometheus'); e2e.components.QueryBuilder.queryPatterns().scrollIntoView().should('exist'); @@ -67,9 +65,9 @@ describe.skip('Prometheus query editor', () => { // check options e2e.components.DataSource.Prometheus.queryEditor.legend().scrollIntoView().should('exist'); e2e.components.DataSource.Prometheus.queryEditor.format().scrollIntoView().should('exist'); - cy.get(`#${selectors.components.DataSource.Prometheus.queryEditor.step}`).scrollIntoView().should('exist'); + cy.get(`[data-test-id="prometheus-step"]`).scrollIntoView().should('exist'); e2e.components.DataSource.Prometheus.queryEditor.type().scrollIntoView().should('exist'); - cy.get(`#${selectors.components.DataSource.Prometheus.queryEditor.exemplars}`).scrollIntoView().should('exist'); + cy.get(`[data-test-id="prometheus-exemplars"]`).scrollIntoView().should('exist'); }); describe('Code editor', () => { diff --git a/e2e/scenes/various-suite/prometheus-variable-editor.spec.ts b/e2e/old-arch/various-suite/prometheus-variable-editor.spec.ts similarity index 90% rename from e2e/scenes/various-suite/prometheus-variable-editor.spec.ts rename to e2e/old-arch/various-suite/prometheus-variable-editor.spec.ts index 4b6c62427aa..8e91ce8c283 100644 --- a/e2e/scenes/various-suite/prometheus-variable-editor.spec.ts +++ b/e2e/old-arch/various-suite/prometheus-variable-editor.spec.ts @@ -11,8 +11,7 @@ const DATASOURCE_NAME = 'prometheusVariableDS'; * Click dashboard settings and then the variables tab */ function navigateToVariables() { - e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); + e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Variables').click(); } @@ -50,7 +49,7 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) { e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); // close to return to dashboard - e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); // add visualization e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible').click(); @@ -77,8 +76,8 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) { // do nothing } } -// Skipping due to race conditions with same old arch test e2e/various-suite/prometheus-variable-editor.spec.ts -describe.skip('Prometheus variable query editor', () => { + +describe('Prometheus variable query editor', () => { beforeEach(() => { createPromDS(DATASOURCE_ID, DATASOURCE_NAME); }); diff --git a/e2e/scenes/various-suite/query-editor.spec.ts b/e2e/old-arch/various-suite/query-editor.spec.ts similarity index 100% rename from e2e/scenes/various-suite/query-editor.spec.ts rename to e2e/old-arch/various-suite/query-editor.spec.ts diff --git a/e2e/scenes/various-suite/return-to-previous.spec.ts b/e2e/old-arch/various-suite/return-to-previous.spec.ts similarity index 100% rename from e2e/scenes/various-suite/return-to-previous.spec.ts rename to e2e/old-arch/various-suite/return-to-previous.spec.ts diff --git a/e2e/scenes/various-suite/select-focus.spec.ts b/e2e/old-arch/various-suite/select-focus.spec.ts similarity index 100% rename from e2e/scenes/various-suite/select-focus.spec.ts rename to e2e/old-arch/various-suite/select-focus.spec.ts diff --git a/e2e/scenes/various-suite/solo-route.spec.ts b/e2e/old-arch/various-suite/solo-route.spec.ts similarity index 100% rename from e2e/scenes/various-suite/solo-route.spec.ts rename to e2e/old-arch/various-suite/solo-route.spec.ts diff --git a/e2e/scenes/various-suite/trace-view-scrolling.spec.ts b/e2e/old-arch/various-suite/trace-view-scrolling.spec.ts similarity index 100% rename from e2e/scenes/various-suite/trace-view-scrolling.spec.ts rename to e2e/old-arch/various-suite/trace-view-scrolling.spec.ts diff --git a/e2e/scenes/various-suite/visualization-suggestions.spec.ts b/e2e/old-arch/various-suite/visualization-suggestions.spec.ts similarity index 100% rename from e2e/scenes/various-suite/visualization-suggestions.spec.ts rename to e2e/old-arch/various-suite/visualization-suggestions.spec.ts diff --git a/e2e/panels-suite/dashlist.spec.ts b/e2e/panels-suite/dashlist.spec.ts index ec053d3e5e4..002cf235e82 100644 --- a/e2e/panels-suite/dashlist.spec.ts +++ b/e2e/panels-suite/dashlist.spec.ts @@ -20,7 +20,11 @@ describe('DashList panel', () => { }); // update variable to b - e2e.pages.Dashboard.SubMenu.submenuItemLabels('server').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('server') + .parent() + .within(() => { + cy.get('input').click(); + }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click(); // blur the dropdown cy.get('body').click(); diff --git a/e2e/panels-suite/frontend-sandbox-panel.spec.ts b/e2e/panels-suite/frontend-sandbox-panel.spec.ts index bab59b99866..b551617b705 100644 --- a/e2e/panels-suite/frontend-sandbox-panel.spec.ts +++ b/e2e/panels-suite/frontend-sandbox-panel.spec.ts @@ -2,8 +2,8 @@ import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; import { e2e } from '../utils'; const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; - -describe('Panel sandbox', () => { +// Skipping due to race conditions with same old arch test e2e/panels-suite/frontend-sandbox-panel.spec.ts +describe.skip('Panel sandbox', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true); diff --git a/e2e/panels-suite/geomap-layer-types.spec.ts b/e2e/panels-suite/geomap-layer-types.spec.ts index 17779dde1c9..70af145c784 100644 --- a/e2e/panels-suite/geomap-layer-types.spec.ts +++ b/e2e/panels-suite/geomap-layer-types.spec.ts @@ -21,41 +21,41 @@ describe('Geomap layer types', () => { e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Heatmap{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('heatmap'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // GeoJSON e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('GeoJSON{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('geojson'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // Open Street Map e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Open Street Map{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('osm-standard'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('not.exist'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // CARTO basemap e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('CARTO basemap{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('carto'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // ArcGIS MapServer e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('ArcGIS MapServer{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('esri-xyz'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // XYZ Tile layer e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('XYZ Tile layer{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('xyz'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); }); it.skip('Tests changing the layer type (alpha)', () => { diff --git a/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts b/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts index 52ca7ed0fac..bce8268b36a 100644 --- a/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts +++ b/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts @@ -9,7 +9,7 @@ describe('Geomap spatial operations', () => { it('Tests location auto option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -27,7 +27,7 @@ describe('Geomap spatial operations', () => { it('Tests location coords option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -51,7 +51,7 @@ describe('Geomap spatial operations', () => { it('Tests geoshash field column appears in table view', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -74,7 +74,7 @@ describe('Geomap spatial operations', () => { it('Tests location lookup option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); diff --git a/e2e/panels-suite/panelEdit_base.spec.ts b/e2e/panels-suite/panelEdit_base.spec.ts index 744c41ed8ce..97eebe35777 100644 --- a/e2e/panels-suite/panelEdit_base.spec.ts +++ b/e2e/panels-suite/panelEdit_base.spec.ts @@ -1,3 +1,5 @@ +import { selectors } from '@grafana/e2e-selectors'; + import { e2e } from '../utils'; const PANEL_UNDER_TEST = 'Lines 500 data points'; @@ -16,62 +18,53 @@ describe('Panel edit tests', () => { e2e.flows.openPanelMenuItem(e2e.flows.PanelMenuItems.Edit, PANEL_UNDER_TEST); - // New panel editor opens when navigating from Panel menu + // // New panel editor opens when navigating from Panel menu e2e.components.PanelEditor.General.content().should('be.visible'); // Queries tab is rendered and open by default e2e.components.PanelEditor.DataPane.content() + .scrollIntoView() .should('be.visible') .within(() => { - e2e.components.Tab.title('Query').should('be.visible'); + e2e.components.Tab.title('Queries').should('be.visible'); // data should be the active tab e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Query1'); // there's already a query so therefore Query + 1 + expect(li.text()).equals('Queries1'); // there's already a query so therefore Query + 1 }); - e2e.components.QueryTab.content().should('be.visible'); + // cy.get('[data-testid]="query-editor-rows"').should('be.visible'); + cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('be.visible'); e2e.components.TransformTab.content().should('not.exist'); e2e.components.AlertTab.content().should('not.exist'); e2e.components.PanelAlertTabContent.content().should('not.exist'); // Bottom pane tabs // Can change to Transform tab - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Transform data0'); // there's no transform so therefore Transform + 0 + expect(li.text()).equals('Transformations0'); // there's no transform so therefore Transform + 0 }); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible'); - e2e.components.QueryTab.content().should('not.exist'); + cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); e2e.components.AlertTab.content().should('not.exist'); e2e.components.PanelAlertTabContent.content().should('not.exist'); // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) - e2e.components.Tab.title('Alert').should('be.visible').click(); + e2e.components.Tab.title('Alert').scrollIntoView().should('be.visible').click(); e2e.components.Tab.active().should('have.text', 'Alert0'); // there's no alert so therefore Alert + 0 // Needs to be disabled until Grafana EE turns unified alerting on by default // e2e.components.AlertTab.content().should('not.exist'); - e2e.components.QueryTab.content().should('not.exist'); + cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); e2e.components.TransformTab.content().should('not.exist'); // Needs to be disabled until Grafana EE turns unified alerting on by default // e2e.components.PanelAlertTabContent.content().should('exist'); // e2e.components.PanelAlertTabContent.content().should('be.visible'); - e2e.components.Tab.title('Query').should('be.visible').click(); + e2e.components.Tab.title('Queries').should('be.visible').click(); }); - // Panel sidebar is rendered open by default - e2e.components.PanelEditor.OptionsPane.content().should('be.visible'); - - // close options pane - e2e.components.PanelEditor.toggleVizOptions().click(); - e2e.components.PanelEditor.OptionsPane.content().should('not.exist'); - - // open options pane - e2e.components.PanelEditor.toggleVizOptions().should('be.visible').click(); - e2e.components.PanelEditor.OptionsPane.content().should('be.visible'); - // Check that Time series is chosen e2e.components.PanelEditor.toggleVizPicker().click(); e2e.components.PluginVisualization.item('Time series').should('be.visible'); @@ -102,6 +95,8 @@ describe('Panel edit tests', () => { e2e.components.PanelEditor.DataPane.content().should('be.visible'); // Field & Overrides tabs (need to switch to React based vis, i.e. Table) + e2e.components.PanelEditor.toggleTableView().click({ force: true }).click({ force: true }); + e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible'); }); diff --git a/e2e/panels-suite/panelEdit_transforms.spec.ts b/e2e/panels-suite/panelEdit_transforms.spec.ts index eb3029b8d98..963991a22e7 100644 --- a/e2e/panels-suite/panelEdit_transforms.spec.ts +++ b/e2e/panels-suite/panelEdit_transforms.spec.ts @@ -8,7 +8,7 @@ describe('Panel edit tests - transformations', () => { it('Tests transformations editor', () => { e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); e2e.components.Transforms.Reduce.calculationsLabel().scrollIntoView().should('be.visible'); @@ -18,7 +18,7 @@ describe('Panel edit tests - transformations', () => { it('Tests case where transformations can be disabled and not clear out panel data', () => { e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); e2e.components.Transforms.disableTransformationButton().should('be.visible').click(); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts index 6346151f2cd..7c7711fa625 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts @@ -2,9 +2,10 @@ import { expect, test } from '@grafana/plugin-e2e'; test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { await page.goto('/'); - const homePageTitle = await page.title(); + const homePageURL = new URL(page.url()); await page.goto('/datasources', { waitUntil: 'networkidle' }); - expect(await page.title()).toEqual(homePageTitle); + const redirectedPageURL = new URL(page.url()); + expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname); }); test('current user should have viewer role', async ({ page, request }) => { diff --git a/e2e/run-suite b/e2e/run-suite index 38770c2becb..11569b43982 100755 --- a/e2e/run-suite +++ b/e2e/run-suite @@ -27,7 +27,7 @@ declare -A env=( testFilesForSingleSuite="*.spec.ts" rootForEnterpriseSuite="./e2e/extensions-suite" -rootForScenesSuite="./e2e/scenes" +rootForOldArch="./e2e/old-arch" declare -A cypressConfig=( [screenshotsFolder]=./e2e/"${args[0]}"/screenshots @@ -88,9 +88,9 @@ case "$1" in ;; "") ;; - "scenes") - env[SCENES]=true - cypressConfig[specPattern]=$rootForScenesSuite/*/$testFilesForSingleSuite + "old-arch") + env[DISABLE_SCENES]=true + cypressConfig[specPattern]=$rootForOldArch/*/$testFilesForSingleSuite cypressConfig[video]=false case "$2" in "debug") @@ -106,10 +106,10 @@ case "$1" in ;; esac ;; - "scenes/"*) + "old-arch/"*) cypressConfig[specPattern]=./e2e/"${args[0]}"/$testFilesForSingleSuite cypressConfig[video]=${args[1]} - env[SCENES]=true + env[DISABLE_SCENES]=true ;; "enterprise-smtp") env[SMTP_PLUGIN_ENABLED]=true diff --git a/e2e/shared/smokeTestScenario.ts b/e2e/shared/smokeTestScenario.ts index 4faee58a237..e45870283ec 100644 --- a/e2e/shared/smokeTestScenario.ts +++ b/e2e/shared/smokeTestScenario.ts @@ -6,6 +6,11 @@ export const smokeTestScenario = () => e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false); e2e.flows.addDataSource(); e2e.flows.addDashboard(); + e2e.flows.addPanel({ + dataSourceName: 'gdev-testdata', + visitDashboardAtStart: false, + timeout: 10000, + }); }); after(() => { @@ -13,11 +18,6 @@ export const smokeTestScenario = () => }); it('Login scenario, create test data source, dashboard, panel, and export scenario', () => { - // wait for time to be set to account for any layout shift - cy.contains('2020-01-01 00:00:00 to 2020-01-01 06:00:00').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add button').click(); - e2e.components.PageToolbar.itemButton('Add new visualization menu item').click(); - e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer() .should('be.visible') .within(() => { @@ -29,8 +29,7 @@ export const smokeTestScenario = () => // Make sure the graph renders via checking legend e2e.components.VizLegend.seriesName('A-series').should('be.visible'); - // Expand options section - e2e.components.PanelEditor.applyButton(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); // Make sure panel is & visualization is added to dashboard e2e.components.VizLegend.seriesName('A-series').should('be.visible'); diff --git a/e2e/smoke-tests-suite/panels_smokescreen.spec.ts b/e2e/smoke-tests-suite/panels_smokescreen.spec.ts index 5ba784d72b2..ca63dadba19 100644 --- a/e2e/smoke-tests-suite/panels_smokescreen.spec.ts +++ b/e2e/smoke-tests-suite/panels_smokescreen.spec.ts @@ -14,17 +14,11 @@ describe('Panels smokescreen', () => { it('Tests each panel type in the panel edit view to ensure no crash', () => { e2e.flows.addDashboard(); - // TODO: Try and use e2e.flows.addPanel() instead of block below - try { - e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add button').click(); - } catch (e) { - // Depending on the screen size, the "Add panel" button might be hidden - e2e.components.PageToolbar.item('Show more items').click(); - e2e.components.PageToolbar.item('Add button').last().click(); - } - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); + e2e.flows.addPanel({ + dataSourceName: 'gdev-testdata', + timeout: 10000, + visitDashboardAtStart: false, + }); cy.window().then((win: Cypress.AUTWindow & { grafanaBootData: GrafanaBootConfig['bootData'] }) => { // Loop through every panel type and ensure no crash diff --git a/e2e/utils/flows/addDashboard.ts b/e2e/utils/flows/addDashboard.ts index cc8e7548032..77dd07e9c9b 100644 --- a/e2e/utils/flows/addDashboard.ts +++ b/e2e/utils/flows/addDashboard.ts @@ -139,9 +139,9 @@ export const addDashboard = (config?: Partial) => { setDashboardTimeRange(timeRange); - e2e.components.PageToolbar.item('Save dashboard').click(); - e2e.pages.SaveDashboardAsModal.newName().clear().type(title, { force: true }); - e2e.pages.SaveDashboardAsModal.save().click(); + e2e.components.NavToolbar.editDashboard.saveButton().click(); + e2e.components.Drawer.DashboardSaveDrawer.saveAsTitleInput().clear().type(title, { force: true }); + e2e.components.Drawer.DashboardSaveDrawer.saveButton().click(); e2e.flows.assertSuccessNotification(); e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible'); diff --git a/e2e/utils/flows/configurePanel.ts b/e2e/utils/flows/configurePanel.ts index 4aa6a6cc5ab..7a881a47c17 100644 --- a/e2e/utils/flows/configurePanel.ts +++ b/e2e/utils/flows/configurePanel.ts @@ -85,15 +85,17 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC e2e.components.Panels.Panel.headerItems('Edit').click(); } else { try { - e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add button').click(); + //Enter edit mode + e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); + e2e.components.PageToolbar.itemButton('Add button').should('be.visible').click(); + e2e.components.NavToolbar.editDashboard.addVisualizationButton().should('be.visible').click(); } catch (e) { // Depending on the screen size, the "Add" button might be hidden e2e.components.PageToolbar.item('Show more items').click(); e2e.components.PageToolbar.item('Add button').last().click(); } - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); + // e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); + // e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); } if (timeRange) { diff --git a/e2e/utils/flows/importDashboard.ts b/e2e/utils/flows/importDashboard.ts index 4e7b8ade4ad..d3c6b7e23b5 100644 --- a/e2e/utils/flows/importDashboard.ts +++ b/e2e/utils/flows/importDashboard.ts @@ -51,7 +51,9 @@ export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: num e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click(); e2e.components.Tab.title('JSON').should('be.visible').click(); - e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click({ force: true }); + e2e.components.PanelInspector.Json.content().should('be.visible'); + e2e.components.ReactMonacoEditor.editorLazy().should('be.visible'); + cy.contains('Panel JSON').click({ force: true }); e2e.components.Select.option().should('be.visible').contains('Panel data').click(); // ensures that panel has loaded without knowingly hitting an error diff --git a/e2e/utils/support/url.ts b/e2e/utils/support/url.ts index 94e42827940..34620974b06 100644 --- a/e2e/utils/support/url.ts +++ b/e2e/utils/support/url.ts @@ -1,3 +1,5 @@ +import { e2e } from '../index'; + const getBaseUrl = () => Cypress.env('BASE_URL') || Cypress.config().baseUrl || 'http://localhost:3000'; export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href; diff --git a/e2e/various-suite/bar-gauge.spec.ts b/e2e/various-suite/bar-gauge.spec.ts index 5f8506b8050..7c28c48a146 100644 --- a/e2e/various-suite/bar-gauge.spec.ts +++ b/e2e/various-suite/bar-gauge.spec.ts @@ -11,7 +11,9 @@ describe('Bar Gauge Panel', () => { // open Panel Tests - Bar Gauge e2e.flows.openDashboard({ uid: 'O6f11TZWk' }); - cy.get(`[data-panelid=6] [data-testid^="${selectors.components.Panels.Visualization.BarGauge.valueV2}"]`) + cy.get( + `[data-viz-panel-key="panel-6"] [data-testid^="${selectors.components.Panels.Visualization.BarGauge.valueV2}"]` + ) .should('have.css', 'color', 'rgb(242, 73, 92)') .contains('100'); }); diff --git a/e2e/various-suite/exemplars.spec.ts b/e2e/various-suite/exemplars.spec.ts index b3c422c1294..675b83039ba 100644 --- a/e2e/various-suite/exemplars.spec.ts +++ b/e2e/various-suite/exemplars.spec.ts @@ -17,8 +17,8 @@ const addDataSource = () => { }, }); }; - -describe('Exemplars', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/exemplars.spec.ts +describe.skip('Exemplars', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); @@ -69,7 +69,7 @@ describe('Exemplars', () => { cy.get(`[data-testid="time-series-zoom-to-data"]`).click(); - e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove'); + e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove', { force: true }); cy.contains('Query with gdev-tempo').click(); e2e.components.TraceViewer.spanBar().should('have.length', 11); }); diff --git a/e2e/various-suite/filter-annotations.spec.ts b/e2e/various-suite/filter-annotations.spec.ts index 26cf83f00e2..35319ee1beb 100644 --- a/e2e/various-suite/filter-annotations.spec.ts +++ b/e2e/various-suite/filter-annotations.spec.ts @@ -1,7 +1,8 @@ import { e2e } from '../utils'; const DASHBOARD_ID = 'ed155665'; -describe('Annotations filtering', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/filter-annotations.spec.ts +describe.skip('Annotations filtering', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -9,7 +10,9 @@ describe('Annotations filtering', () => { it('Tests switching filter type updates the UI accordingly', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID }); - e2e.components.PageToolbar.item('Dashboard settings').click(); + e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); + e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); + e2e.components.Tab.title('Annotations').click(); cy.contains('New query').click(); e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type('Red - Panel two'); @@ -35,20 +38,30 @@ describe('Annotations filtering', () => { .type('Panel two{enter}', { force: true }); }); - e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click({ force: true }); + cy.get('body').click(); - e2e.pages.Dashboard.SubMenu.Annotations.annotationsWrapper() + e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); + + e2e.pages.Dashboard.Controls() .should('be.visible') .within(() => { - e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red - Panel two').should('be.visible'); - e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red - Panel two') - .should('be.checked') - .uncheck({ force: true }) - .should('not.be.checked') - .check({ force: true }); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('Red - Panel two') + .should('be.visible') + .parent() + .within((el) => { + cy.get('input') + .should('be.checked') + .uncheck({ force: true }) + .should('not.be.checked') + .check({ force: true }); + }); - e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red, only panel 1').should('be.visible'); - e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red, only panel 1').should('be.checked'); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('Red, only panel 1') + .should('be.visible') + .parent() + .within((el) => { + cy.get('input').should('be.checked'); + }); }); e2e.components.Panels.Panel.title('Panel one') diff --git a/e2e/various-suite/frontend-sandbox-datasource.spec.ts b/e2e/various-suite/frontend-sandbox-datasource.spec.ts index bfc3fd3fdc3..651ba170db9 100644 --- a/e2e/various-suite/frontend-sandbox-datasource.spec.ts +++ b/e2e/various-suite/frontend-sandbox-datasource.spec.ts @@ -6,7 +6,8 @@ const DATASOURCE_ID = 'sandbox-test-datasource'; let DATASOURCE_CONNECTION_ID = ''; const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance'; -describe('Datasource sandbox', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts +describe.skip('Datasource sandbox', () => { before(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); @@ -85,6 +86,7 @@ describe('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); + // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); @@ -96,7 +98,6 @@ describe('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); - // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); @@ -129,7 +130,6 @@ describe('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); - // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); diff --git a/e2e/various-suite/keybinds.spec.ts b/e2e/various-suite/keybinds.spec.ts index 2f12aee7586..c3a8b71359d 100644 --- a/e2e/various-suite/keybinds.spec.ts +++ b/e2e/various-suite/keybinds.spec.ts @@ -1,7 +1,8 @@ import { e2e } from '../utils'; import { fromBaseUrl } from '../utils/support/url'; -describe('Keyboard shortcuts', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/keybinds.spec.ts +describe.skip('Keyboard shortcuts', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); @@ -33,12 +34,10 @@ describe('Keyboard shortcuts', () => { zone: 'Browser', }); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - let expectedRange = `Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00`; - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); cy.get('body').type('{ctrl}z'); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`; + let expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`; e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); }); diff --git a/e2e/various-suite/loki-table-explore-to-dash.spec.ts b/e2e/various-suite/loki-table-explore-to-dash.spec.ts index c56aa16b578..088745c7fca 100644 --- a/e2e/various-suite/loki-table-explore-to-dash.spec.ts +++ b/e2e/various-suite/loki-table-explore-to-dash.spec.ts @@ -110,7 +110,8 @@ const lokiQueryResult = { }, }; -describe('Loki Query Editor', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/loki-table-explore-to-dash.spec.ts +describe.skip('Loki Query Editor', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); diff --git a/e2e/various-suite/pie-chart.spec.ts b/e2e/various-suite/pie-chart.spec.ts index 2030aed53fe..c6d537a40a2 100644 --- a/e2e/various-suite/pie-chart.spec.ts +++ b/e2e/various-suite/pie-chart.spec.ts @@ -11,9 +11,8 @@ describe('Pie Chart Panel', () => { // open Panel Tests - Pie Chart e2e.flows.openDashboard({ uid: 'lVE-2YFMz' }); - cy.get(`[data-panelid=11] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]`).should( - 'have.length', - 5 - ); + cy.get( + `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` + ).should('have.length', 5); }); }); diff --git a/e2e/various-suite/prometheus-annotations.spec.ts b/e2e/various-suite/prometheus-annotations.spec.ts index b32620dda70..91392a145a1 100644 --- a/e2e/various-suite/prometheus-annotations.spec.ts +++ b/e2e/various-suite/prometheus-annotations.spec.ts @@ -14,7 +14,8 @@ const DATASOURCE_NAME = 'aprometheusAnnotationDS'; * */ function navigateToAnnotations() { - e2e.components.PageToolbar.item('Dashboard settings').click(); + e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); + e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); e2e.components.Tab.title('Annotations').click(); } @@ -67,7 +68,7 @@ describe('Prometheus annotations', () => { // series value as timestamp e2e.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp().scrollIntoView().should('exist'); - e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); // check that annotation exists cy.get('body').contains(annotationName); diff --git a/e2e/various-suite/prometheus-editor.spec.ts b/e2e/various-suite/prometheus-editor.spec.ts index d04d502a706..09ae51e4f7f 100644 --- a/e2e/various-suite/prometheus-editor.spec.ts +++ b/e2e/various-suite/prometheus-editor.spec.ts @@ -41,8 +41,8 @@ function navigateToEditor(editorType: editorType, name: string): void { e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(name).scrollIntoView().should('be.visible').click(); } - -describe('Prometheus query editor', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/prometheus-editor.spec.ts +describe.skip('Prometheus query editor', () => { it('should have a kickstart component', () => { navigateToEditor('Code', 'prometheus'); e2e.components.QueryBuilder.queryPatterns().scrollIntoView().should('exist'); diff --git a/e2e/various-suite/prometheus-variable-editor.spec.ts b/e2e/various-suite/prometheus-variable-editor.spec.ts index 8e91ce8c283..4b6c62427aa 100644 --- a/e2e/various-suite/prometheus-variable-editor.spec.ts +++ b/e2e/various-suite/prometheus-variable-editor.spec.ts @@ -11,7 +11,8 @@ const DATASOURCE_NAME = 'prometheusVariableDS'; * Click dashboard settings and then the variables tab */ function navigateToVariables() { - e2e.components.PageToolbar.item('Dashboard settings').click(); + e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); + e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); e2e.components.Tab.title('Variables').click(); } @@ -49,7 +50,7 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) { e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); // close to return to dashboard - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); // add visualization e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible').click(); @@ -76,8 +77,8 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) { // do nothing } } - -describe('Prometheus variable query editor', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/prometheus-variable-editor.spec.ts +describe.skip('Prometheus variable query editor', () => { beforeEach(() => { createPromDS(DATASOURCE_ID, DATASOURCE_NAME); }); diff --git a/package.json b/package.json index 68a167324ab..1d94cc0a482 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@emotion/eslint-plugin": "11.12.0", "@grafana/eslint-config": "7.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "1.8.1", + "@grafana/plugin-e2e": "^1.8.2", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", "@playwright/test": "1.47.2", diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d37138665a3..35f58666dd4 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -67,9 +67,10 @@ var ( { Name: "publicDashboardsScene", Description: "Enables public dashboard rendering using scenes", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaSharingSquad, + Expression: "true", // enabled by default }, { Name: "lokiExperimentalStreaming", @@ -888,23 +889,26 @@ var ( { Name: "dashboardSceneForViewers", Description: "Enables dashboard rendering using Scenes for viewer roles", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaDashboardsSquad, + Expression: "true", // enabled by default }, { Name: "dashboardSceneSolo", Description: "Enables rendering dashboards using scenes for solo panels", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaDashboardsSquad, + Expression: "true", // enabled by default }, { Name: "dashboardScene", Description: "Enables dashboard rendering using scenes for all roles", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaDashboardsSquad, + Expression: "true", // enabled by default }, { Name: "panelFilterVariable", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d00de03a658..de789afd8ae 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -5,7 +5,7 @@ queryOverLive,experimental,@grafana/dashboards-squad,false,false,true panelTitleSearch,preview,@grafana/search-and-storage,false,false,false publicDashboards,GA,@grafana/sharing-squad,false,false,false publicDashboardsEmailSharing,preview,@grafana/sharing-squad,false,false,false -publicDashboardsScene,experimental,@grafana/sharing-squad,false,false,true +publicDashboardsScene,GA,@grafana/sharing-squad,false,false,true lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,false featureHighlights,GA,@grafana/grafana-as-code,false,false,false storage,experimental,@grafana/search-and-storage,false,false,false @@ -117,9 +117,9 @@ alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false extractFieldsNameDeduplication,experimental,@grafana/dataviz-squad,false,false,true -dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,true -dashboardSceneSolo,experimental,@grafana/dashboards-squad,false,false,true -dashboardScene,experimental,@grafana/dashboards-squad,false,false,true +dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true +dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true +dashboardScene,GA,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/sharing-squad,false,false,false ssoSettingsApi,GA,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 14973b66edb..2c4fb70bb06 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -840,40 +840,52 @@ { "metadata": { "name": "dashboardScene", - "resourceVersion": "1718727528075", - "creationTimestamp": "2023-11-13T08:51:21Z" + "resourceVersion": "1727354524763", + "creationTimestamp": "2023-11-13T08:51:21Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC" + } }, "spec": { "description": "Enables dashboard rendering using scenes for all roles", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/dashboards-squad", - "frontend": true + "frontend": true, + "expression": "true" } }, { "metadata": { "name": "dashboardSceneForViewers", - "resourceVersion": "1718727528075", - "creationTimestamp": "2023-11-02T19:02:25Z" + "resourceVersion": "1727354524763", + "creationTimestamp": "2023-11-02T19:02:25Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC" + } }, "spec": { "description": "Enables dashboard rendering using Scenes for viewer roles", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/dashboards-squad", - "frontend": true + "frontend": true, + "expression": "true" } }, { "metadata": { "name": "dashboardSceneSolo", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-02-11T08:08:47Z" + "resourceVersion": "1727354524763", + "creationTimestamp": "2024-02-11T08:08:47Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC" + } }, "spec": { "description": "Enables rendering dashboards using scenes for solo panels", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/dashboards-squad", - "frontend": true + "frontend": true, + "expression": "true" } }, { @@ -2516,14 +2528,18 @@ { "metadata": { "name": "publicDashboardsScene", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-03-22T14:48:21Z" + "resourceVersion": "1727354524763", + "creationTimestamp": "2024-03-22T14:48:21Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC" + } }, "spec": { "description": "Enables public dashboard rendering using scenes", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/sharing-squad", - "frontend": true + "frontend": true, + "expression": "true" } }, { diff --git a/scripts/drone/pipelines/build.star b/scripts/drone/pipelines/build.star index 722e2ca1136..b330fc61fdc 100644 --- a/scripts/drone/pipelines/build.star +++ b/scripts/drone/pipelines/build.star @@ -97,13 +97,13 @@ def build_e2e(trigger, ver_mode): build_test_plugins_step(), grafana_server_step(), e2e_tests_step("dashboards-suite"), - e2e_tests_step("scenes/dashboards-suite"), + e2e_tests_step("old-arch/dashboards-suite"), e2e_tests_step("smoke-tests-suite"), - e2e_tests_step("scenes/smoke-tests-suite"), + e2e_tests_step("old-arch/smoke-tests-suite"), e2e_tests_step("panels-suite"), - e2e_tests_step("scenes/panels-suite"), + e2e_tests_step("old-arch/panels-suite"), e2e_tests_step("various-suite"), - e2e_tests_step("scenes/various-suite"), + e2e_tests_step("old-arch/various-suite"), cloud_plugins_e2e_tests_step( "cloud-plugins-suite", cloud = "azure", diff --git a/yarn.lock b/yarn.lock index f7d85e7980c..42b0af03219 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3949,16 +3949,16 @@ __metadata: languageName: unknown linkType: soft -"@grafana/plugin-e2e@npm:1.8.1": - version: 1.8.1 - resolution: "@grafana/plugin-e2e@npm:1.8.1" +"@grafana/plugin-e2e@npm:^1.8.2": + version: 1.8.2 + resolution: "@grafana/plugin-e2e@npm:1.8.2" dependencies: semver: "npm:^7.5.4" uuid: "npm:^10.0.0" yaml: "npm:^2.3.4" peerDependencies: "@playwright/test": ^1.41.2 - checksum: 10/eca47ddc1b3a2cfbeea3f8c1a5f438b631a8cc689947e9a1c2f7171d3896fe319492e4a3e10c6a27ee34ab4140cffc9dad3bd723e0eab837a8acd4c57e8ed477 + checksum: 10/33afac70ec9a926d41f2cc1a07f818f3705e63a33d5c9ce85569b19ec585fed1d8e863a9320c54ca6dc96f8c1e20699bb6ad44714e91ba895c3c937395aa9b16 languageName: node linkType: hard @@ -18943,7 +18943,7 @@ __metadata: "@grafana/lezer-logql": "npm:0.2.6" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/o11y-ds-frontend": "workspace:*" - "@grafana/plugin-e2e": "npm:1.8.1" + "@grafana/plugin-e2e": "npm:^1.8.2" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" From e6c962e37c6d0346861fceff3ed008f501a8073a Mon Sep 17 00:00:00 2001 From: antonio <45235678+tonypowa@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:05:39 +0200 Subject: [PATCH 070/174] docs>tutorial:improve set up section (#93988) * docs>tutorial:improve set up section * removed section * simplified content/fixed link --- .../tutorials/alerting-get-started/index.md | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/docs/sources/tutorials/alerting-get-started/index.md b/docs/sources/tutorials/alerting-get-started/index.md index 169b1634b65..7ebea1418eb 100644 --- a/docs/sources/tutorials/alerting-get-started/index.md +++ b/docs/sources/tutorials/alerting-get-started/index.md @@ -49,31 +49,43 @@ Before you dive in, remember that you can [explore advanced topics like alert in {{< /docs/ignore >}} - + + +{{< docs/ignore >}} + +## Set up the Grafana stack + +{{< /docs/ignore >}} ## Before you begin -### Grafana Cloud users +There are different ways you can follow along with this tutorial. -As a Grafana Cloud user, you don't have to install anything. +### Grafana Cloud - +As a Grafana Cloud user, you don't have to install anything. [Create your free account](http://grafana.com/auth/sign-up/create-user). Continue to [Create a contact point](#create-a-contact-point). - +### Interactive learning environment -### Grafana OSS users +Alternatively, you can try out this example in our interactive learning environment: [Get started with Grafana Alerting](https://killercoda.com/grafana-labs/course/grafana/alerting-get-started/). -In order to run a Grafana stack locally, ensure you have the following applications installed. +It's a fully configured environment with all the dependencies already installed. + +### Grafana OSS + +If you opt to run a Grafana stack locally, ensure you have the following applications installed: - [Docker Compose](https://docs.docker.com/get-docker/) (included in Docker for Desktop for macOS and Windows) - [Git](https://git-scm.com/) -#### Set up the Grafana Stack (OSS users) +#### Set up the Grafana stack (OSS users) -To demonstrate the observation of data using the Grafana stack, download the files to your local machine. + + +To demonstrate the observation of data using the Grafana stack, download and run the following files. 1. Clone the [tutorial environment repository](https://www.github.com/grafana/tutorial-environment). @@ -135,19 +147,6 @@ To demonstrate the observation of data using the Grafana stack, download the fil {{< /docs/ignore >}} - - - {{< admonition type="tip" >}} - Alternatively, you can try out this example in our interactive learning environment: [Get started with Grafana Alerting](https://killercoda.com/grafana-labs/course/grafana/alerting-get-started/). - - It's a fully configured environment with all the dependencies already installed. - - ![Interactive](/media/docs/grafana/full-stack-ile.png) - - Provide feedback, report bugs, and raise issues in the [Grafana Killercoda repository](https://github.com/grafana/killercoda). - {{< /admonition >}} - - From 1c14c85b9768c8b2b9dde44cef1f48db82090982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 30 Sep 2024 12:19:24 +0200 Subject: [PATCH 071/174] Dashboards: Fixes view & edit keyboard shortcuts when grafana is behind a subpath (#93955) DashboardScene: Fixes view & edit keyboard shortcuts when grafana is behind a subpath --- .../features/dashboard-scene/scene/keyboardShortcuts.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index 10fc2feac74..e9399c8feb4 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -1,4 +1,4 @@ -import { SetPanelAttentionEvent } from '@grafana/data'; +import { locationUtil, SetPanelAttentionEvent } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { sceneGraph, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; @@ -43,7 +43,8 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { key: 'v', onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => { if (!scene.state.viewPanelScene) { - locationService.push(getViewPanelUrl(vizPanel)); + const url = locationUtil.stripBaseFromUrl(getViewPanelUrl(vizPanel)); + locationService.push(url); } }), }); @@ -173,7 +174,8 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { if (sceneRoot instanceof DashboardScene) { const panelId = getPanelIdForVizPanel(vizPanel); if (!scene.state.editPanel) { - locationService.push(getEditPanelUrl(panelId)); + const url = locationUtil.stripBaseFromUrl(getEditPanelUrl(panelId)); + locationService.push(url); } } }), From b17e256a3c85667de9ba3a059fec9bc6f74f3501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 30 Sep 2024 12:19:47 +0200 Subject: [PATCH 072/174] DashboardScene: Fixes url issue with subpath when exiting edit mode (#93962) --- .../scene/DashboardScene.test.tsx | 14 +++++++++++++- .../dashboard-scene/scene/DashboardScene.tsx | 18 +++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 6dd0aedbfdf..741bc4bb5a7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1,4 +1,4 @@ -import { CoreApp, LoadingState, getDefaultTimeRange, store } from '@grafana/data'; +import { CoreApp, GrafanaConfig, LoadingState, getDefaultTimeRange, locationUtil, store } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { sceneGraph, @@ -76,6 +76,12 @@ jest.mock('app/features/manage-dashboards/state/actions', () => ({ deleteDashboard: jest.fn().mockResolvedValue({}), })); +locationUtil.initialize({ + config: { appSubUrl: '/subUrl' } as GrafanaConfig, + getVariablesUrlParams: jest.fn(), + getTimeRangeForUrl: jest.fn(), +}); + const worker = createWorker(); mockResultsOfDetectChangesWorker({ hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: false }); @@ -134,6 +140,7 @@ describe('DashboardScene', () => { beforeEach(() => { scene = buildTestScene(); + locationService.push('/d/dash-1'); deactivateScene = scene.activate(); scene.onEnterEditMode(); jest.clearAllMocks(); @@ -143,6 +150,11 @@ describe('DashboardScene', () => { expect(scene.state.isEditing).toBe(true); }); + it('Can exit edit mode', () => { + scene.exitEditMode({ skipConfirm: true }); + expect(locationService.getLocation().pathname).toBe('/d/dash-1'); + }); + it('Exiting already saved dashboard should not restore initial state', () => { scene.setState({ title: 'Updated title' }); expect(scene.state.isDirty).toBe(true); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 72c7025a25f..da51aa2499a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -301,15 +301,15 @@ export class DashboardScene extends SceneObjectBase { // We are updating url and removing editview and editPanel. // The initial url may be including edit view, edit panel or inspect query params if the user pasted the url, // hence we need to cleanup those query params to get back to the dashboard view. Otherwise url sync can trigger overlays. - locationService.replace( - locationUtil.getUrlForPartial(this._initialUrlState!, { - editPanel: null, - editview: null, - inspect: null, - inspectTab: null, - shareView: null, - }) - ); + const url = locationUtil.getUrlForPartial(this._initialUrlState!, { + editPanel: null, + editview: null, + inspect: null, + inspectTab: null, + shareView: null, + }); + + locationService.replace(locationUtil.stripBaseFromUrl(url)); if (this._fromExplore) { this.cleanupStateFromExplore(); From 673b98cf1031cdc6dfadd79c6a07fd288b8bd460 Mon Sep 17 00:00:00 2001 From: Ronald McCollam Date: Mon, 30 Sep 2024 06:56:02 -0400 Subject: [PATCH 073/174] Add PostgreSQL example dashboard link (#93854) Co-authored-by: Jack Baldry --- docs/sources/datasources/postgres/_index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/datasources/postgres/_index.md b/docs/sources/datasources/postgres/_index.md index 86f769d5a77..4b1ab7898c4 100644 --- a/docs/sources/datasources/postgres/_index.md +++ b/docs/sources/datasources/postgres/_index.md @@ -66,6 +66,8 @@ For instructions on how to add a data source to Grafana, refer to the [administr Only users with the organization administrator role can add data sources. Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system. +{{< docs/play title="PostgreSQL Overview" url="https://play.grafana.org/d/ddvpgdhiwjvuod/postgresql-overview" >}} + ## PostgreSQL settings To configure basic settings for the data source, complete the following steps: From e1146120f48b30aecd1589dbb213a9e3d438d8b3 Mon Sep 17 00:00:00 2001 From: Ronald McCollam Date: Mon, 30 Sep 2024 06:56:12 -0400 Subject: [PATCH 074/174] Update MySQL example dashboard (#93853) Co-authored-by: Jack Baldry --- docs/sources/datasources/mysql/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/datasources/mysql/_index.md b/docs/sources/datasources/mysql/_index.md index 3125a0b2452..783d7ee1a2f 100644 --- a/docs/sources/datasources/mysql/_index.md +++ b/docs/sources/datasources/mysql/_index.md @@ -73,7 +73,7 @@ For instructions on how to add a data source to Grafana, refer to the [administr Only users with the organization administrator role can add data sources. Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system. -{{< docs/play title="MySQL: Cities of the World Sample Data Set" url="https://play.grafana.org/d/8JOvPQr7k/" >}} +{{< docs/play title="MySQL Overview" url="https://play.grafana.org/d/edyh1ib7db6rkb/mysql-overview" >}} ## Configure the data source From 0a26c9e9aec15ba0d21a836e1cbc1b03bad4bc34 Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Mon, 30 Sep 2024 13:14:07 +0200 Subject: [PATCH 075/174] Unistore : Ensure Watch works in HA mode. (#93428) * Replace Watch with WatchNext * remove watchset * fix previous page and closing the channel * Remove the broadcaster cache to prevent dupplicated events * add watch bookmark * add watch bookmark * cleanup comments * disable the tests for bookmarks for now * Ensure we send previosu events * lint * re-introduce the cache * load from cache * disabling legacy test * disabling legacy test * Update pkg/storage/unified/resource/server.go Co-authored-by: Diego Augusto Molina * Could not read previous events * add proper migration * Add previous_resource_version to both history and resource * First event should have an RV of 2 and not 1 * Test both storage backends * fix the inital RV for the sql backend * ensure graceful stop of the stream decoder * gocyclo --------- Co-authored-by: Diego Augusto Molina --- .../storage/testing/watcher_tests.go | 35 +- pkg/storage/unified/apistore/store.go | 164 +------- pkg/storage/unified/apistore/store_test.go | 13 +- pkg/storage/unified/apistore/stream.go | 57 ++- pkg/storage/unified/apistore/watcher_test.go | 245 +++++++++--- pkg/storage/unified/apistore/watchset.go | 376 ------------------ pkg/storage/unified/resource/server.go | 118 ++++-- pkg/storage/unified/sql/backend.go | 46 ++- pkg/storage/unified/sql/backend_test.go | 8 +- .../sql/data/resource_history_insert.sql | 2 + .../sql/data/resource_history_poll.sql | 3 +- .../unified/sql/data/resource_insert.sql | 2 + .../sql/data/resource_version_insert.sql | 2 +- .../unified/sql/db/migrations/resource_mig.go | 20 +- pkg/storage/unified/sql/queries.go | 2 + pkg/storage/unified/sql/queries_test.go | 15 +- ...ry_insert-insert into resource_history.sql | 2 + ...sql--resource_history_poll-single path.sql | 16 + .../mysql--resource_insert-simple.sql | 2 + ...l--resource_version_insert-single path.sql | 2 +- ...ry_insert-insert into resource_history.sql | 2 + ...res--resource_history_poll-single path.sql | 16 + .../postgres--resource_insert-simple.sql | 2 + ...s--resource_version_insert-single path.sql | 2 +- ...ry_insert-insert into resource_history.sql | 2 + ...ite--resource_history_poll-single path.sql | 16 + .../sqlite--resource_insert-simple.sql | 2 + ...e--resource_version_insert-single path.sql | 2 +- 28 files changed, 475 insertions(+), 699 deletions(-) delete mode 100644 pkg/storage/unified/apistore/watchset.go create mode 100755 pkg/storage/unified/sql/testdata/mysql--resource_history_poll-single path.sql create mode 100755 pkg/storage/unified/sql/testdata/postgres--resource_history_poll-single path.sql create mode 100755 pkg/storage/unified/sql/testdata/sqlite--resource_history_poll-single path.sql diff --git a/pkg/apiserver/storage/testing/watcher_tests.go b/pkg/apiserver/storage/testing/watcher_tests.go index 1684f15819f..213b7553faa 100644 --- a/pkg/apiserver/storage/testing/watcher_tests.go +++ b/pkg/apiserver/storage/testing/watcher_tests.go @@ -1407,22 +1407,25 @@ func RunWatchSemantics(ctx context.Context, t *testing.T, store storage.Interfac podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, }, - - { - name: "legacy, RV=0", - resourceVersion: "0", - initialPods: []*example.Pod{makePod("1"), makePod("2"), makePod("3")}, - expectedInitialEventsInRandomOrder: addEventsFromCreatedPods, - podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, - expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, - }, - { - name: "legacy, RV=unset", - initialPods: []*example.Pod{makePod("1"), makePod("2"), makePod("3")}, - expectedInitialEventsInRandomOrder: addEventsFromCreatedPods, - podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, - expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, - }, + // Not Supported by unistore because there is no way to differentiate between: + // - SendInitialEvents=nil && resourceVersion=0 + // - sendInitialEvents=false && resourceVersion=0 + // This is a Legacy feature in k8s.io/apiserver/pkg/storage/etcd3/watcher_test.go#196 + // { + // name: "legacy, RV=0", + // resourceVersion: "0", + // initialPods: []*example.Pod{makePod("1"), makePod("2"), makePod("3")}, + // expectedInitialEventsInRandomOrder: addEventsFromCreatedPods, + // podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, + // expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, + // }, + // { + // name: "legacy, RV=unset", + // initialPods: []*example.Pod{makePod("1"), makePod("2"), makePod("3")}, + // expectedInitialEventsInRandomOrder: addEventsFromCreatedPods, + // podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, + // expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, + // }, } for idx, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { diff --git a/pkg/storage/unified/apistore/store.go b/pkg/storage/unified/apistore/store.go index 4d2947ff54b..d4911da167c 100644 --- a/pkg/storage/unified/apistore/store.go +++ b/pkg/storage/unified/apistore/store.go @@ -26,7 +26,6 @@ import ( "k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/apiserver/pkg/storage/storagebackend/factory" "k8s.io/client-go/tools/cache" - "k8s.io/klog/v2" "github.com/grafana/grafana/pkg/apimachinery/utils" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" @@ -51,7 +50,6 @@ type Storage struct { store resource.ResourceClient getKey func(string) (*resource.ResourceKey, error) - watchSet *WatchSet versioner storage.Versioner } @@ -84,8 +82,7 @@ func NewStorage( trigger: trigger, indexers: indexers, - watchSet: NewWatchSet(), - getKey: keyParser, + getKey: keyParser, versioner: &storage.APIObjectVersioner{}, } @@ -112,9 +109,7 @@ func NewStorage( } } - return s, func() { - s.watchSet.cleanupWatchers() - }, nil + return s, func() {}, nil } func (s *Storage) Versioner() storage.Versioner { @@ -165,11 +160,6 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou }) } - s.watchSet.notifyWatchers(watch.Event{ - Object: out.DeepCopyObject(), - Type: watch.Added, - }, nil) - return nil } @@ -226,16 +216,11 @@ func (s *Storage) Delete( if err := s.versioner.UpdateObject(out, uint64(rsp.ResourceVersion)); err != nil { return err } - - s.watchSet.notifyWatchers(watch.Event{ - Object: out.DeepCopyObject(), - Type: watch.Deleted, - }, nil) return nil } // This version is not yet passing the watch tests -func (s *Storage) WatchNEXT(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { +func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { k, err := s.getKey(key) if err != nil { return watch.NewEmptyWatch(), nil @@ -255,10 +240,11 @@ func (s *Storage) WatchNEXT(ctx context.Context, key string, opts storage.ListOp if opts.SendInitialEvents != nil { cmd.SendInitialEvents = *opts.SendInitialEvents } - + ctx, cancelWatch := context.WithCancel(ctx) client, err := s.store.Watch(ctx, cmd) if err != nil { // if the context was canceled, just return a new empty watch + cancelWatch() if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.EOF) { return watch.NewEmptyWatch(), nil } @@ -266,138 +252,11 @@ func (s *Storage) WatchNEXT(ctx context.Context, key string, opts storage.ListOp } reporter := apierrors.NewClientErrorReporter(500, "WATCH", "") - decoder := &streamDecoder{ - client: client, - newFunc: s.newFunc, - predicate: predicate, - codec: s.codec, - } + decoder := newStreamDecoder(client, s.newFunc, predicate, s.codec, cancelWatch) return watch.NewStreamWatcher(decoder, reporter), nil } -// Watch begins watching the specified key. Events are decoded into API objects, -// and any items selected by the predicate are sent down to returned watch.Interface. -// resourceVersion may be used to specify what version to begin watching, -// which should be the current resourceVersion, and no longer rv+1 -// (e.g. reconnecting without missing any updates). -// If resource version is "0", this interface will get current object at given key -// and send it in an "ADDED" event, before watch starts. -func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { - k, err := s.getKey(key) - if err != nil { - return watch.NewEmptyWatch(), nil - } - - req, predicate, err := toListRequest(k, opts) - if err != nil { - return watch.NewEmptyWatch(), nil - } - - listObj := s.newListFunc() - - var namespace *string - if k.Namespace != "" { - namespace = &k.Namespace - } - - if ctx.Err() != nil { - return watch.NewEmptyWatch(), nil - } - - if (opts.SendInitialEvents == nil && req.ResourceVersion == 0) || (opts.SendInitialEvents != nil && *opts.SendInitialEvents) { - if err := s.GetList(ctx, key, opts, listObj); err != nil { - return nil, err - } - - listAccessor, err := meta.ListAccessor(listObj) - if err != nil { - klog.Errorf("could not determine new list accessor in watch") - return nil, err - } - // Updated if requesting RV was either "0" or "" - maybeUpdatedRV, err := s.versioner.ParseResourceVersion(listAccessor.GetResourceVersion()) - if err != nil { - klog.Errorf("could not determine new list RV in watch") - return nil, err - } - - jw := s.watchSet.newWatch(ctx, maybeUpdatedRV, predicate, s.versioner, namespace) - - initEvents := make([]watch.Event, 0) - listPtr, err := meta.GetItemsPtr(listObj) - if err != nil { - return nil, err - } - v, err := conversion.EnforcePtr(listPtr) - if err != nil || v.Kind() != reflect.Slice { - return nil, fmt.Errorf("need pointer to slice: %v", err) - } - - for i := 0; i < v.Len(); i++ { - obj, ok := v.Index(i).Addr().Interface().(runtime.Object) - if !ok { - return nil, fmt.Errorf("need item to be a runtime.Object: %v", err) - } - - initEvents = append(initEvents, watch.Event{ - Type: watch.Added, - Object: obj.DeepCopyObject(), - }) - } - - if predicate.AllowWatchBookmarks && len(initEvents) > 0 { - listRV, err := s.versioner.ParseResourceVersion(listAccessor.GetResourceVersion()) - if err != nil { - return nil, fmt.Errorf("could not get last init event's revision for bookmark: %v", err) - } - - bookmarkEvent := watch.Event{ - Type: watch.Bookmark, - Object: s.newFunc(), - } - - if err := s.versioner.UpdateObject(bookmarkEvent.Object, listRV); err != nil { - return nil, err - } - - bookmarkObject, err := meta.Accessor(bookmarkEvent.Object) - if err != nil { - return nil, fmt.Errorf("could not get bookmark object's acccesor: %v", err) - } - bookmarkObject.SetAnnotations(map[string]string{"k8s.io/initial-events-end": "true"}) - initEvents = append(initEvents, bookmarkEvent) - } - - jw.Start(initEvents...) - return jw, nil - } - - maybeUpdatedRV := uint64(req.ResourceVersion) - if maybeUpdatedRV == 0 { - rsp, err := s.store.List(ctx, &resource.ListRequest{ - Options: &resource.ListOptions{ - Key: k, - }, - Limit: 1, // we ignore the results, just look at the RV - }) - if err != nil { - return nil, err - } - if rsp.Error != nil { - return nil, resource.GetError(rsp.Error) - } - maybeUpdatedRV = uint64(rsp.ResourceVersion) - if maybeUpdatedRV < 1 { - return nil, fmt.Errorf("expecting a non-zero resource version") - } - } - jw := s.watchSet.newWatch(ctx, maybeUpdatedRV, predicate, s.versioner, namespace) - - jw.Start() - return jw, nil -} - // Get unmarshals object found at key into objPtr. On a not found error, will either // return a zero object of the requested type, or an error, depending on 'opts.ignoreNotFound'. // Treats empty responses and nil response nodes exactly like a not found error. @@ -668,17 +527,6 @@ func (s *Storage) GuaranteedUpdate( return err } - if created { - s.watchSet.notifyWatchers(watch.Event{ - Object: destination.DeepCopyObject(), - Type: watch.Added, - }, nil) - } else { - s.watchSet.notifyWatchers(watch.Event{ - Object: destination.DeepCopyObject(), - Type: watch.Modified, - }, existingObj.DeepCopyObject()) - } return nil } diff --git a/pkg/storage/unified/apistore/store_test.go b/pkg/storage/unified/apistore/store_test.go index 8977693c966..287aeea5c41 100644 --- a/pkg/storage/unified/apistore/store_test.go +++ b/pkg/storage/unified/apistore/store_test.go @@ -92,12 +92,13 @@ func TestCreate(t *testing.T) { storagetesting.RunTestCreate(ctx, t, store, checkStorageInvariants(store)) } -func TestCreateWithTTL(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestCreateWithTTL(ctx, t, store) -} +// No TTL support in unifed storage +// func TestCreateWithTTL(t *testing.T) { +// ctx, store, destroyFunc, err := testSetup(t) +// defer destroyFunc() +// assert.NoError(t, err) +// storagetesting.RunTestCreateWithTTL(ctx, t, store) +// } func TestCreateWithKeyExist(t *testing.T) { ctx, store, destroyFunc, err := testSetup(t) diff --git a/pkg/storage/unified/apistore/stream.go b/pkg/storage/unified/apistore/stream.go index a425279185a..9546e3e8b64 100644 --- a/pkg/storage/unified/apistore/stream.go +++ b/pkg/storage/unified/apistore/stream.go @@ -1,9 +1,11 @@ package apistore import ( + "context" "errors" "fmt" "io" + "sync" grpcCodes "google.golang.org/grpc/codes" grpcStatus "google.golang.org/grpc/status" @@ -17,12 +19,23 @@ import ( ) type streamDecoder struct { - client resource.ResourceStore_WatchClient - newFunc func() runtime.Object - predicate storage.SelectionPredicate - codec runtime.Codec + client resource.ResourceStore_WatchClient + newFunc func() runtime.Object + predicate storage.SelectionPredicate + codec runtime.Codec + cancelWatch context.CancelFunc + done sync.WaitGroup } +func newStreamDecoder(client resource.ResourceStore_WatchClient, newFunc func() runtime.Object, predicate storage.SelectionPredicate, codec runtime.Codec, cancelWatch context.CancelFunc) *streamDecoder { + return &streamDecoder{ + client: client, + newFunc: newFunc, + predicate: predicate, + codec: codec, + cancelWatch: cancelWatch, + } +} func (d *streamDecoder) toObject(w *resource.WatchEvent_Resource) (runtime.Object, error) { obj, _, err := d.codec.Decode(w.Value, nil, d.newFunc()) if err == nil { @@ -35,25 +48,30 @@ func (d *streamDecoder) toObject(w *resource.WatchEvent_Resource) (runtime.Objec return obj, err } +// nolint: gocyclo // we may be able to simplify this in the future, but this is a complex function by nature func (d *streamDecoder) Decode() (action watch.EventType, object runtime.Object, err error) { + d.done.Add(1) + defer d.done.Done() decode: for { - err := d.client.Context().Err() - if err != nil { - klog.Errorf("client: context error: %s\n", err) - return watch.Error, nil, err + var evt *resource.WatchEvent + var err error + select { + case <-d.client.Context().Done(): + default: + evt, err = d.client.Recv() } - evt, err := d.client.Recv() - if errors.Is(err, io.EOF) { + switch { + case errors.Is(d.client.Context().Err(), context.Canceled): + return watch.Error, nil, io.EOF + case d.client.Context().Err() != nil: + return watch.Error, nil, d.client.Context().Err() + case errors.Is(err, io.EOF): + return watch.Error, nil, io.EOF + case grpcStatus.Code(err) == grpcCodes.Canceled: return watch.Error, nil, err - } - - if grpcStatus.Code(err) == grpcCodes.Canceled { - return watch.Error, nil, err - } - - if err != nil { + case err != nil: klog.Errorf("client: error receiving result: %s", err) return watch.Error, nil, err } @@ -194,10 +212,15 @@ decode: } func (d *streamDecoder) Close() { + // Close the send stream err := d.client.CloseSend() if err != nil { klog.Errorf("error closing watch stream: %s", err) } + // Cancel the send context + d.cancelWatch() + // Wait for all decode operations to finish + d.done.Wait() } var _ watch.Decoder = (*streamDecoder)(nil) diff --git a/pkg/storage/unified/apistore/watcher_test.go b/pkg/storage/unified/apistore/watcher_test.go index fb4deb11811..203d14c0729 100644 --- a/pkg/storage/unified/apistore/watcher_test.go +++ b/pkg/storage/unified/apistore/watcher_test.go @@ -7,9 +7,9 @@ package apistore import ( "context" - "fmt" "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,7 +29,20 @@ import ( "k8s.io/apiserver/pkg/storage/storagebackend/factory" storagetesting "github.com/grafana/grafana/pkg/apiserver/storage/testing" + infraDB "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/sql" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +type StorageType string + +const ( + StorageTypeFile StorageType = "file" + StorageTypeUnified StorageType = "unified" ) var scheme = runtime.NewScheme() @@ -48,6 +61,7 @@ type setupOptions struct { prefix string resourcePrefix string groupResource schema.GroupResource + storageType StorageType } type setupOption func(*setupOptions, testing.TB) @@ -59,10 +73,20 @@ func withDefaults(options *setupOptions, t testing.TB) { options.prefix = t.TempDir() options.resourcePrefix = storagetesting.KeyFunc("", "") options.groupResource = schema.GroupResource{Resource: "pods"} + options.storageType = StorageTypeFile +} +func withStorageType(storageType StorageType) setupOption { + return func(options *setupOptions, t testing.TB) { + options.storageType = storageType + } } var _ setupOption = withDefaults +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Interface, factory.DestroyFunc, error) { setupOpts := setupOptions{} opts = append([]setupOption{withDefaults}, opts...) @@ -85,18 +109,56 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte Metadata: fileblob.MetadataDontWrite, // skip }) require.NoError(t, err) - fmt.Printf("ROOT: %s\n\n", tmp) } ctx := storagetesting.NewContext() - backend, err := resource.NewCDKBackend(ctx, resource.CDKBackendOptions{ - Bucket: bucket, - }) - require.NoError(t, err) - server, err := resource.NewResourceServer(resource.ResourceServerOptions{ - Backend: backend, - }) - require.NoError(t, err) + var server resource.ResourceServer + switch setupOpts.storageType { + case StorageTypeFile: + backend, err := resource.NewCDKBackend(ctx, resource.CDKBackendOptions{ + Bucket: bucket, + }) + require.NoError(t, err) + + server, err = resource.NewResourceServer(resource.ResourceServerOptions{ + Backend: backend, + }) + require.NoError(t, err) + + // Issue a health check to ensure the server is initialized + _, err = server.IsHealthy(ctx, &resource.HealthCheckRequest{}) + require.NoError(t, err) + case StorageTypeUnified: + if testing.Short() { + t.Skip("skipping integration test") + } + dbstore := infraDB.InitTestDB(t) + cfg := setting.NewCfg() + features := featuremgmt.WithFeatures() + + eDB, err := dbimpl.ProvideResourceDB(dbstore, cfg, features, nil) + require.NoError(t, err) + require.NotNil(t, eDB) + + ret, err := sql.NewBackend(sql.BackendOptions{ + DBProvider: eDB, + PollingInterval: time.Millisecond, // Keep this fast + }) + require.NoError(t, err) + require.NotNil(t, ret) + ctx := storagetesting.NewContext() + err = ret.Init(ctx) + require.NoError(t, err) + + server, err = resource.NewResourceServer(resource.ResourceServerOptions{ + Backend: ret, + Diagnostics: ret, + Lifecycle: ret, + }) + require.NoError(t, err) + default: + t.Fatalf("unsupported storage type: %s", setupOpts.storageType) + } client := resource.NewLocalResourceClient(server) config := storagebackend.NewDefaultConfig(setupOpts.prefix, setupOpts.codec) @@ -124,55 +186,82 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte } func TestWatch(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatch(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t, withStorageType(s)) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatch(ctx, t, store) + }) + } } func TestClusterScopedWatch(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestClusterScopedWatch(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestClusterScopedWatch(ctx, t, store) + }) + } } func TestNamespaceScopedWatch(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestNamespaceScopedWatch(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestNamespaceScopedWatch(ctx, t, store) + }) + } } func TestDeleteTriggerWatch(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestDeleteTriggerWatch(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestDeleteTriggerWatch(ctx, t, store) + }) + } } -func TestWatchFromZero(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchFromZero(ctx, t, store, nil) -} +// Not Supported by unistore because there is no way to differentiate between: +// - SendInitialEvents=nil && resourceVersion=0 +// - sendInitialEvents=false && resourceVersion=0 +// This is a Legacy feature in k8s.io/apiserver/pkg/storage/etcd3/watcher_test.go#196 +// func TestWatchFromZero(t *testing.T) { +// ctx, store, destroyFunc, err := testSetup(t) +// defer destroyFunc() +// assert.NoError(t, err) +// storagetesting.RunTestWatchFromZero(ctx, t, store, nil) +// } // TestWatchFromNonZero tests that // - watch from non-0 should just watch changes after given version func TestWatchFromNonZero(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchFromNonZero(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchFromNonZero(ctx, t, store) + }) + } } +/* +Only valid when using a cached storage func TestDelayedWatchDelivery(t *testing.T) { ctx, store, destroyFunc, err := testSetup(t) defer destroyFunc() assert.NoError(t, err) storagetesting.RunTestDelayedWatchDelivery(ctx, t, store) } +/* /* func TestWatchError(t *testing.T) { @@ -182,24 +271,36 @@ func TestWatchError(t *testing.T) { */ func TestWatchContextCancel(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchContextCancel(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchContextCancel(ctx, t, store) + }) + } } func TestWatcherTimeout(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatcherTimeout(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatcherTimeout(ctx, t, store) + }) + } } func TestWatchDeleteEventObjectHaveLatestRV(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchDeleteEventObjectHaveLatestRV(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchDeleteEventObjectHaveLatestRV(ctx, t, store) + }) + } } // TODO: enable when we support flow control and priority fairness @@ -221,31 +322,47 @@ func TestWatchDeleteEventObjectHaveLatestRV(t *testing.T) { // setting allowWatchBookmarks query param against // etcd implementation doesn't have any effect. func TestWatchDispatchBookmarkEvents(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchDispatchBookmarkEvents(ctx, t, store, false) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchDispatchBookmarkEvents(ctx, t, store, false) + }) + } } func TestSendInitialEventsBackwardCompatibility(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunSendInitialEventsBackwardCompatibility(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunSendInitialEventsBackwardCompatibility(ctx, t, store) + }) + } } func TestEtcdWatchSemantics(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunWatchSemantics(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunWatchSemantics(ctx, t, store) + }) + } } func TestEtcdWatchSemanticInitialEventsExtended(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunWatchSemanticInitialEventsExtended(ctx, t, store) + for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { + t.Run(string(s), func(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunWatchSemanticInitialEventsExtended(ctx, t, store) + }) + } } func newPod() runtime.Object { diff --git a/pkg/storage/unified/apistore/watchset.go b/pkg/storage/unified/apistore/watchset.go deleted file mode 100644 index 9c9d214b4b6..00000000000 --- a/pkg/storage/unified/apistore/watchset.go +++ /dev/null @@ -1,376 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Provenance-includes-location: https://github.com/tilt-dev/tilt-apiserver/blob/main/pkg/storage/filepath/watchset.go -// Provenance-includes-license: Apache-2.0 -// Provenance-includes-copyright: The Kubernetes Authors. - -package apistore - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/storage" - "k8s.io/klog/v2" -) - -const ( - UpdateChannelSize = 25 - InitialWatchNodesSize = 20 - InitialBufferedEventsSize = 25 -) - -type eventWrapper struct { - ev watch.Event - // optional: oldObject is only set for modifications for determining their type as necessary (when using predicate filtering) - oldObject runtime.Object -} - -type watchNode struct { - ctx context.Context - s *WatchSet - id uint64 - updateCh chan eventWrapper - outCh chan watch.Event - requestedRV uint64 - // the watch may or may not be namespaced for a namespaced resource. This is always nil for cluster-scoped kinds - watchNamespace *string - predicate storage.SelectionPredicate - versioner storage.Versioner -} - -// Keeps track of which watches need to be notified -type WatchSet struct { - mu sync.RWMutex - // mu protects both nodes and counter - nodes map[uint64]*watchNode - counter atomic.Uint64 - buffered []eventWrapper - bufferedMutex sync.RWMutex -} - -func NewWatchSet() *WatchSet { - return &WatchSet{ - buffered: make([]eventWrapper, 0, InitialBufferedEventsSize), - nodes: make(map[uint64]*watchNode, InitialWatchNodesSize), - } -} - -// Creates a new watch with a unique id, but -// does not start sending events to it until start() is called. -func (s *WatchSet) newWatch(ctx context.Context, requestedRV uint64, p storage.SelectionPredicate, versioner storage.Versioner, namespace *string) *watchNode { - s.counter.Add(1) - - node := &watchNode{ - ctx: ctx, - requestedRV: requestedRV, - id: s.counter.Load(), - s: s, - // updateCh size needs to be > 1 to allow slower clients to not block passing new events - updateCh: make(chan eventWrapper, UpdateChannelSize), - // outCh size needs to be > 1 for single process use-cases such as tests where watch and event seeding from CUD - // events is happening on the same thread - outCh: make(chan watch.Event, UpdateChannelSize), - predicate: p, - watchNamespace: namespace, - versioner: versioner, - } - - return node -} - -func (s *WatchSet) cleanupWatchers() { - s.mu.Lock() - defer s.mu.Unlock() - for _, w := range s.nodes { - w.stop() - } -} - -// oldObject is only passed in the event of a modification -// in case a predicate filtered watch is impacted as a result of modification -// NOTE: this function gives one the misperception that a newly added node will never -// get a double event, one from buffered and one from the update channel -// That perception is not true. Even though this function maintains the lock throughout the function body -// it is not true of the Start function. So basically, the Start function running after this function -// fully stands the chance of another future notifyWatchers double sending it the event through the two means mentioned -func (s *WatchSet) notifyWatchers(ev watch.Event, oldObject runtime.Object) { - s.mu.RLock() - defer s.mu.RUnlock() - - updateEv := eventWrapper{ - ev: ev, - } - if oldObject != nil { - updateEv.oldObject = oldObject - } - - // Events are always buffered. - // this is because of an inadvertent delay which is built into the watch process - // Watch() from storage returns Watch.Interface with a async start func. - // The only way to guarantee that we can interpret the passed RV correctly is to play it against missed events - // (notice the loop below over s.nodes isn't exactly going to work on a new node - // unless start is called on it) - s.bufferedMutex.Lock() - s.buffered = append(s.buffered, updateEv) - s.bufferedMutex.Unlock() - - for _, w := range s.nodes { - w.updateCh <- updateEv - } -} - -// isValid is not necessary to be called on oldObject in UpdateEvents - assuming the Watch pushes correctly setup eventWrapper our way -// first bool is whether the event is valid for current watcher -// second bool is whether checking the old value against the predicate may be valuable to the caller -// second bool may be a helpful aid to establish context around MODIFIED events -// (note that this second bool is only marked true if we pass other checks first, namely RV and namespace) -func (w *watchNode) isValid(e eventWrapper) (bool, bool, error) { - obj, err := meta.Accessor(e.ev.Object) - if err != nil { - klog.Error("Could not get accessor to object in event") - return false, false, nil - } - - eventRV, err := w.getResourceVersionAsInt(e.ev.Object) - if err != nil { - return false, false, err - } - - if eventRV < w.requestedRV { - return false, false, nil - } - - if w.watchNamespace != nil && *w.watchNamespace != obj.GetNamespace() { - return false, false, err - } - - valid, err := w.predicate.Matches(e.ev.Object) - if err != nil { - return false, false, err - } - - return valid, e.ev.Type == watch.Modified, nil -} - -// Only call this method if current object matches the predicate -func (w *watchNode) handleAddedForFilteredList(e eventWrapper) (*watch.Event, error) { - if e.oldObject == nil { - return nil, fmt.Errorf("oldObject should be set for modified events") - } - - ok, err := w.predicate.Matches(e.oldObject) - if err != nil { - return nil, err - } - - if !ok { - e.ev.Type = watch.Added - return &e.ev, nil - } - - return nil, nil -} - -func (w *watchNode) handleDeletedForFilteredList(e eventWrapper) (*watch.Event, error) { - if e.oldObject == nil { - return nil, fmt.Errorf("oldObject should be set for modified events") - } - - ok, err := w.predicate.Matches(e.oldObject) - if err != nil { - return nil, err - } - - if !ok { - return nil, nil - } - - // isn't a match but used to be - e.ev.Type = watch.Deleted - - oldObjectAccessor, err := meta.Accessor(e.oldObject) - if err != nil { - klog.Errorf("Could not get accessor to correct the old RV of filtered out object") - return nil, err - } - - currentRV, err := getResourceVersion(e.ev.Object) - if err != nil { - klog.Errorf("Could not get accessor to object in event") - return nil, err - } - - oldObjectAccessor.SetResourceVersion(currentRV) - e.ev.Object = e.oldObject - - return &e.ev, nil -} - -func (w *watchNode) processEvent(e eventWrapper, isInitEvent bool) error { - if isInitEvent { - // Init events have already been vetted against the predicate and other RV behavior - // Let them pass through - w.outCh <- e.ev - return nil - } - - valid, runDeleteFromFilteredListHandler, err := w.isValid(e) - if err != nil { - klog.Errorf("Could not determine validity of the event: %v", err) - return err - } - if valid { - if e.ev.Type == watch.Modified { - ev, err := w.handleAddedForFilteredList(e) - if err != nil { - return err - } - if ev != nil { - w.outCh <- *ev - } else { - // forward the original event if add handling didn't signal any impact - w.outCh <- e.ev - } - } else { - w.outCh <- e.ev - } - return nil - } - - if runDeleteFromFilteredListHandler { - if e.ev.Type == watch.Modified { - ev, err := w.handleDeletedForFilteredList(e) - if err != nil { - return err - } - if ev != nil { - w.outCh <- *ev - } - } // explicitly doesn't have an event forward for the else case here - return nil - } - - return nil -} - -// Start sending events to this watch. -func (w *watchNode) Start(initEvents ...watch.Event) { - w.s.mu.Lock() - w.s.nodes[w.id] = w - w.s.mu.Unlock() - - go func() { - maxRV := uint64(0) - for _, ev := range initEvents { - currentRV, err := w.getResourceVersionAsInt(ev.Object) - if err != nil { - klog.Errorf("Could not determine init event RV for deduplication of buffered events: %v", err) - continue - } - - if maxRV < currentRV { - maxRV = currentRV - } - - if err := w.processEvent(eventWrapper{ev: ev}, true); err != nil { - klog.Errorf("Could not process event: %v", err) - } - } - - // If we had no init events, simply rely on the passed RV - if maxRV == 0 { - maxRV = w.requestedRV - } - - w.s.bufferedMutex.RLock() - for _, e := range w.s.buffered { - eventRV, err := w.getResourceVersionAsInt(e.ev.Object) - if err != nil { - klog.Errorf("Could not determine RV for deduplication of buffered events: %v", err) - continue - } - - if maxRV >= eventRV { - continue - } else { - maxRV = eventRV - } - - if err := w.processEvent(e, false); err != nil { - klog.Errorf("Could not process event: %v", err) - } - } - w.s.bufferedMutex.RUnlock() - - for { - select { - case e, ok := <-w.updateCh: - if !ok { - close(w.outCh) - return - } - - eventRV, err := w.getResourceVersionAsInt(e.ev.Object) - if err != nil { - klog.Errorf("Could not determine RV for deduplication of channel events: %v", err) - continue - } - - if maxRV >= eventRV { - continue - } else { - maxRV = eventRV - } - - if err := w.processEvent(e, false); err != nil { - klog.Errorf("Could not process event: %v", err) - } - case <-w.ctx.Done(): - close(w.outCh) - return - } - } - }() -} - -func (w *watchNode) Stop() { - w.s.mu.Lock() - defer w.s.mu.Unlock() - w.stop() -} - -// Unprotected func: ensure mutex on the parent watch set is locked before calling -func (w *watchNode) stop() { - if _, ok := w.s.nodes[w.id]; ok { - delete(w.s.nodes, w.id) - close(w.updateCh) - } -} - -func (w *watchNode) ResultChan() <-chan watch.Event { - return w.outCh -} - -func getResourceVersion(obj runtime.Object) (string, error) { - accessor, err := meta.Accessor(obj) - if err != nil { - klog.Error("Could not get accessor to object in event") - return "", err - } - return accessor.GetResourceVersion(), nil -} - -func (w *watchNode) getResourceVersionAsInt(obj runtime.Object) (uint64, error) { - accessor, err := meta.Accessor(obj) - if err != nil { - klog.Error("Could not get accessor to object in event") - return 0, err - } - - return w.versioner.ParseResourceVersion(accessor.GetResourceVersion()) -} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index a31ca6fe37b..3bf1db0529a 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" "sync" + "sync/atomic" "time" "go.opentelemetry.io/otel/trace" @@ -144,14 +145,15 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { var _ ResourceServer = &server{} type server struct { - tracer trace.Tracer - log *slog.Logger - backend StorageBackend - index ResourceIndexServer - diagnostics DiagnosticsServer - access WriteAccessHooks - lifecycle LifecycleHooks - now func() int64 + tracer trace.Tracer + log *slog.Logger + backend StorageBackend + index ResourceIndexServer + diagnostics DiagnosticsServer + access WriteAccessHooks + lifecycle LifecycleHooks + now func() int64 + mostRecentRV atomic.Int64 // The most recent resource version seen by the server // Background watch task -- this has permissions for everything ctx context.Context @@ -326,12 +328,12 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons rsp.Error = e return rsp, nil } - var err error rsp.ResourceVersion, err = s.backend.WriteEvent(ctx, *event) if err != nil { rsp.Error = AsErrorResult(err) } + s.log.Debug("server.WriteEvent", "type", event.Type, "rv", rsp.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "name", event.Key.Name, "resource", event.Key.Resource) return rsp, nil } @@ -537,6 +539,8 @@ func (s *server) initWatcher() error { for { // pipe all events v := <-events + s.log.Debug("Server. Streaming Event", "type", v.Type, "previousRV", v.PreviousRV, "group", v.Key.Group, "namespace", v.Key.Namespace, "resource", v.Key.Resource, "name", v.Key.Name) + s.mostRecentRV.Store(v.ResourceVersion) out <- v } }() @@ -552,23 +556,67 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { return err } - // Start listening -- this will buffer any changes that happen while we backfill + // Start listening -- this will buffer any changes that happen while we backfill. + // If events are generated faster than we can process them, then some events will be dropped. + // TODO: Think of a way to allow the client to catch up. stream, err := s.broadcaster.Subscribe(ctx) if err != nil { return err } defer s.broadcaster.Unsubscribe(stream) - since := req.Since - if req.SendInitialEvents { - fmt.Printf("TODO... query\n") - // All initial events are CREATE + if !req.SendInitialEvents && req.Since == 0 { + // This is a temporary hack only relevant for tests to ensure that the first events are sent. + // This is required because the SQL backend polls the database every 100ms. + // TODO: Implement a getLatestResourceVersion method in the backend. + time.Sleep(10 * time.Millisecond) + } - if req.AllowWatchBookmarks { - fmt.Printf("TODO... send bookmark\n") + mostRecentRV := s.mostRecentRV.Load() // get the latest resource version + var initialEventsRV int64 // resource version coming from the initial events + if req.SendInitialEvents { + // Backfill the stream by adding every existing entities. + initialEventsRV, err = s.backend.ListIterator(ctx, &ListRequest{Options: req.Options}, func(iter ListIterator) error { + for iter.Next() { + if err := iter.Error(); err != nil { + return err + } + if err := srv.Send(&WatchEvent{ + Type: WatchEvent_ADDED, + Resource: &WatchEvent_Resource{ + Value: iter.Value(), + Version: iter.ResourceVersion(), + }, + }); err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + } + if req.SendInitialEvents && req.AllowWatchBookmarks { + if err := srv.Send(&WatchEvent{ + Type: WatchEvent_BOOKMARK, + Resource: &WatchEvent_Resource{ + Version: initialEventsRV, + }, + }); err != nil { + return err } } + var since int64 // resource version to start watching from + switch { + case req.SendInitialEvents: + since = initialEventsRV + case req.Since == 0: + since = mostRecentRV + default: + since = req.Since + } for { select { case <-ctx.Done(): @@ -579,23 +627,39 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { s.log.Debug("watch events closed") return nil } - + s.log.Debug("Server Broadcasting", "type", event.Type, "rv", event.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "resource", event.Key.Resource, "name", event.Key.Name) if event.ResourceVersion > since && matchesQueryKey(req.Options.Key, event.Key) { - // Currently sending *every* event - // if req.Options.Labels != nil { - // // match *either* the old or new object - // } - // TODO: return values that match either the old or the new - - if err := srv.Send(&WatchEvent{ + value := event.Value + // remove the delete marker stored in the value for deleted objects + if event.Type == WatchEvent_DELETED { + value = []byte{} + } + resp := &WatchEvent{ Timestamp: event.Timestamp, Type: event.Type, Resource: &WatchEvent_Resource{ - Value: event.Value, + Value: value, Version: event.ResourceVersion, }, - // TODO... previous??? - }); err != nil { + } + if event.PreviousRV > 0 { + prevObj, err := s.Read(ctx, &ReadRequest{Key: event.Key, ResourceVersion: event.PreviousRV}) + if err != nil { + // This scenario should never happen, but if it does, we should log it and continue + // sending the event without the previous object. The client will decide what to do. + s.log.Error("error reading previous object", "key", event.Key, "resource_version", event.PreviousRV, "error", prevObj.Error) + } else { + if prevObj.ResourceVersion != event.PreviousRV { + s.log.Error("resource version mismatch", "key", event.Key, "resource_version", event.PreviousRV, "actual", prevObj.ResourceVersion) + return fmt.Errorf("resource version mismatch") + } + resp.Previous = &WatchEvent_Resource{ + Value: prevObj.Value, + Version: prevObj.ResourceVersion, + } + } + } + if err := srv.Send(resp); err != nil { return err } } diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index ae70e141219..913d105babb 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -22,6 +22,7 @@ import ( ) const trace_prefix = "sql.resource." +const defaultPollingInterval = 100 * time.Millisecond type Backend interface { resource.StorageBackend @@ -30,8 +31,9 @@ type Backend interface { } type BackendOptions struct { - DBProvider db.DBProvider - Tracer trace.Tracer + DBProvider db.DBProvider + Tracer trace.Tracer + PollingInterval time.Duration } func NewBackend(opts BackendOptions) (Backend, error) { @@ -43,12 +45,17 @@ func NewBackend(opts BackendOptions) (Backend, error) { } ctx, cancel := context.WithCancel(context.Background()) + pollingInterval := opts.PollingInterval + if pollingInterval == 0 { + pollingInterval = defaultPollingInterval + } return &backend{ - done: ctx.Done(), - cancel: cancel, - log: log.New("sql-resource-server"), - tracer: opts.Tracer, - dbProvider: opts.DBProvider, + done: ctx.Done(), + cancel: cancel, + log: log.New("sql-resource-server"), + tracer: opts.Tracer, + dbProvider: opts.DBProvider, + pollingInterval: pollingInterval, }, nil } @@ -70,6 +77,7 @@ type backend struct { // watch streaming //stream chan *resource.WatchEvent + pollingInterval time.Duration } func (b *backend) Init(ctx context.Context) error { @@ -180,7 +188,6 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, return nil }) - return newVersion, err } @@ -512,8 +519,7 @@ func (b *backend) WatchWriteEvents(ctx context.Context) (<-chan *resource.Writte } func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan<- *resource.WrittenEvent) { - interval := 100 * time.Millisecond // TODO make this configurable - t := time.NewTicker(interval) + t := time.NewTicker(b.pollingInterval) defer close(stream) defer t.Stop() @@ -526,7 +532,7 @@ func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan grv, err := b.listLatestRVs(ctx) if err != nil { b.log.Error("get the latest resource version", "err", err) - t.Reset(interval) + t.Reset(b.pollingInterval) continue } for group, items := range grv { @@ -543,7 +549,7 @@ func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan next, err := b.poll(ctx, group, resource, since[group][resource], stream) if err != nil { b.log.Error("polling for resource", "err", err) - t.Reset(interval) + t.Reset(b.pollingInterval) continue } if next > since[group][resource] { @@ -552,7 +558,7 @@ func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan } } - t.Reset(interval) + t.Reset(b.pollingInterval) } } } @@ -636,7 +642,8 @@ func (b *backend) poll(ctx context.Context, grp string, res string, since int64, Resource: rec.Key.Resource, Name: rec.Key.Name, }, - Type: resource.WatchEvent_Type(rec.Action), + Type: resource.WatchEvent_Type(rec.Action), + PreviousRV: rec.PreviousRV, }, ResourceVersion: rec.ResourceVersion, // Timestamp: , // TODO: add timestamp @@ -663,15 +670,16 @@ func resourceVersionAtomicInc(ctx context.Context, x db.ContextExecer, d sqltemp if errors.Is(err, sql.ErrNoRows) { // if there wasn't a row associated with the given resource, we create one with - // version 1 + // version 2 to match the etcd behavior. if _, err = dbutil.Exec(ctx, x, sqlResourceVersionInsert, sqlResourceVersionRequest{ - SQLTemplate: sqltemplate.New(d), - Group: key.Group, - Resource: key.Resource, + SQLTemplate: sqltemplate.New(d), + Group: key.Group, + Resource: key.Resource, + resourceVersion: &resourceVersion{1}, }); err != nil { return 0, fmt.Errorf("insert into resource_version: %w", err) } - return 1, nil + return 2, nil } if err != nil { diff --git a/pkg/storage/unified/sql/backend_test.go b/pkg/storage/unified/sql/backend_test.go index b24024aef90..33b7bab7d6a 100644 --- a/pkg/storage/unified/sql/backend_test.go +++ b/pkg/storage/unified/sql/backend_test.go @@ -227,7 +227,7 @@ func TestResourceVersionAtomicInc(t *testing.T) { v, err := resourceVersionAtomicInc(ctx, b.DB, dialect, resKey) require.NoError(t, err) - require.Equal(t, int64(1), v) + require.Equal(t, int64(2), v) }) t.Run("happy path - update existing row", func(t *testing.T) { @@ -304,7 +304,7 @@ func TestBackend_create(t *testing.T) { v, err := b.create(ctx, event) require.NoError(t, err) - require.Equal(t, int64(1), v) + require.Equal(t, int64(2), v) }) t.Run("error inserting into resource", func(t *testing.T) { @@ -409,7 +409,7 @@ func TestBackend_update(t *testing.T) { v, err := b.update(ctx, event) require.NoError(t, err) - require.Equal(t, int64(1), v) + require.Equal(t, int64(2), v) }) t.Run("error in first update to resource", func(t *testing.T) { @@ -513,7 +513,7 @@ func TestBackend_delete(t *testing.T) { v, err := b.delete(ctx, event) require.NoError(t, err) - require.Equal(t, int64(1), v) + require.Equal(t, int64(2), v) }) t.Run("error deleting resource", func(t *testing.T) { diff --git a/pkg/storage/unified/sql/data/resource_history_insert.sql b/pkg/storage/unified/sql/data/resource_history_insert.sql index 018b65739d8..2669ef82447 100644 --- a/pkg/storage/unified/sql/data/resource_history_insert.sql +++ b/pkg/storage/unified/sql/data/resource_history_insert.sql @@ -6,6 +6,7 @@ INSERT INTO {{ .Ident "resource_history" }} {{ .Ident "namespace" }}, {{ .Ident "name" }}, + {{ .Ident "previous_resource_version"}}, {{ .Ident "value" }}, {{ .Ident "action" }} ) @@ -17,6 +18,7 @@ INSERT INTO {{ .Ident "resource_history" }} {{ .Arg .WriteEvent.Key.Namespace }}, {{ .Arg .WriteEvent.Key.Name }}, + {{ .Arg .WriteEvent.PreviousRV }}, {{ .Arg .WriteEvent.Value }}, {{ .Arg .WriteEvent.Type }} ) diff --git a/pkg/storage/unified/sql/data/resource_history_poll.sql b/pkg/storage/unified/sql/data/resource_history_poll.sql index bebfab9286d..8e4a7374fdb 100644 --- a/pkg/storage/unified/sql/data/resource_history_poll.sql +++ b/pkg/storage/unified/sql/data/resource_history_poll.sql @@ -5,7 +5,8 @@ SELECT {{ .Ident "resource" | .Into .Response.Key.Resource }}, {{ .Ident "name" | .Into .Response.Key.Name }}, {{ .Ident "value" | .Into .Response.Value }}, - {{ .Ident "action" | .Into .Response.Action }} + {{ .Ident "action" | .Into .Response.Action }}, + {{ .Ident "previous_resource_version" | .Into .Response.PreviousRV }} FROM {{ .Ident "resource_history" }} WHERE 1 = 1 diff --git a/pkg/storage/unified/sql/data/resource_insert.sql b/pkg/storage/unified/sql/data/resource_insert.sql index e127901ae50..ccaca2f12f7 100644 --- a/pkg/storage/unified/sql/data/resource_insert.sql +++ b/pkg/storage/unified/sql/data/resource_insert.sql @@ -7,6 +7,7 @@ INSERT INTO {{ .Ident "resource" }} {{ .Ident "namespace" }}, {{ .Ident "name" }}, + {{ .Ident "previous_resource_version" }}, {{ .Ident "value" }}, {{ .Ident "action" }} ) @@ -17,6 +18,7 @@ INSERT INTO {{ .Ident "resource" }} {{ .Arg .WriteEvent.Key.Namespace }}, {{ .Arg .WriteEvent.Key.Name }}, + {{ .Arg .WriteEvent.PreviousRV }}, {{ .Arg .WriteEvent.Value }}, {{ .Arg .WriteEvent.Type }} ) diff --git a/pkg/storage/unified/sql/data/resource_version_insert.sql b/pkg/storage/unified/sql/data/resource_version_insert.sql index 6c2342905da..6c3aab0dcd4 100644 --- a/pkg/storage/unified/sql/data/resource_version_insert.sql +++ b/pkg/storage/unified/sql/data/resource_version_insert.sql @@ -8,6 +8,6 @@ INSERT INTO {{ .Ident "resource_version" }} VALUES ( {{ .Arg .Group }}, {{ .Arg .Resource }}, - 1 + 2 ) ; diff --git a/pkg/storage/unified/sql/db/migrations/resource_mig.go b/pkg/storage/unified/sql/db/migrations/resource_mig.go index adfd75a0b73..38824569a05 100644 --- a/pkg/storage/unified/sql/db/migrations/resource_mig.go +++ b/pkg/storage/unified/sql/db/migrations/resource_mig.go @@ -10,8 +10,7 @@ func initResourceTables(mg *migrator.Migrator) string { marker := "Initialize resource tables" mg.AddMigration(marker, &migrator.RawSQLMigration{}) - tables := []migrator.Table{} - tables = append(tables, migrator.Table{ + resource_table := migrator.Table{ Name: "resource", Columns: []*migrator.Column{ // primary identifier @@ -33,9 +32,8 @@ func initResourceTables(mg *migrator.Migrator) string { Indices: []*migrator.Index{ {Cols: []string{"namespace", "group", "resource", "name"}, Type: migrator.UniqueIndex}, }, - }) - - tables = append(tables, migrator.Table{ + } + resource_history_table := migrator.Table{ Name: "resource_history", Columns: []*migrator.Column{ // primary identifier @@ -62,7 +60,9 @@ func initResourceTables(mg *migrator.Migrator) string { // index to support watch poller {Cols: []string{"resource_version"}, Type: migrator.IndexType}, }, - }) + } + + tables := []migrator.Table{resource_table, resource_history_table} // tables = append(tables, migrator.Table{ // Name: "resource_label_set", @@ -97,5 +97,13 @@ func initResourceTables(mg *migrator.Migrator) string { } } + mg.AddMigration("Add column previous_resource_version in resource_history", migrator.NewAddColumnMigration(resource_history_table, &migrator.Column{ + Name: "previous_resource_version", Type: migrator.DB_BigInt, Nullable: false, + })) + + mg.AddMigration("Add column previous_resource_version in resource", migrator.NewAddColumnMigration(resource_table, &migrator.Column{ + Name: "previous_resource_version", Type: migrator.DB_BigInt, Nullable: false, + })) + return marker } diff --git a/pkg/storage/unified/sql/queries.go b/pkg/storage/unified/sql/queries.go index 893169c3f3a..11882f17cb2 100644 --- a/pkg/storage/unified/sql/queries.go +++ b/pkg/storage/unified/sql/queries.go @@ -70,6 +70,7 @@ func (r sqlResourceRequest) Validate() error { type historyPollResponse struct { Key resource.ResourceKey ResourceVersion int64 + PreviousRV int64 Value []byte Action int } @@ -101,6 +102,7 @@ func (r *sqlResourceHistoryPollRequest) Results() (*historyPollResponse, error) Name: r.Response.Key.Name, }, ResourceVersion: r.Response.ResourceVersion, + PreviousRV: r.Response.PreviousRV, Value: r.Response.Value, Action: r.Response.Action, }, nil diff --git a/pkg/storage/unified/sql/queries_test.go b/pkg/storage/unified/sql/queries_test.go index b5ac7f57217..df7ed9167f7 100644 --- a/pkg/storage/unified/sql/queries_test.go +++ b/pkg/storage/unified/sql/queries_test.go @@ -104,6 +104,18 @@ func TestUnifiedStorageQueries(t *testing.T) { }, }, }, + sqlResourceHistoryPoll: { + { + Name: "single path", + Data: &sqlResourceHistoryPollRequest{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Resource: "res", + Group: "group", + SinceResourceVersion: 1234, + Response: new(historyPollResponse), + }, + }, + }, sqlResourceUpdateRV: { { @@ -143,7 +155,8 @@ func TestUnifiedStorageQueries(t *testing.T) { Data: &sqlResourceRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), WriteEvent: resource.WriteEvent{ - Key: &resource.ResourceKey{}, + Key: &resource.ResourceKey{}, + PreviousRV: 1234, }, }, }, diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql index 27f5000fc9f..d76132ae625 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql @@ -5,6 +5,7 @@ INSERT INTO `resource_history` `resource`, `namespace`, `name`, + `previous_resource_version`, `value`, `action` ) @@ -14,6 +15,7 @@ INSERT INTO `resource_history` '', '', '', + 1234, '[]', 'UNKNOWN' ) diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_poll-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_poll-single path.sql new file mode 100755 index 00000000000..a29cf35d4da --- /dev/null +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_poll-single path.sql @@ -0,0 +1,16 @@ +SELECT + `resource_version`, + `namespace`, + `group`, + `resource`, + `name`, + `value`, + `action`, + `previous_resource_version` + FROM `resource_history` + WHERE 1 = 1 + AND `group` = 'group' + AND `resource` = 'res' + AND `resource_version` > 1234 + ORDER BY `resource_version` ASC +; diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_insert-simple.sql b/pkg/storage/unified/sql/testdata/mysql--resource_insert-simple.sql index 0897963b19c..5bf3424e55b 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_insert-simple.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_insert-simple.sql @@ -5,6 +5,7 @@ INSERT INTO `resource` `resource`, `namespace`, `name`, + `previous_resource_version`, `value`, `action` ) @@ -14,6 +15,7 @@ INSERT INTO `resource` 'rr', 'nn', 'name', + 123, '[]', 'ADDED' ) diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql index 350f77472ab..f99b2b00148 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO `resource_version` VALUES ( '', '', - 1 + 2 ) ; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql index 643741bc3b1..a15a8db4b1e 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql @@ -5,6 +5,7 @@ INSERT INTO "resource_history" "resource", "namespace", "name", + "previous_resource_version", "value", "action" ) @@ -14,6 +15,7 @@ INSERT INTO "resource_history" '', '', '', + 1234, '[]', 'UNKNOWN' ) diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_poll-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_poll-single path.sql new file mode 100755 index 00000000000..d038317381a --- /dev/null +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_poll-single path.sql @@ -0,0 +1,16 @@ +SELECT + "resource_version", + "namespace", + "group", + "resource", + "name", + "value", + "action", + "previous_resource_version" + FROM "resource_history" + WHERE 1 = 1 + AND "group" = 'group' + AND "resource" = 'res' + AND "resource_version" > 1234 + ORDER BY "resource_version" ASC +; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_insert-simple.sql b/pkg/storage/unified/sql/testdata/postgres--resource_insert-simple.sql index 9150eb59fef..fc2d22be1c4 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_insert-simple.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_insert-simple.sql @@ -5,6 +5,7 @@ INSERT INTO "resource" "resource", "namespace", "name", + "previous_resource_version", "value", "action" ) @@ -14,6 +15,7 @@ INSERT INTO "resource" 'rr', 'nn', 'name', + 123, '[]', 'ADDED' ) diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql index 99003d5fefe..14b25955585 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO "resource_version" VALUES ( '', '', - 1 + 2 ) ; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql index 643741bc3b1..a15a8db4b1e 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql @@ -5,6 +5,7 @@ INSERT INTO "resource_history" "resource", "namespace", "name", + "previous_resource_version", "value", "action" ) @@ -14,6 +15,7 @@ INSERT INTO "resource_history" '', '', '', + 1234, '[]', 'UNKNOWN' ) diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_poll-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_poll-single path.sql new file mode 100755 index 00000000000..d038317381a --- /dev/null +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_poll-single path.sql @@ -0,0 +1,16 @@ +SELECT + "resource_version", + "namespace", + "group", + "resource", + "name", + "value", + "action", + "previous_resource_version" + FROM "resource_history" + WHERE 1 = 1 + AND "group" = 'group' + AND "resource" = 'res' + AND "resource_version" > 1234 + ORDER BY "resource_version" ASC +; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_insert-simple.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_insert-simple.sql index 9150eb59fef..fc2d22be1c4 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_insert-simple.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_insert-simple.sql @@ -5,6 +5,7 @@ INSERT INTO "resource" "resource", "namespace", "name", + "previous_resource_version", "value", "action" ) @@ -14,6 +15,7 @@ INSERT INTO "resource" 'rr', 'nn', 'name', + 123, '[]', 'ADDED' ) diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql index 99003d5fefe..14b25955585 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO "resource_version" VALUES ( '', '', - 1 + 2 ) ; From ddbf0a05afe1c0904ea8af3f9018232920a24c67 Mon Sep 17 00:00:00 2001 From: Kristina Date: Mon, 30 Sep 2024 07:43:45 -0500 Subject: [PATCH 076/174] Correlations: Update docs to include information on external correlation type (#93772) Change docs to reflect different correlation types --- docs/sources/administration/correlations/_index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sources/administration/correlations/_index.md b/docs/sources/administration/correlations/_index.md index 977849d2948..3a61b1800b5 100644 --- a/docs/sources/administration/correlations/_index.md +++ b/docs/sources/administration/correlations/_index.md @@ -12,16 +12,18 @@ weight: 900 # Correlations -You can create interactive links for Explore visualizations to run queries related to presented data by setting up Correlations. +You can create interactive links for Explore visualizations by setting up Correlations. These links can either run queries or generate external URLs related to presented data. -A correlation defines how data in one [data source]({{< relref "../../datasources" >}}) is used to query data in another data source. +A correlation defines how data in one [data source]({{< relref "../../datasources" >}}) is used to query data in another data source or to generate an external URL. Some examples: - an application name returned in a logs data source can be used to query metrics related to that application in a metrics data source, or - a user name returned by an SQL data source can be used to query logs related to that particular user in a logs data source +- a customer ID in a logs data source can link to a different platform that has a profile on that customer. [Explore]({{< relref "../../explore" >}}) takes user-defined correlations to display links inside the visualizations. -You can click on a link to run the related query and see results in [Explore Split View]({{< relref "../../explore#split-and-compare" >}}). +If a correlation links to a query, you can click on that link to run the related query and see results in [Explore Split View]({{< relref "../../explore#split-and-compare" >}}). +If a correlation links to an external URL, you can click on the link to open the URL in a new tab in your browser. Explore visualizations that currently support showing links based on correlations: From 6f92fd64ce1c4402cbc03dfe6553d8cb3426e9f8 Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 30 Sep 2024 09:57:25 -0300 Subject: [PATCH 077/174] Cloud migrations: add more context to errors (#93814) * Cloud migrations: add more context to errors * calls to assert.ErrorIs was passing arguments in the wrong order --- .../cloudmigration/cloudmigrationimpl/cloudmigration.go | 5 +++-- .../cloudmigrationimpl/cloudmigration_test.go | 2 +- .../cloudmigration/cloudmigrationimpl/xorm_store.go | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index f78a32dad8c..9e2433d79ce 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -179,7 +179,8 @@ func (s *Service) GetToken(ctx context.Context) (gcom.TokenView, error) { } logger.Info("cloud migration token not found") - return gcom.TokenView{}, cloudmigration.ErrTokenNotFound + return gcom.TokenView{}, fmt.Errorf("fetching cloud migration token: instance=%+v accessPolicyName=%s accessTokenName=%s %w", + instance, accessPolicyName, accessTokenName, cloudmigration.ErrTokenNotFound) } func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessTokenResponse, error) { @@ -300,7 +301,7 @@ func (s *Service) ValidateToken(ctx context.Context, cm cloudmigration.CloudMigr defer span.End() if err := s.gmsClient.ValidateKey(ctx, cm); err != nil { - return fmt.Errorf("validating key: %w", err) + return fmt.Errorf("validating token: %w", err) } return nil diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 95e8a28fbad..ea4d905e97f 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -58,7 +58,7 @@ func Test_CreateGetAndDeleteToken(t *testing.T) { assert.NoError(t, err) _, err = s.GetToken(context.Background()) - assert.ErrorIs(t, cloudmigration.ErrTokenNotFound, err) + assert.ErrorIs(t, err, cloudmigration.ErrTokenNotFound) cm := cloudmigration.CloudMigrationSession{} err = s.ValidateToken(context.Background(), cm) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index 33e3b392287..22de2474310 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -432,16 +432,16 @@ func (ss *sqlStore) encryptToken(ctx context.Context, cm *cloudmigration.CloudMi func (ss *sqlStore) decryptToken(ctx context.Context, cm *cloudmigration.CloudMigrationSession) error { if cm == nil { - return cloudmigration.ErrMigrationNotFound + return fmt.Errorf("unable to decypt token because migration session was not found: %w", cloudmigration.ErrMigrationNotFound) } if len(cm.AuthToken) == 0 { - return cloudmigration.ErrTokenNotFound + return fmt.Errorf("unable to decrypt token because token is empty: %w", cloudmigration.ErrTokenNotFound) } decoded, err := base64.StdEncoding.DecodeString(cm.AuthToken) if err != nil { - return fmt.Errorf("token could not be decoded") + return fmt.Errorf("unable to base64 decode token: %w", err) } t, err := ss.secretsService.Decrypt(ctx, decoded) From a45662bf2d05c71d75fe48e646930a2f30e1296d Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:26:16 +0100 Subject: [PATCH 078/174] Revert "Restore Dashboards: Add e2e tests (again) (#93214)" (#94003) --- e2e/dashboards-suite/dashboard-browse.spec.ts | 8 - .../dashboard-restore.spec.ts | 82 ------- e2e/dashboards/TestRestoreDashboard.json | 209 ------------------ e2e/utils/flows/deleteDashboard.ts | 8 - .../src/selectors/pages.ts | 8 - .../dashboard/components/DashNav/DashNav.tsx | 1 - .../page/components/SearchResultsTable.tsx | 17 +- scripts/grafana-server/custom.ini | 2 +- 8 files changed, 4 insertions(+), 331 deletions(-) delete mode 100644 e2e/dashboards-suite/dashboard-restore.spec.ts delete mode 100644 e2e/dashboards/TestRestoreDashboard.json diff --git a/e2e/dashboards-suite/dashboard-browse.spec.ts b/e2e/dashboards-suite/dashboard-browse.spec.ts index 3bd04f4e338..a5dc3cfc5ca 100644 --- a/e2e/dashboards-suite/dashboard-browse.spec.ts +++ b/e2e/dashboards-suite/dashboard-browse.spec.ts @@ -49,12 +49,4 @@ describe.skip('Dashboard browse', () => { e2e.flows.confirmDelete(); e2e.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard').should('not.exist'); }); - - afterEach(() => { - // Permanently delete dashboard - e2e.pages.RecentlyDeleted.visit(); - e2e.pages.Search.table.row('E2E Test - Import Dashboard').find('[type="checkbox"]').click({ force: true }); - cy.contains('button', 'Delete permanently').click(); - e2e.flows.confirmDelete(); - }); }); diff --git a/e2e/dashboards-suite/dashboard-restore.spec.ts b/e2e/dashboards-suite/dashboard-restore.spec.ts deleted file mode 100644 index 270b9794e31..00000000000 --- a/e2e/dashboards-suite/dashboard-restore.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import testDashboard from '../dashboards/TestRestoreDashboard.json'; -import { e2e } from '../utils'; - -describe('Dashboard restore', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Should delete, restore and permanently delete from the Dashboards page', () => { - e2e.flows.importDashboard(testDashboard, 1000, true); - - e2e.pages.Dashboards.visit(); - - // Delete dashboard - e2e.pages.BrowseDashboards.table - .row('E2E Test - Restore Dashboard') - .find('[type="checkbox"]') - .click({ force: true }); - deleteDashboard('Delete'); - - // Dashboard should appear in Recently Deleted - e2e.pages.RecentlyDeleted.visit(); - e2e.pages.Search.table.row('E2E Test - Restore Dashboard').should('exist'); - - // Restore dashboard - e2e.pages.Search.table.row('E2E Test - Restore Dashboard').find('[type="checkbox"]').click({ force: true }); - cy.contains('button', 'Restore').click(); - cy.contains('p', 'This action will restore 1 dashboard.').should('be.visible'); - e2e.pages.ConfirmModal.delete().click(); - e2e.components.Alert.alertV2('success').contains('Dashboard E2E Test - Restore Dashboard restored').should('exist'); - - // Dashboard should appear in Browse - e2e.pages.Dashboards.visit(); - e2e.pages.BrowseDashboards.table.row('E2E Test - Restore Dashboard').should('exist'); - - // Delete dashboard - e2e.pages.BrowseDashboards.table - .row('E2E Test - Restore Dashboard') - .find('[type="checkbox"]') - .click({ force: true }); - deleteDashboard('Delete'); - - // Permanently delete dashboard - permanentlyDeleteDashboard(); - }); - - it('Should delete, restore and permanently delete from the Dashboard settings', () => { - e2e.flows.importDashboard(testDashboard, 1000, true); - - e2e.flows.openDashboard({ uid: '355ac6c2-8a12-4469-8b99-4750eb8d0966' }); - e2e.pages.Dashboard.DashNav.settingsButton().click(); - deleteDashboard('Delete dashboard'); - - // Permanently delete dashboard - permanentlyDeleteDashboard(); - }); -}); - -const deleteDashboard = (buttonName: string) => { - cy.contains('button', buttonName).click(); - e2e.flows.confirmDelete(); - e2e.components.Alert.alertV2('success') - .contains('Dashboard E2E Test - Restore Dashboard moved to Recently deleted') - .should('exist'); - e2e.pages.BrowseDashboards.table.row('E2E Test - Restore Dashboard').should('not.exist'); -}; - -const permanentlyDeleteDashboard = () => { - // Permanently delete dashboard - e2e.pages.RecentlyDeleted.visit(); - e2e.pages.Search.table.row('E2E Test - Restore Dashboard').find('[type="checkbox"]').click({ force: true }); - cy.contains('button', 'Delete permanently').click(); - cy.contains('p', 'This action will delete 1 dashboard.').should('be.visible'); - e2e.flows.confirmDelete(); - e2e.components.Alert.alertV2('success').contains('Dashboard E2E Test - Restore Dashboard deleted').should('exist'); - - // Dashboard should not appear in Recently Deleted or Browse - e2e.pages.Search.table.row('E2E Test - Restore Dashboard').should('not.exist'); - - e2e.pages.Dashboards.visit(); - e2e.pages.BrowseDashboards.table.row('E2E Test - Restore Dashboard').should('not.exist'); -}; diff --git a/e2e/dashboards/TestRestoreDashboard.json b/e2e/dashboards/TestRestoreDashboard.json deleted file mode 100644 index b34ff4c0c05..00000000000 --- a/e2e/dashboards/TestRestoreDashboard.json +++ /dev/null @@ -1,209 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "target": { - "limit": 100, - "matchAny": false, - "tags": [], - "type": "dashboard" - }, - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": 322, - "links": [], - "liveNow": false, - "panels": [ - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 6, - "options": { - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "text": {} - }, - "pluginVersion": "8.3.0-pre", - "title": "Gauge Example", - "type": "gauge" - }, - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 8 - }, - "id": 4, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "8.3.0-pre", - "title": "Stat", - "type": "stat" - }, - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 16 - }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "title": "Time series example", - "type": "timeseries" - } - ], - "refresh": false, - "schemaVersion": 31, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "2021-09-01T04:00:00.000Z", - "to": "2021-09-15T04:00:00.000Z" - }, - "timepicker": {}, - "timezone": "", - "title": "E2E Test - Restore Dashboard", - "uid": "355ac6c2-8a12-4469-8b99-4750eb8d0966", - "version": 4 -} diff --git a/e2e/utils/flows/deleteDashboard.ts b/e2e/utils/flows/deleteDashboard.ts index 7eb294f6758..de492012c2f 100644 --- a/e2e/utils/flows/deleteDashboard.ts +++ b/e2e/utils/flows/deleteDashboard.ts @@ -29,14 +29,6 @@ export const deleteDashboard = ({ quick = false, title, uid }: DeleteDashboardCo const quickDelete = (uid: string) => { cy.request('DELETE', fromBaseUrl(`/api/dashboards/uid/${uid}`)); - cy.window().then((win: Cypress.AUTWindow) => { - if ( - win.grafanaBootData.settings.featureToggles.dashboardRestore && - win.grafanaBootData.settings.featureToggles.dashboardRestoreUI - ) { - cy.request('DELETE', fromBaseUrl(`/api/dashboards/uid/${uid}/trash`)); - } - }); }; const uiDelete = (uid: string, title: string) => { diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 2b835699614..be04b0e8296 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -57,7 +57,6 @@ export const Pages = { navV2: 'data-testid Dashboard navigation', publicDashboardTag: 'data-testid public dashboard tag', shareButton: 'data-testid share-button', - settingsButton: 'data-testid settings-button', scrollContainer: 'data-testid Dashboard canvas scroll container', newShareButton: { container: 'data-testid new share button', @@ -240,9 +239,6 @@ export const Pages = { */ dashboards: (title: string) => `Dashboard search item ${title}`, }, - RecentlyDeleted: { - url: '/dashboard/recently-deleted', - }, SaveDashboardAsModal: { newName: 'Save dashboard title field', save: 'Save dashboard button', @@ -407,10 +403,6 @@ export const Pages = { FolderView: { url: '/?search=open&layout=folders', }, - table: { - body: 'data-testid search-table', - row: (name: string) => `data-testid search row ${name}`, - }, }, PublicDashboards: { ListItem: { diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index b0bb9796b16..86a3986be31 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -326,7 +326,6 @@ export const DashNav = memo((props) => { diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 471bdeb0108..20ae3c292ff 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -7,7 +7,6 @@ import InfiniteLoader from 'react-window-infinite-loader'; import { Observable } from 'rxjs'; import { Field, GrafanaTheme2 } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; import { TableCellHeight } from '@grafana/schema'; import { useStyles2, useTheme2 } from '@grafana/ui'; import { TableCell } from '@grafana/ui/src/components/Table/TableCell'; @@ -138,13 +137,9 @@ export const SearchResultsTable = React.memo( className += ' ' + styles.selectedRow; } const { key, ...rowProps } = row.getRowProps({ style }); + return ( -
+
{row.cells.map((cell: Cell, index: number) => { return ( +
{headerGroups.map((headerGroup) => { const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({ style: { width }, diff --git a/scripts/grafana-server/custom.ini b/scripts/grafana-server/custom.ini index d1751079a18..5d09d40ac63 100644 --- a/scripts/grafana-server/custom.ini +++ b/scripts/grafana-server/custom.ini @@ -4,7 +4,7 @@ content_security_policy = true content_security_policy_template = """require-trusted-types-for 'script'; script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';""" [feature_toggles] -enable = publicDashboards, dashboardRestore, dashboardRestoreUI +enable = publicDashboards [plugins] allow_loading_unsigned_plugins=grafana-extensionstest-app,grafana-extensionexample1-app,grafana-extensionexample2-app, From ae05e4422dea57cec6988a8d18b7f2e1e04d10e7 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:00:20 +0100 Subject: [PATCH 079/174] I18n: Download translations from Crowdin (#93983) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 4 ++++ public/locales/es-ES/grafana.json | 4 ++++ public/locales/fr-FR/grafana.json | 4 ++++ public/locales/pt-BR/grafana.json | 4 ++++ public/locales/zh-Hans/grafana.json | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 14d1b38c250..20f666b7c50 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -56,6 +56,10 @@ "pause": "" }, "alerting": { + "annotations": { + "description": "", + "title": "" + }, "central-alert-history": { "details": { "error": "", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 16222b0ec75..3bbdc263417 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -56,6 +56,10 @@ "pause": "" }, "alerting": { + "annotations": { + "description": "", + "title": "" + }, "central-alert-history": { "details": { "error": "", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index fe362f36f1b..e7e2be6f6bd 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -56,6 +56,10 @@ "pause": "" }, "alerting": { + "annotations": { + "description": "", + "title": "" + }, "central-alert-history": { "details": { "error": "", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 9deb7dbb811..791b5b97fa9 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -56,6 +56,10 @@ "pause": "" }, "alerting": { + "annotations": { + "description": "", + "title": "" + }, "central-alert-history": { "details": { "error": "", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index e248c210cea..ea860460b48 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -56,6 +56,10 @@ "pause": "" }, "alerting": { + "annotations": { + "description": "", + "title": "" + }, "central-alert-history": { "details": { "error": "", From b937b70a46e75ddd2fcd8fd02db576007bc70d41 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:05:16 +0100 Subject: [PATCH 080/174] Internationalization: Restore some plurals text (#94002) --- public/locales/en-US/grafana.json | 22 +++++++++++----------- public/locales/pseudo-LOCALE/grafana.json | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 85cfb57b5c4..7e370221b5d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -134,7 +134,7 @@ "parse-mode-warning-title": "Telegram messages are limited to 4096 UTF-8 characters." }, "used-by_one": "Used by {{ count }} notification policy", - "used-by_other": "Used by {{ count }} notification policy", + "used-by_other": "Used by {{ count }} notification policies", "used-by-rules_one": "Used by {{ count }} alert rule", "used-by-rules_other": "Used by {{ count }} alert rules" }, @@ -190,7 +190,7 @@ "inherited": "Inherited", "mute-time": "Muted when", "n-instances_one": "instance", - "n-instances_other": "instance", + "n-instances_other": "instances", "timingOptions": { "groupInterval": { "description": "How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent.", @@ -206,7 +206,7 @@ } } }, - "n-more-policies_one": "{{count}} additional policies", + "n-more-policies_one": "{{count}} additional policy", "n-more-policies_other": "{{count}} additional policies", "new-child": "New child policy", "new-policy": "Add new policy", @@ -324,15 +324,15 @@ }, "counts": { "alertRule_one": "{{count}} alert rule", - "alertRule_other": "{{count}} alert rule", + "alertRule_other": "{{count}} alert rules", "dashboard_one": "{{count}} dashboard", - "dashboard_other": "{{count}} dashboard", + "dashboard_other": "{{count}} dashboards", "folder_one": "{{count}} folder", - "folder_other": "{{count}} folder", + "folder_other": "{{count}} folders", "libraryPanel_one": "{{count}} library panel", - "libraryPanel_other": "{{count}} library panel", + "libraryPanel_other": "{{count}} library panels", "total_one": "{{count}} item", - "total_other": "{{count}} item" + "total_other": "{{count}} items" }, "dashboards-tree": { "collapse-folder-button": "Collapse folder {{title}}", @@ -2218,16 +2218,16 @@ "confirm-text": "Delete", "delete-button": "Delete", "delete-loading": "Deleting...", - "text_one": "This action will delete {{numberOfDashboards}} dashboards.", + "text_one": "This action will delete {{numberOfDashboards}} dashboard.", "text_other": "This action will delete {{numberOfDashboards}} dashboards.", "title": "Permanently Delete Dashboards" }, "restore-modal": { - "folder-picker-text_one": "Please choose a folder where your dashboards will be restored.", + "folder-picker-text_one": "Please choose a folder where your dashboard will be restored.", "folder-picker-text_other": "Please choose a folder where your dashboards will be restored.", "restore-button": "Restore", "restore-loading": "Restoring...", - "text_one": "This action will restore {{numberOfDashboards}} dashboards.", + "text_one": "This action will restore {{numberOfDashboards}} dashboard.", "text_other": "This action will restore {{numberOfDashboards}} dashboards.", "title": "Restore Dashboards" } diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index adb604efb29..dad1f0097d0 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -134,7 +134,7 @@ "parse-mode-warning-title": "Ŧęľęģřäm męşşäģęş äřę ľįmįŧęđ ŧő 4096 ŮŦF-8 čĥäřäčŧęřş." }, "used-by_one": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčy", - "used-by_other": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčy", + "used-by_other": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčįęş", "used-by-rules_one": "Ůşęđ þy {{ count }} äľęřŧ řūľę", "used-by-rules_other": "Ůşęđ þy {{ count }} äľęřŧ řūľęş" }, @@ -190,7 +190,7 @@ "inherited": "Ĩʼnĥęřįŧęđ", "mute-time": "Mūŧęđ ŵĥęʼn", "n-instances_one": "įʼnşŧäʼnčę", - "n-instances_other": "įʼnşŧäʼnčę", + "n-instances_other": "įʼnşŧäʼnčęş", "timingOptions": { "groupInterval": { "description": "Ħőŵ ľőʼnģ ŧő ŵäįŧ þęƒőřę şęʼnđįʼnģ ä ʼnőŧįƒįčäŧįőʼn äþőūŧ ʼnęŵ äľęřŧş ŧĥäŧ äřę äđđęđ ŧő ä ģřőūp őƒ äľęřŧş ƒőř ŵĥįčĥ äʼn įʼnįŧįäľ ʼnőŧįƒįčäŧįőʼn ĥäş äľřęäđy þęęʼn şęʼnŧ.", @@ -206,7 +206,7 @@ } } }, - "n-more-policies_one": "{{count}} äđđįŧįőʼnäľ pőľįčįęş", + "n-more-policies_one": "{{count}} äđđįŧįőʼnäľ pőľįčy", "n-more-policies_other": "{{count}} äđđįŧįőʼnäľ pőľįčįęş", "new-child": "Ńęŵ čĥįľđ pőľįčy", "new-policy": "Åđđ ʼnęŵ pőľįčy", @@ -324,15 +324,15 @@ }, "counts": { "alertRule_one": "{{count}} äľęřŧ řūľę", - "alertRule_other": "{{count}} äľęřŧ řūľę", + "alertRule_other": "{{count}} äľęřŧ řūľęş", "dashboard_one": "{{count}} đäşĥþőäřđ", - "dashboard_other": "{{count}} đäşĥþőäřđ", + "dashboard_other": "{{count}} đäşĥþőäřđş", "folder_one": "{{count}} ƒőľđęř", - "folder_other": "{{count}} ƒőľđęř", + "folder_other": "{{count}} ƒőľđęřş", "libraryPanel_one": "{{count}} ľįþřäřy päʼnęľ", - "libraryPanel_other": "{{count}} ľįþřäřy päʼnęľ", + "libraryPanel_other": "{{count}} ľįþřäřy päʼnęľş", "total_one": "{{count}} įŧęm", - "total_other": "{{count}} įŧęm" + "total_other": "{{count}} įŧęmş" }, "dashboards-tree": { "collapse-folder-button": "Cőľľäpşę ƒőľđęř {{title}}", @@ -2218,16 +2218,16 @@ "confirm-text": "Đęľęŧę", "delete-button": "Đęľęŧę", "delete-loading": "Đęľęŧįʼnģ...", - "text_one": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę {{numberOfDashboards}} đäşĥþőäřđş.", + "text_one": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę {{numberOfDashboards}} đäşĥþőäřđ.", "text_other": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę {{numberOfDashboards}} đäşĥþőäřđş.", "title": "Pęřmäʼnęʼnŧľy Đęľęŧę Đäşĥþőäřđş" }, "restore-modal": { - "folder-picker-text_one": "Pľęäşę čĥőőşę ä ƒőľđęř ŵĥęřę yőūř đäşĥþőäřđş ŵįľľ þę řęşŧőřęđ.", + "folder-picker-text_one": "Pľęäşę čĥőőşę ä ƒőľđęř ŵĥęřę yőūř đäşĥþőäřđ ŵįľľ þę řęşŧőřęđ.", "folder-picker-text_other": "Pľęäşę čĥőőşę ä ƒőľđęř ŵĥęřę yőūř đäşĥþőäřđş ŵįľľ þę řęşŧőřęđ.", "restore-button": "Ŗęşŧőřę", "restore-loading": "Ŗęşŧőřįʼnģ...", - "text_one": "Ŧĥįş äčŧįőʼn ŵįľľ řęşŧőřę {{numberOfDashboards}} đäşĥþőäřđş.", + "text_one": "Ŧĥįş äčŧįőʼn ŵįľľ řęşŧőřę {{numberOfDashboards}} đäşĥþőäřđ.", "text_other": "Ŧĥįş äčŧįőʼn ŵįľľ řęşŧőřę {{numberOfDashboards}} đäşĥþőäřđş.", "title": "Ŗęşŧőřę Đäşĥþőäřđş" } From 54faa541c38dea1b6c698c1f03f6aed07fddaf9c Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:23:37 +0200 Subject: [PATCH 081/174] Alerting docs: Move the `Condition operators` to the Classic condition section (#93997) Alerting docs: Move the `Condition operators` to the Classic conditions section --- .../fundamentals/alert-rule-evaluation/_index.md | 13 ------------- .../fundamentals/alert-rules/queries-conditions.md | 6 ++++++ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md index ea83debb9dc..5de512dd012 100644 --- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md @@ -53,19 +53,6 @@ The pending period specifies how long the condition must be met before firing, e You can also set the pending period to zero to skip it and have the alert fire immediately once the condition is met. -## Condition operator - -There are several condition operators available. - -- **and**: Two conditions before and after must be true for the overall condition to be true. -- **or**: If one of conditions before and after are true, the overall condition is true. -- **logic-or**: If the condition before logic-or is true, the overall condition is immediately true, without evaluating subsequent conditions. - -Here are some examples of operators. - -- `TRUE and TRUE or FALSE and FALSE` evaluate to `FALSE`, because last two conditions return `FALSE`. -- `TRUE and TRUE logic-or FALSE and FALSE` evaluate to `TRUE`, because the preceding condition returns `TRUE`. - ## Evaluation example Keep in mind: diff --git a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md index 9e6ece241e1..a3caf9f1f07 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md +++ b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md @@ -111,6 +111,12 @@ Classic conditions exist mainly for compatibility reasons and should be avoided Classic condition checks if any time series data matches the alert condition. It always produce one alert instance only, no matter how many time series meet the condition. +| Condition operators | How it works | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| and | Two conditions before and after must be true for the overall condition to be true. | +| or | If one of conditions before and after are true, the overall condition is true. | +| logic-or | If the condition before `logic-or` is true, the overall condition is immediately true, without evaluating subsequent conditions. For instance, `TRUE and TRUE logic-or FALSE and FALSE` evaluate to `TRUE`, because the preceding condition returns `TRUE`. | + ## Aggregations Grafana Alerting provides the following aggregation functions to enable you to further refine your query. From b7a7f2bd62d5ec58951273130a1b275278bb0d0a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Sep 2024 16:33:15 +0200 Subject: [PATCH 082/174] Plugins: Use handler middleware from the SDK (#93445) updates sdk to v0.251.0 --- go.mod | 2 +- go.sum | 4 +- go.work.sum | 8 +- pkg/aggregator/go.mod | 2 +- pkg/aggregator/go.sum | 4 +- pkg/api/plugin_resource_test.go | 3 +- pkg/plugins/ifaces.go | 25 +- pkg/plugins/manager/client/client.go | 4 + pkg/plugins/manager/client/client_test.go | 42 +++ .../manager/client/clienttest/clienttest.go | 331 ------------------ pkg/plugins/manager/client/decorator.go | 196 ----------- pkg/plugins/manager/client/decorator_test.go | 249 ------------- pkg/promlib/go.mod | 2 +- pkg/promlib/go.sum | 4 +- .../clientmiddleware/base_middleware.go | 60 ---- .../clientmiddleware/caching_middleware.go | 37 +- .../caching_middleware_test.go | 42 +-- .../clear_auth_headers_middleware.go | 25 +- .../clear_auth_headers_middleware_test.go | 50 +-- .../contextual_logger_middleware.go | 31 +- .../clientmiddleware/cookies_middleware.go | 27 +- .../cookies_middleware_test.go | 38 +- .../clientmiddleware/forward_id_middleware.go | 27 +- .../forward_id_middleware_test.go | 14 +- .../grafana_request_id_header_middleware.go | 29 +- ...afana_request_id_header_middleware_test.go | 18 +- .../clientmiddleware/httpclient_middleware.go | 25 +- .../httpclient_middleware_test.go | 40 +-- .../clientmiddleware/logger_middleware.go | 53 ++- .../clientmiddleware/metrics_middleware.go | 30 +- .../metrics_middleware_test.go | 40 +-- .../clientmiddleware/oauthtoken_middleware.go | 27 +- .../oauthtoken_middleware_test.go | 26 +- .../plugin_request_meta_middleware.go | 31 +- .../plugin_request_meta_middleware_test.go | 17 +- .../resource_response_middleware.go | 52 --- .../resource_response_middleware_test.go | 41 --- .../status_source_middleware.go | 15 +- .../status_source_middleware_test.go | 10 +- .../clientmiddleware/testing.go | 27 +- .../tracing_header_middleware.go | 25 +- .../tracing_header_middleware_test.go | 50 +-- .../clientmiddleware/tracing_middleware.go | 31 +- .../tracing_middleware_test.go | 59 ++-- .../user_header_middleware.go | 27 +- .../user_header_middleware_test.go | 50 +-- .../pluginsintegration/pluginsintegration.go | 22 +- 47 files changed, 524 insertions(+), 1448 deletions(-) delete mode 100644 pkg/plugins/manager/client/clienttest/clienttest.go delete mode 100644 pkg/plugins/manager/client/decorator.go delete mode 100644 pkg/plugins/manager/client/decorator_test.go delete mode 100644 pkg/services/pluginsintegration/clientmiddleware/base_middleware.go delete mode 100644 pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware.go delete mode 100644 pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware_test.go diff --git a/go.mod b/go.mod index 3d55ba923c1..4496998f90f 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.250.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.251.0 // @grafana/plugins-platform-backend github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad diff --git a/go.sum b/go.sum index 143299e44c6..92daabd7e00 100644 --- a/go.sum +++ b/go.sum @@ -2293,8 +2293,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQWUh6L85c6EJUN8Jdo= -github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= +github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990 h1:uQMZE/z+Y+o/U0z/g8ckAHss7U7LswedilByA2535DU= github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990/go.mod h1:3Vi0xv/4OBkBw4R9GAERkSrBnx06qrjpmNBRisucuSM= github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI= diff --git a/go.work.sum b/go.work.sum index ca6f13d695d..e993d1985f4 100644 --- a/go.work.sum +++ b/go.work.sum @@ -470,6 +470,8 @@ github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJ github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s= github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -792,6 +794,7 @@ github.com/relvacode/iso8601 v1.4.0 h1:GsInVSEJfkYuirYFxa80nMLbH2aydgZpIf52gYZXU github.com/relvacode/iso8601 v1.4.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= @@ -995,11 +998,9 @@ golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1011,7 +1012,6 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -1023,14 +1023,12 @@ gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPj gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo= google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys= google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf h1:T4tsZBlZYXK3j40sQNP5MBO32I+rn6ypV1PpklsiV8k= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:5/MT647Cn/GGhwTpXC7QqcaR5Cnee4v4MKCU1/nwnIQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 6b0c3c779f7..a895aa18a63 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -4,7 +4,7 @@ go 1.23.1 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.250.0 + github.com/grafana/grafana-plugin-sdk-go v0.251.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 55fbbd6b5b2..758a214e167 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -130,8 +130,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQWUh6L85c6EJUN8Jdo= -github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= +github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index a83a9400cc5..b628d35e85a 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -21,7 +21,6 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" - pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/caching" @@ -172,7 +171,7 @@ func TestCallResource(t *testing.T) { }, })) middlewares := pluginsintegration.CreateMiddlewares(cfg, &oauthtokentest.Service{}, tracing.InitializeTracerForTest(), &caching.OSSCachingService{}, featuremgmt.WithFeatures(), prometheus.DefaultRegisterer, pluginRegistry) - pc, err := pluginClient.NewDecorator(&fakes.FakePluginClient{ + pc, err := backend.HandlerFromMiddlewares(&fakes.FakePluginClient{ CallResourceHandlerFunc: backend.CallResourceHandlerFunc(func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { return errors.New("something went wrong") diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 5606d4b6259..14684b3bc55 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -88,13 +88,7 @@ type FoundPlugin struct { // Client is used to communicate with backend plugin implementations. type Client interface { - backend.QueryDataHandler - backend.CheckHealthHandler - backend.StreamHandler - backend.AdmissionHandler - backend.ConversionHandler - backend.CallResourceHandler - backend.CollectMetricsHandler + backend.Handler } // BackendFactoryProvider provides a backend factory for a provided plugin. @@ -131,23 +125,6 @@ type Licensing interface { AppURL() string } -// ClientMiddleware is an interface representing the ability to create a middleware -// that implements the Client interface. -type ClientMiddleware interface { - // CreateClientMiddleware creates a new client middleware. - CreateClientMiddleware(next Client) Client -} - -// The ClientMiddlewareFunc type is an adapter to allow the use of ordinary -// functions as ClientMiddleware's. If f is a function with the appropriate -// signature, ClientMiddlewareFunc(f) is a ClientMiddleware that calls f. -type ClientMiddlewareFunc func(next Client) Client - -// CreateClientMiddleware implements the ClientMiddleware interface. -func (fn ClientMiddlewareFunc) CreateClientMiddleware(next Client) Client { - return fn(next) -} - type SignatureCalculator interface { Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, error) } diff --git a/pkg/plugins/manager/client/client.go b/pkg/plugins/manager/client/client.go index 33753947c88..c6382451a87 100644 --- a/pkg/plugins/manager/client/client.go +++ b/pkg/plugins/manager/client/client.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/util/proxyutil" ) const ( @@ -101,8 +102,11 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq removeConnectionHeaders(res.Headers) removeHopByHopHeaders(res.Headers) removeNonAllowedHeaders(res.Headers) + } else { + res.Headers = map[string][]string{} } + proxyutil.SetProxyResponseHeaders(res.Headers) ensureContentTypeHeader(res) } diff --git a/pkg/plugins/manager/client/client_test.go b/pkg/plugins/manager/client/client_test.go index 20401d34250..420754059d2 100644 --- a/pkg/plugins/manager/client/client_test.go +++ b/pkg/plugins/manager/client/client_test.go @@ -310,6 +310,48 @@ func TestCallResource(t *testing.T) { require.Equal(t, "should not be deleted", res.Headers["X-Custom"][0]) }) + t.Run("Should set proxy response headers", func(t *testing.T) { + resHeaders := map[string][]string{ + "X-Custom": {"should not be deleted"}, + } + + req := &backend.CallResourceRequest{ + PluginContext: backend.PluginContext{ + PluginID: "pid", + }, + } + + responses := []*backend.CallResourceResponse{} + sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { + responses = append(responses, res) + return nil + }) + + p.RegisterClient(&fakePluginBackend{ + crr: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return sender.Send(&backend.CallResourceResponse{ + Headers: resHeaders, + Status: http.StatusOK, + Body: []byte(backendResponse), + }) + }, + }) + err := registry.Add(context.Background(), p) + require.NoError(t, err) + + client := ProvideService(registry) + + err = client.CallResource(context.Background(), req, sender) + require.NoError(t, err) + + require.Len(t, responses, 1) + res := responses[0] + require.Equal(t, http.StatusOK, res.Status) + require.Equal(t, []byte(backendResponse), res.Body) + require.Equal(t, "sandbox", res.Headers["Content-Security-Policy"][0]) + require.Equal(t, "should not be deleted", res.Headers["X-Custom"][0]) + }) + t.Run("Should ensure content type header", func(t *testing.T) { tcs := []struct { contentType string diff --git a/pkg/plugins/manager/client/clienttest/clienttest.go b/pkg/plugins/manager/client/clienttest/clienttest.go deleted file mode 100644 index 45886258a20..00000000000 --- a/pkg/plugins/manager/client/clienttest/clienttest.go +++ /dev/null @@ -1,331 +0,0 @@ -package clienttest - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager/client" - "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/web" -) - -type TestClient struct { - plugins.Client - QueryDataFunc backend.QueryDataHandlerFunc - CallResourceFunc backend.CallResourceHandlerFunc - CheckHealthFunc backend.CheckHealthHandlerFunc - CollectMetricsFunc backend.CollectMetricsHandlerFunc - SubscribeStreamFunc func(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) - PublishStreamFunc func(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) - RunStreamFunc func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error - ValidateAdmissionFunc backend.ValidateAdmissionFunc - MutateAdmissionFunc backend.MutateAdmissionFunc - ConvertObjectsFunc backend.ConvertObjectsFunc -} - -func (c *TestClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if c.QueryDataFunc != nil { - return c.QueryDataFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if c.CallResourceFunc != nil { - return c.CallResourceFunc(ctx, req, sender) - } - - return nil -} - -func (c *TestClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if c.CheckHealthFunc != nil { - return c.CheckHealthFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - if c.CollectMetricsFunc != nil { - return c.CollectMetricsFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - if c.PublishStreamFunc != nil { - return c.PublishStreamFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - if c.SubscribeStreamFunc != nil { - return c.SubscribeStreamFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - if c.RunStreamFunc != nil { - return c.RunStreamFunc(ctx, req, sender) - } - return nil -} - -func (c *TestClient) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - if c.ValidateAdmissionFunc != nil { - return c.ValidateAdmissionFunc(ctx, req) - } - return nil, nil -} - -func (c *TestClient) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - if c.MutateAdmissionFunc != nil { - return c.MutateAdmissionFunc(ctx, req) - } - return nil, nil -} - -func (c *TestClient) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - if c.ConvertObjectsFunc != nil { - return c.ConvertObjectsFunc(ctx, req) - } - return nil, nil -} - -type MiddlewareScenarioContext struct { - QueryDataCallChain []string - CallResourceCallChain []string - CollectMetricsCallChain []string - CheckHealthCallChain []string - SubscribeStreamCallChain []string - PublishStreamCallChain []string - RunStreamCallChain []string - InstanceSettingsCallChain []string - ValidateAdmissionCallChain []string - MutateAdmissionCallChain []string - ConvertObjectsCallChain []string -} - -func (ctx *MiddlewareScenarioContext) NewMiddleware(name string) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - return &TestMiddleware{ - next: next, - Name: name, - sCtx: ctx, - } - }) -} - -type TestMiddleware struct { - next plugins.Client - sCtx *MiddlewareScenarioContext - Name string -} - -func (m *TestMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - m.sCtx.QueryDataCallChain = append(m.sCtx.QueryDataCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.QueryData(ctx, req) - m.sCtx.QueryDataCallChain = append(m.sCtx.QueryDataCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - m.sCtx.CallResourceCallChain = append(m.sCtx.CallResourceCallChain, fmt.Sprintf("before %s", m.Name)) - err := m.next.CallResource(ctx, req, sender) - m.sCtx.CallResourceCallChain = append(m.sCtx.CallResourceCallChain, fmt.Sprintf("after %s", m.Name)) - return err -} - -func (m *TestMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - m.sCtx.CollectMetricsCallChain = append(m.sCtx.CollectMetricsCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.CollectMetrics(ctx, req) - m.sCtx.CollectMetricsCallChain = append(m.sCtx.CollectMetricsCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.ValidateAdmission(ctx, req) - m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.MutateAdmission(ctx, req) - m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - m.sCtx.ConvertObjectsCallChain = append(m.sCtx.ConvertObjectsCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.ConvertObjects(ctx, req) - m.sCtx.ConvertObjectsCallChain = append(m.sCtx.ConvertObjectsCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.CheckHealth(ctx, req) - m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - m.sCtx.SubscribeStreamCallChain = append(m.sCtx.SubscribeStreamCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.SubscribeStream(ctx, req) - m.sCtx.SubscribeStreamCallChain = append(m.sCtx.SubscribeStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - m.sCtx.PublishStreamCallChain = append(m.sCtx.PublishStreamCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.PublishStream(ctx, req) - m.sCtx.PublishStreamCallChain = append(m.sCtx.PublishStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - m.sCtx.RunStreamCallChain = append(m.sCtx.RunStreamCallChain, fmt.Sprintf("before %s", m.Name)) - err := m.next.RunStream(ctx, req, sender) - m.sCtx.RunStreamCallChain = append(m.sCtx.RunStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return err -} - -var _ plugins.Client = &TestClient{} - -type ClientDecoratorTest struct { - T *testing.T - Context context.Context - TestClient *TestClient - Middlewares []plugins.ClientMiddleware - Decorator *client.Decorator - ReqContext *contextmodel.ReqContext - QueryDataReq *backend.QueryDataRequest - QueryDataCtx context.Context - CallResourceReq *backend.CallResourceRequest - CallResourceCtx context.Context - CheckHealthReq *backend.CheckHealthRequest - CheckHealthCtx context.Context - CollectMetricsReq *backend.CollectMetricsRequest - CollectMetricsCtx context.Context - SubscribeStreamReq *backend.SubscribeStreamRequest - SubscribeStreamCtx context.Context - PublishStreamReq *backend.PublishStreamRequest - PublishStreamCtx context.Context - - // When CallResource is called, the sender will be called with these values - callResourceResponses []*backend.CallResourceResponse -} - -type ClientDecoratorTestOption func(*ClientDecoratorTest) - -func NewClientDecoratorTest(t *testing.T, opts ...ClientDecoratorTestOption) *ClientDecoratorTest { - cdt := &ClientDecoratorTest{ - T: t, - Context: context.Background(), - } - cdt.TestClient = &TestClient{ - QueryDataFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - cdt.QueryDataReq = req - cdt.QueryDataCtx = ctx - return nil, nil - }, - CallResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - cdt.CallResourceReq = req - cdt.CallResourceCtx = ctx - if cdt.callResourceResponses != nil { - for _, r := range cdt.callResourceResponses { - if err := sender.Send(r); err != nil { - return err - } - } - } - return nil - }, - CheckHealthFunc: func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - cdt.CheckHealthReq = req - cdt.CheckHealthCtx = ctx - return nil, nil - }, - CollectMetricsFunc: func(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - cdt.CollectMetricsReq = req - cdt.CollectMetricsCtx = ctx - return nil, nil - }, - SubscribeStreamFunc: func(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - cdt.SubscribeStreamReq = req - cdt.SubscribeStreamCtx = ctx - return nil, nil - }, - PublishStreamFunc: func(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - cdt.PublishStreamReq = req - cdt.PublishStreamCtx = ctx - return nil, nil - }, - } - require.NotNil(t, cdt) - - for _, opt := range opts { - opt(cdt) - } - - d, err := client.NewDecorator(cdt.TestClient, cdt.Middlewares...) - require.NoError(t, err) - require.NotNil(t, d) - - cdt.Decorator = d - - return cdt -} - -func WithReqContext(req *http.Request, user *user.SignedInUser) ClientDecoratorTestOption { - return ClientDecoratorTestOption(func(cdt *ClientDecoratorTest) { - if cdt.ReqContext == nil { - cdt.ReqContext = &contextmodel.ReqContext{ - Context: &web.Context{ - Resp: web.NewResponseWriter(req.Method, httptest.NewRecorder()), - }, - SignedInUser: user, - } - } - - cdt.Context = ctxkey.Set(cdt.Context, cdt.ReqContext) - - *req = *req.WithContext(cdt.Context) - cdt.ReqContext.Req = req - }) -} - -func WithMiddlewares(middlewares ...plugins.ClientMiddleware) ClientDecoratorTestOption { - return ClientDecoratorTestOption(func(cdt *ClientDecoratorTest) { - if cdt.Middlewares == nil { - cdt.Middlewares = []plugins.ClientMiddleware{} - } - - cdt.Middlewares = append(cdt.Middlewares, middlewares...) - }) -} - -// WithResourceResponses can be used to make the test client send simulated resource responses back over the sender stream -func WithResourceResponses(responses []*backend.CallResourceResponse) ClientDecoratorTestOption { - return ClientDecoratorTestOption(func(cdt *ClientDecoratorTest) { - cdt.callResourceResponses = responses - }) -} diff --git a/pkg/plugins/manager/client/decorator.go b/pkg/plugins/manager/client/decorator.go deleted file mode 100644 index 90a679d5ebe..00000000000 --- a/pkg/plugins/manager/client/decorator.go +++ /dev/null @@ -1,196 +0,0 @@ -package client - -import ( - "context" - "errors" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/plugins" -) - -// Decorator allows a plugins.Client to be decorated with middlewares. -type Decorator struct { - client plugins.Client - middlewares []plugins.ClientMiddleware -} - -var ( - _ = plugins.Client(&Decorator{}) -) - -// NewDecorator creates a new plugins.client decorator. -func NewDecorator(client plugins.Client, middlewares ...plugins.ClientMiddleware) (*Decorator, error) { - if client == nil { - return nil, errors.New("client cannot be nil") - } - - return &Decorator{ - client: client, - middlewares: middlewares, - }, nil -} - -func (d *Decorator) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if req == nil { - return nil, errNilRequest - } - ctx = backend.WithEndpoint(ctx, backend.EndpointQueryData) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - - return client.QueryData(ctx, req) -} - -func (d *Decorator) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req == nil { - return errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointCallResource) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - if sender == nil { - return errors.New("sender cannot be nil") - } - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.CallResource(ctx, req, sender) -} - -func (d *Decorator) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointCollectMetrics) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.CollectMetrics(ctx, req) -} - -func (d *Decorator) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointCheckHealth) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.CheckHealth(ctx, req) -} - -func (d *Decorator) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointSubscribeStream) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.SubscribeStream(ctx, req) -} - -func (d *Decorator) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointPublishStream) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.PublishStream(ctx, req) -} - -func (d *Decorator) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - if req == nil { - return errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointRunStream) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - if sender == nil { - return errors.New("sender cannot be nil") - } - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.RunStream(ctx, req, sender) -} - -func (d *Decorator) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointValidateAdmission) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.ValidateAdmission(ctx, req) -} - -func (d *Decorator) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointMutateAdmission) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.MutateAdmission(ctx, req) -} - -func (d *Decorator) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointConvertObject) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.ConvertObjects(ctx, req) -} - -func clientFromMiddlewares(middlewares []plugins.ClientMiddleware, finalClient plugins.Client) plugins.Client { - if len(middlewares) == 0 { - return finalClient - } - - reversed := reverseMiddlewares(middlewares) - next := finalClient - - for _, m := range reversed { - next = m.CreateClientMiddleware(next) - } - - return next -} - -func reverseMiddlewares(middlewares []plugins.ClientMiddleware) []plugins.ClientMiddleware { - reversed := make([]plugins.ClientMiddleware, len(middlewares)) - copy(reversed, middlewares) - - for i, j := 0, len(reversed)-1; i < j; i, j = i+1, j-1 { - reversed[i], reversed[j] = reversed[j], reversed[i] - } - - return reversed -} diff --git a/pkg/plugins/manager/client/decorator_test.go b/pkg/plugins/manager/client/decorator_test.go deleted file mode 100644 index 5ed063233e7..00000000000 --- a/pkg/plugins/manager/client/decorator_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package client - -import ( - "context" - "fmt" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/plugins" -) - -func TestDecorator(t *testing.T) { - var queryDataCalled bool - var callResourceCalled bool - var checkHealthCalled bool - c := &TestClient{ - QueryDataFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - queryDataCalled = true - return nil, nil - }, - CallResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - callResourceCalled = true - return nil - }, - CheckHealthFunc: func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - checkHealthCalled = true - return nil, nil - }, - } - require.NotNil(t, c) - - ctx := MiddlewareScenarioContext{} - - mwOne := ctx.NewMiddleware("mw1") - mwTwo := ctx.NewMiddleware("mw2") - - d, err := NewDecorator(c, mwOne, mwTwo) - require.NoError(t, err) - require.NotNil(t, d) - - _, _ = d.QueryData(context.Background(), &backend.QueryDataRequest{}) - require.True(t, queryDataCalled) - - sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { - return nil - }) - - _ = d.CallResource(context.Background(), &backend.CallResourceRequest{}, sender) - require.True(t, callResourceCalled) - - _, _ = d.CheckHealth(context.Background(), &backend.CheckHealthRequest{}) - require.True(t, checkHealthCalled) - - require.Len(t, ctx.QueryDataCallChain, 4) - require.EqualValues(t, []string{"before mw1", "before mw2", "after mw2", "after mw1"}, ctx.QueryDataCallChain) - require.Len(t, ctx.CallResourceCallChain, 4) - require.EqualValues(t, []string{"before mw1", "before mw2", "after mw2", "after mw1"}, ctx.CallResourceCallChain) - require.Len(t, ctx.CheckHealthCallChain, 4) - require.EqualValues(t, []string{"before mw1", "before mw2", "after mw2", "after mw1"}, ctx.CheckHealthCallChain) -} - -func TestReverseMiddlewares(t *testing.T) { - t.Run("Should reverse 1 middleware", func(t *testing.T) { - ctx := MiddlewareScenarioContext{} - middlewares := []plugins.ClientMiddleware{ - ctx.NewMiddleware("mw1"), - } - reversed := reverseMiddlewares(middlewares) - require.Len(t, reversed, 1) - require.Equal(t, "mw1", reversed[0].CreateClientMiddleware(nil).(*TestMiddleware).Name) - }) - - t.Run("Should reverse 2 middlewares", func(t *testing.T) { - ctx := MiddlewareScenarioContext{} - middlewares := []plugins.ClientMiddleware{ - ctx.NewMiddleware("mw1"), - ctx.NewMiddleware("mw2"), - } - reversed := reverseMiddlewares(middlewares) - require.Len(t, reversed, 2) - require.Equal(t, "mw2", reversed[0].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw1", reversed[1].CreateClientMiddleware(nil).(*TestMiddleware).Name) - }) - - t.Run("Should reverse 3 middlewares", func(t *testing.T) { - ctx := MiddlewareScenarioContext{} - middlewares := []plugins.ClientMiddleware{ - ctx.NewMiddleware("mw1"), - ctx.NewMiddleware("mw2"), - ctx.NewMiddleware("mw3"), - } - reversed := reverseMiddlewares(middlewares) - require.Len(t, reversed, 3) - require.Equal(t, "mw3", reversed[0].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw2", reversed[1].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw1", reversed[2].CreateClientMiddleware(nil).(*TestMiddleware).Name) - }) - - t.Run("Should reverse 4 middlewares", func(t *testing.T) { - ctx := MiddlewareScenarioContext{} - middlewares := []plugins.ClientMiddleware{ - ctx.NewMiddleware("mw1"), - ctx.NewMiddleware("mw2"), - ctx.NewMiddleware("mw3"), - ctx.NewMiddleware("mw4"), - } - reversed := reverseMiddlewares(middlewares) - require.Len(t, reversed, 4) - require.Equal(t, "mw4", reversed[0].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw3", reversed[1].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw2", reversed[2].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw1", reversed[3].CreateClientMiddleware(nil).(*TestMiddleware).Name) - }) -} - -type TestClient struct { - plugins.Client - QueryDataFunc backend.QueryDataHandlerFunc - CallResourceFunc backend.CallResourceHandlerFunc - CheckHealthFunc backend.CheckHealthHandlerFunc -} - -func (c *TestClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if c.QueryDataFunc != nil { - return c.QueryDataFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if c.CallResourceFunc != nil { - return c.CallResourceFunc(ctx, req, sender) - } - - return nil -} - -func (c *TestClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if c.CheckHealthFunc != nil { - return c.CheckHealthFunc(ctx, req) - } - - return nil, nil -} - -type MiddlewareScenarioContext struct { - QueryDataCallChain []string - CallResourceCallChain []string - CollectMetricsCallChain []string - CheckHealthCallChain []string - SubscribeStreamCallChain []string - PublishStreamCallChain []string - RunStreamCallChain []string - InstanceSettingsCallChain []string - ValidateAdmissionCallChain []string - MutateAdmissionCallChain []string - ConvertObjectCallChain []string -} - -func (ctx *MiddlewareScenarioContext) NewMiddleware(name string) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - return &TestMiddleware{ - next: next, - Name: name, - sCtx: ctx, - } - }) -} - -type TestMiddleware struct { - next plugins.Client - sCtx *MiddlewareScenarioContext - Name string -} - -func (m *TestMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - m.sCtx.QueryDataCallChain = append(m.sCtx.QueryDataCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.QueryData(ctx, req) - m.sCtx.QueryDataCallChain = append(m.sCtx.QueryDataCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - m.sCtx.CallResourceCallChain = append(m.sCtx.CallResourceCallChain, fmt.Sprintf("before %s", m.Name)) - err := m.next.CallResource(ctx, req, sender) - m.sCtx.CallResourceCallChain = append(m.sCtx.CallResourceCallChain, fmt.Sprintf("after %s", m.Name)) - return err -} - -func (m *TestMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - m.sCtx.CollectMetricsCallChain = append(m.sCtx.CollectMetricsCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.CollectMetrics(ctx, req) - m.sCtx.CollectMetricsCallChain = append(m.sCtx.CollectMetricsCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.CheckHealth(ctx, req) - m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - m.sCtx.SubscribeStreamCallChain = append(m.sCtx.SubscribeStreamCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.SubscribeStream(ctx, req) - m.sCtx.SubscribeStreamCallChain = append(m.sCtx.SubscribeStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - m.sCtx.PublishStreamCallChain = append(m.sCtx.PublishStreamCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.PublishStream(ctx, req) - m.sCtx.PublishStreamCallChain = append(m.sCtx.PublishStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - m.sCtx.RunStreamCallChain = append(m.sCtx.RunStreamCallChain, fmt.Sprintf("before %s", m.Name)) - err := m.next.RunStream(ctx, req, sender) - m.sCtx.RunStreamCallChain = append(m.sCtx.RunStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return err -} - -func (m *TestMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.ValidateAdmission(ctx, req) - m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.MutateAdmission(ctx, req) - m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - m.sCtx.ConvertObjectCallChain = append(m.sCtx.ConvertObjectCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.ConvertObjects(ctx, req) - m.sCtx.ConvertObjectCallChain = append(m.sCtx.ConvertObjectCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -var _ plugins.Client = &TestClient{} diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index ba5b99a39c3..81f09839646 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -4,7 +4,7 @@ go 1.23.1 require ( github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 - github.com/grafana/grafana-plugin-sdk-go v0.250.0 + github.com/grafana/grafana-plugin-sdk-go v0.251.0 github.com/json-iterator/go v1.1.12 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.20.3 diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index ae0bdbf758b..9d75718ccf1 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -98,8 +98,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 h1:as4PmrFoYI1byS5JjsgPC7uSGTMh+SgS0ePv6hOyDGU= github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3/go.mod h1:lcjGB6SuaZ2o44A9nD6p/tR4QXSPbzViRY520Gy6pTQ= -github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQWUh6L85c6EJUN8Jdo= -github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= +github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= diff --git a/pkg/services/pluginsintegration/clientmiddleware/base_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/base_middleware.go deleted file mode 100644 index 32a795a5477..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/base_middleware.go +++ /dev/null @@ -1,60 +0,0 @@ -package clientmiddleware - -import ( - "context" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/plugins" -) - -var _ = plugins.Client(&baseMiddleware{}) - -// The base middleware simply passes the request down the chain -// This allows middleware to avoid implementing all the noop functions -type baseMiddleware struct { - next plugins.Client -} - -func (m *baseMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - return m.next.QueryData(ctx, req) -} - -func (m *baseMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - return m.next.CallResource(ctx, req, sender) -} - -func (m *baseMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - return m.next.CheckHealth(ctx, req) -} - -func (m *baseMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - return m.next.CollectMetrics(ctx, req) -} - -func (m *baseMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - return m.next.SubscribeStream(ctx, req) -} - -func (m *baseMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - return m.next.PublishStream(ctx, req) -} - -func (m *baseMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - return m.next.RunStream(ctx, req, sender) -} - -// ValidateAdmission implements backend.AdmissionHandler. -func (m *baseMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - return m.next.ValidateAdmission(ctx, req) -} - -// MutateAdmission implements backend.AdmissionHandler. -func (m *baseMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - return m.next.MutateAdmission(ctx, req) -} - -// ConvertObject implements backend.AdmissionHandler. -func (m *baseMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - return m.next.ConvertObjects(ctx, req) -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go index 95756b169af..80058c0a5e8 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go @@ -10,7 +10,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -19,15 +18,15 @@ import ( // needed to mock the function for testing var shouldCacheQuery = awsds.ShouldCacheQuery -// NewCachingMiddleware creates a new plugins.ClientMiddleware that will +// NewCachingMiddleware creates a new backend.HandlerMiddleware that will // attempt to read and write query results to the cache -func NewCachingMiddleware(cachingService caching.CachingService) plugins.ClientMiddleware { +func NewCachingMiddleware(cachingService caching.CachingService) backend.HandlerMiddleware { return NewCachingMiddlewareWithFeatureManager(cachingService, nil) } -// NewCachingMiddlewareWithFeatureManager creates a new plugins.ClientMiddleware that will +// NewCachingMiddlewareWithFeatureManager creates a new backend.HandlerMiddleware that will // attempt to read and write query results to the cache with a feature manager -func NewCachingMiddlewareWithFeatureManager(cachingService caching.CachingService, features featuremgmt.FeatureToggles) plugins.ClientMiddleware { +func NewCachingMiddlewareWithFeatureManager(cachingService caching.CachingService, features featuremgmt.FeatureToggles) backend.HandlerMiddleware { log := log.New("caching_middleware") if err := prometheus.Register(QueryCachingRequestHistogram); err != nil { log.Error("Error registering prometheus collector 'QueryRequestHistogram'", "error", err) @@ -35,20 +34,18 @@ func NewCachingMiddlewareWithFeatureManager(cachingService caching.CachingServic if err := prometheus.Register(ResourceCachingRequestHistogram); err != nil { log.Error("Error registering prometheus collector 'ResourceRequestHistogram'", "error", err) } - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &CachingMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, - caching: cachingService, - log: log, - features: features, + BaseHandler: backend.NewBaseHandler(next), + caching: cachingService, + log: log, + features: features, } }) } type CachingMiddleware struct { - baseMiddleware + backend.BaseHandler caching caching.CachingService log log.Logger @@ -60,12 +57,12 @@ type CachingMiddleware struct { // If the cache service is implemented, we capture the request duration as a metric. The service is expected to write any response headers. func (m *CachingMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } reqCtx := contexthandler.FromContext(ctx) if reqCtx == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } // time how long this request takes @@ -92,7 +89,7 @@ func (m *CachingMiddleware) QueryData(ctx context.Context, req *backend.QueryDat } // Cache miss; do the actual queries - resp, err := m.next.QueryData(ctx, req) + resp, err := m.BaseHandler.QueryData(ctx, req) // Update the query cache with the result for this metrics request if err == nil && cr.UpdateCacheFn != nil { @@ -125,12 +122,12 @@ func (m *CachingMiddleware) QueryData(ctx context.Context, req *backend.QueryDat // If the cache service is implemented, we capture the request duration as a metric. The service is expected to write any response headers. func (m *CachingMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } reqCtx := contexthandler.FromContext(ctx) if reqCtx == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } // time how long this request takes @@ -157,7 +154,7 @@ func (m *CachingMiddleware) CallResource(ctx context.Context, req *backend.CallR // Cache miss; do the actual request // If there is no update cache func, just pass in the original sender if cr.UpdateCacheFn == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } // Otherwise, intercept the responses in a wrapped sender so we can cache them first cacheSender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { @@ -165,5 +162,5 @@ func (m *CachingMiddleware) CallResource(ctx context.Context, req *backend.CallR return sender.Send(res) }) - return m.next.CallResource(ctx, req, cacheSender) + return m.BaseHandler.CallResource(ctx, req, cacheSender) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go index 5a54eafc775..548f7a693bf 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -22,9 +22,9 @@ func TestCachingMiddleware(t *testing.T) { require.NoError(t, err) cs := caching.NewFakeOSSCachingService() - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCachingMiddleware(cs)), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCachingMiddleware(cs)), ) jsonDataMap := map[string]any{} @@ -63,7 +63,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = true cs.ReturnQueryResponse = dataResponse - resp, err := cdt.Decorator.QueryData(req.Context(), qdr) + resp, err := cdt.MiddlewareHandler.QueryData(req.Context(), qdr) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleQueryRequest", 1) @@ -92,7 +92,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = false cs.ReturnQueryResponse = dataResponse - resp, err := cdt.Decorator.QueryData(req.Context(), qdr) + resp, err := cdt.MiddlewareHandler.QueryData(req.Context(), qdr) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleQueryRequest", 1) @@ -105,9 +105,9 @@ func TestCachingMiddleware(t *testing.T) { }) t.Run("with async queries", func(t *testing.T) { - asyncCdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares( + asyncCdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares( NewCachingMiddlewareWithFeatureManager(cs, featuremgmt.WithFeatures(featuremgmt.FlagAwsAsyncQueryCaching))), ) t.Run("If shoudCacheQuery returns true update cache function is called", func(t *testing.T) { @@ -128,7 +128,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = false cs.ReturnQueryResponse = dataResponse - resp, err := asyncCdt.Decorator.QueryData(req.Context(), qdr) + resp, err := asyncCdt.MiddlewareHandler.QueryData(req.Context(), qdr) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleQueryRequest", 1) @@ -158,7 +158,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = false cs.ReturnQueryResponse = dataResponse - resp, err := asyncCdt.Decorator.QueryData(req.Context(), qdr) + resp, err := asyncCdt.MiddlewareHandler.QueryData(req.Context(), qdr) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleQueryRequest", 1) @@ -196,10 +196,10 @@ func TestCachingMiddleware(t *testing.T) { } cs := caching.NewFakeOSSCachingService() - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCachingMiddleware(cs)), - clienttest.WithResourceResponses([]*backend.CallResourceResponse{simulatedPluginResponse}), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCachingMiddleware(cs)), + handlertest.WithResourceResponses([]*backend.CallResourceResponse{simulatedPluginResponse}), ) jsonDataMap := map[string]any{} @@ -235,7 +235,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = true cs.ReturnResourceResponse = dataResponse - err := cdt.Decorator.CallResource(req.Context(), crr, storeOneResponseCallResourceSender) + err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, storeOneResponseCallResourceSender) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleResourceRequest", 1) @@ -255,7 +255,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = false cs.ReturnResourceResponse = dataResponse - err := cdt.Decorator.CallResource(req.Context(), crr, storeOneResponseCallResourceSender) + err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, storeOneResponseCallResourceSender) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleResourceRequest", 1) @@ -272,9 +272,9 @@ func TestCachingMiddleware(t *testing.T) { require.NoError(t, err) cs := caching.NewFakeOSSCachingService() - cdt := clienttest.NewClientDecoratorTest(t, + cdt := handlertest.NewHandlerMiddlewareTest(t, // Skip the request context in this case - clienttest.WithMiddlewares(NewCachingMiddleware(cs)), + handlertest.WithMiddlewares(NewCachingMiddleware(cs)), ) reqCtx := contexthandler.FromContext(req.Context()) require.Nil(t, reqCtx) @@ -298,7 +298,7 @@ func TestCachingMiddleware(t *testing.T) { PluginContext: pluginCtx, } - resp, err := cdt.Decorator.QueryData(context.Background(), qdr) + resp, err := cdt.MiddlewareHandler.QueryData(context.Background(), qdr) assert.NoError(t, err) // Cache service is never called cs.AssertCalls(t, "HandleQueryRequest", 0) @@ -315,7 +315,7 @@ func TestCachingMiddleware(t *testing.T) { PluginContext: pluginCtx, } - err := cdt.Decorator.CallResource(req.Context(), crr, nopCallResourceSender) + err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, nopCallResourceSender) assert.NoError(t, err) // Cache service is never called cs.AssertCalls(t, "HandleResourceRequest", 0) diff --git a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go index 1ec57e09f39..7a942eb4661 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go @@ -5,25 +5,22 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" ) -// NewClearAuthHeadersMiddleware creates a new plugins.ClientMiddleware +// NewClearAuthHeadersMiddleware creates a new backend.HandlerMiddleware // that will clear any outgoing HTTP headers that was part of the incoming // HTTP request and used when authenticating to Grafana. -func NewClearAuthHeadersMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewClearAuthHeadersMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &ClearAuthHeadersMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type ClearAuthHeadersMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *ClearAuthHeadersMiddleware) clearHeaders(ctx context.Context, h backend.ForwardHTTPHeaders) { @@ -43,30 +40,30 @@ func (m *ClearAuthHeadersMiddleware) clearHeaders(ctx context.Context, h backend func (m *ClearAuthHeadersMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } m.clearHeaders(ctx, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *ClearAuthHeadersMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } m.clearHeaders(ctx, req) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *ClearAuthHeadersMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } m.clearHeaders(ctx, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go index 2aa709ff987..a02e3d3500d 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -23,9 +23,9 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { req.Header.Set(otherHeader, "test") t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewClearAuthHeadersMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -33,7 +33,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { } t.Run("Should not attach delete headers middleware when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -43,7 +43,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should not attach delete headers middleware when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -53,7 +53,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should not attach delete headers middleware when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -64,9 +64,9 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewClearAuthHeadersMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -74,7 +74,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { } t.Run("Should not attach delete headers middleware when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -84,7 +84,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should not attach delete headers middleware when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -94,7 +94,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should not attach delete headers middleware when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -110,9 +110,9 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { require.NoError(t, err) t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewClearAuthHeadersMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), ) req := req.WithContext(contexthandler.WithAuthHTTPHeaders(req.Context(), setting.NewCfg())) @@ -126,7 +126,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { } t.Run("Should attach delete headers middleware when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -137,7 +137,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should attach delete headers middleware when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -148,7 +148,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should attach delete headers middleware when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -160,9 +160,9 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewClearAuthHeadersMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), ) req := req.WithContext(contexthandler.WithAuthHTTPHeaders(req.Context(), setting.NewCfg())) @@ -176,7 +176,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { } t.Run("Should attach delete headers middleware when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -187,7 +187,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should attach delete headers middleware when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -198,7 +198,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should attach delete headers middleware when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go index 53325809c73..fbb6f7c23e7 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go @@ -6,21 +6,20 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" ) -// NewContextualLoggerMiddleware creates a new plugins.ClientMiddleware that adds +// NewContextualLoggerMiddleware creates a new backend.HandlerMiddleware that adds // a contextual logger to the request context. -func NewContextualLoggerMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewContextualLoggerMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &ContextualLoggerMiddleware{ - next: next, + BaseHandler: backend.NewBaseHandler(next), } }) } type ContextualLoggerMiddleware struct { - next plugins.Client + backend.BaseHandler } // instrumentContext adds a contextual logger with plugin and request details to the given context. @@ -45,53 +44,53 @@ func instrumentContext(ctx context.Context, pCtx backend.PluginContext) context. func (m *ContextualLoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *ContextualLoggerMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *ContextualLoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } func (m *ContextualLoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.CollectMetrics(ctx, req) + return m.BaseHandler.CollectMetrics(ctx, req) } func (m *ContextualLoggerMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.SubscribeStream(ctx, req) + return m.BaseHandler.SubscribeStream(ctx, req) } func (m *ContextualLoggerMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.PublishStream(ctx, req) + return m.BaseHandler.PublishStream(ctx, req) } func (m *ContextualLoggerMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.RunStream(ctx, req, sender) + return m.BaseHandler.RunStream(ctx, req, sender) } // ValidateAdmission implements backend.AdmissionHandler. func (m *ContextualLoggerMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.ValidateAdmission(ctx, req) + return m.BaseHandler.ValidateAdmission(ctx, req) } // MutateAdmission implements backend.AdmissionHandler. func (m *ContextualLoggerMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.MutateAdmission(ctx, req) + return m.BaseHandler.MutateAdmission(ctx, req) } // ConvertObject implements backend.AdmissionHandler. func (m *ContextualLoggerMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.ConvertObjects(ctx, req) + return m.BaseHandler.ConvertObjects(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware.go index 73e1a1f3d78..fc1ba372ae5 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/util/proxyutil" @@ -14,22 +13,20 @@ import ( const cookieHeaderName = "Cookie" -// NewCookiesMiddleware creates a new plugins.ClientMiddleware that will -// forward incoming HTTP request Cookies to outgoing plugins.Client requests +// NewCookiesMiddleware creates a new backend.HandlerMiddleware that will +// forward incoming HTTP request Cookies to outgoing backend.Handler requests // if the datasource has enabled forwarding of cookies (keepCookies). -func NewCookiesMiddleware(skipCookiesNames []string) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewCookiesMiddleware(skipCookiesNames []string) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &CookiesMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), skipCookiesNames: skipCookiesNames, } }) } type CookiesMiddleware struct { - baseMiddleware + backend.BaseHandler skipCookiesNames []string } @@ -87,7 +84,7 @@ func (m *CookiesMiddleware) applyCookies(ctx context.Context, pCtx backend.Plugi func (m *CookiesMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } err := m.applyCookies(ctx, req.PluginContext, req) @@ -95,12 +92,12 @@ func (m *CookiesMiddleware) QueryData(ctx context.Context, req *backend.QueryDat return nil, err } - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *CookiesMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } err := m.applyCookies(ctx, req.PluginContext, req) @@ -108,12 +105,12 @@ func (m *CookiesMiddleware) CallResource(ctx context.Context, req *backend.CallR return err } - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *CookiesMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } err := m.applyCookies(ctx, req.PluginContext, req) @@ -121,5 +118,5 @@ func (m *CookiesMiddleware) CheckHealth(ctx context.Context, req *backend.CheckH return nil, err } - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware_test.go index 41822d6a4ba..778363b4936 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" ) @@ -28,9 +28,9 @@ func TestCookiesMiddleware(t *testing.T) { }) req.Header.Set(otherHeader, "test") - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), ) jsonDataMap := map[string]any{} @@ -44,7 +44,7 @@ func TestCookiesMiddleware(t *testing.T) { } t.Run("Should not forward cookies when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -60,7 +60,7 @@ func TestCookiesMiddleware(t *testing.T) { Headers: map[string][]string{otherHeader: {"test"}}, } pReq.Headers[backend.CookiesHeaderName] = []string{req.Header.Get(backend.CookiesHeaderName)} - err = cdt.Decorator.CallResource(req.Context(), pReq, nopCallResourceSender) + err = cdt.MiddlewareHandler.CallResource(req.Context(), pReq, nopCallResourceSender) require.NoError(t, err) require.NotNil(t, cdt.CallResourceReq) require.Len(t, cdt.CallResourceReq.Headers, 1) @@ -68,7 +68,7 @@ func TestCookiesMiddleware(t *testing.T) { }) t.Run("Should not forward cookies when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -97,9 +97,9 @@ func TestCookiesMiddleware(t *testing.T) { req.Header.Set(otherHeader, "test") - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), ) jsonDataMap := map[string]any{ @@ -115,7 +115,7 @@ func TestCookiesMiddleware(t *testing.T) { } t.Run("Should forward cookies when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -127,7 +127,7 @@ func TestCookiesMiddleware(t *testing.T) { }) t.Run("Should forward cookies when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -140,7 +140,7 @@ func TestCookiesMiddleware(t *testing.T) { }) t.Run("Should forward cookies when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -166,9 +166,9 @@ func TestCookiesMiddleware(t *testing.T) { }) req.Header.Set(otherHeader, "test") - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), ) pluginCtx := backend.PluginContext{ @@ -181,7 +181,7 @@ func TestCookiesMiddleware(t *testing.T) { Headers: map[string]string{otherHeader: "test"}, } pReq.Headers[backend.CookiesHeaderName] = req.Header.Get(backend.CookiesHeaderName) - _, err = cdt.Decorator.QueryData(req.Context(), pReq) + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), pReq) require.NoError(t, err) require.NotNil(t, cdt.QueryDataReq) require.Len(t, cdt.QueryDataReq.Headers, 1) @@ -194,7 +194,7 @@ func TestCookiesMiddleware(t *testing.T) { Headers: map[string][]string{otherHeader: {"test"}}, } pReq.Headers[backend.CookiesHeaderName] = []string{req.Header.Get(backend.CookiesHeaderName)} - err = cdt.Decorator.CallResource(req.Context(), pReq, nopCallResourceSender) + err = cdt.MiddlewareHandler.CallResource(req.Context(), pReq, nopCallResourceSender) require.NoError(t, err) require.NotNil(t, cdt.CallResourceReq) require.Len(t, cdt.CallResourceReq.Headers, 1) @@ -207,7 +207,7 @@ func TestCookiesMiddleware(t *testing.T) { Headers: map[string]string{otherHeader: "test"}, } pReq.Headers[backend.CookiesHeaderName] = req.Header.Get(backend.CookiesHeaderName) - _, err = cdt.Decorator.CheckHealth(req.Context(), pReq) + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), pReq) require.NoError(t, err) require.NotNil(t, cdt.CheckHealthReq) require.Len(t, cdt.CheckHealthReq.Headers, 1) diff --git a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go index 9e799759157..42764a7641f 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go @@ -5,26 +5,23 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" ) const forwardIDHeaderName = "X-Grafana-Id" -// NewForwardIDMiddleware creates a new plugins.ClientMiddleware that will -// set grafana id header on outgoing plugins.Client requests -func NewForwardIDMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +// NewForwardIDMiddleware creates a new backend.HandlerMiddleware that will +// set grafana id header on outgoing backend.Handler requests +func NewForwardIDMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &ForwardIDMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type ForwardIDMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *ForwardIDMiddleware) applyToken(ctx context.Context, pCtx backend.PluginContext, req backend.ForwardHTTPHeaders) error { @@ -43,7 +40,7 @@ func (m *ForwardIDMiddleware) applyToken(ctx context.Context, pCtx backend.Plugi func (m *ForwardIDMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } err := m.applyToken(ctx, req.PluginContext, req) @@ -51,12 +48,12 @@ func (m *ForwardIDMiddleware) QueryData(ctx context.Context, req *backend.QueryD return nil, err } - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *ForwardIDMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } err := m.applyToken(ctx, req.PluginContext, req) @@ -64,12 +61,12 @@ func (m *ForwardIDMiddleware) CallResource(ctx context.Context, req *backend.Cal return err } - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *ForwardIDMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } err := m.applyToken(ctx, req.PluginContext, req) @@ -77,5 +74,5 @@ func (m *ForwardIDMiddleware) CheckHealth(ctx context.Context, req *backend.Chec return nil, err } - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go index 2972c612a92..bc8532e7f75 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" @@ -21,14 +21,14 @@ func TestForwardIDMiddleware(t *testing.T) { } t.Run("Should set forwarded id header if present", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewForwardIDMiddleware())) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{IDToken: "some-token"}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: pluginContext, }, nopCallResourceSender) require.NoError(t, err) @@ -37,14 +37,14 @@ func TestForwardIDMiddleware(t *testing.T) { }) t.Run("Should not set forwarded id header if not present", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewForwardIDMiddleware())) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: pluginContext, }, nopCallResourceSender) require.NoError(t, err) @@ -57,14 +57,14 @@ func TestForwardIDMiddleware(t *testing.T) { } t.Run("Should set forwarded id header to app plugin if present", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewForwardIDMiddleware())) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{IDToken: "some-token"}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: pluginContext, }, nopCallResourceSender) require.NoError(t, err) diff --git a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go index c6c4a5947a6..9a23168c6f5 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -24,24 +23,22 @@ const GrafanaSignedRequestID = "X-Grafana-Signed-Request-Id" const XRealIPHeader = "X-Real-Ip" const GrafanaInternalRequest = "X-Grafana-Internal-Request" -// NewHostedGrafanaACHeaderMiddleware creates a new plugins.ClientMiddleware that will +// NewHostedGrafanaACHeaderMiddleware creates a new backend.HandlerMiddleware that will // generate a random request ID, sign it using internal key and populate X-Grafana-Request-ID with the request ID // and X-Grafana-Signed-Request-ID with signed request ID. We can then use this to verify that the request // is coming from hosted Grafana and is not an external request. This is used for IP range access control. -func NewHostedGrafanaACHeaderMiddleware(cfg *setting.Cfg) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewHostedGrafanaACHeaderMiddleware(cfg *setting.Cfg) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &HostedGrafanaACHeaderMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, - log: log.New("ip_header_middleware"), - cfg: cfg, + BaseHandler: backend.NewBaseHandler(next), + log: log.New("ip_header_middleware"), + cfg: cfg, } }) } type HostedGrafanaACHeaderMiddleware struct { - baseMiddleware + backend.BaseHandler log log.Logger cfg *setting.Cfg } @@ -120,30 +117,30 @@ func GetGrafanaRequestIDHeaders(req *http.Request, cfg *setting.Cfg, logger log. func (m *HostedGrafanaACHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } m.applyGrafanaRequestIDHeader(ctx, req.PluginContext, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *HostedGrafanaACHeaderMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } m.applyGrafanaRequestIDHeader(ctx, req.PluginContext, req) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *HostedGrafanaACHeaderMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } m.applyGrafanaRequestIDHeader(ctx, req.PluginContext, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go index 3c389edea1d..01e398f4004 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" @@ -26,7 +26,7 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{ @@ -35,7 +35,7 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ URL: "https://logs.grafana.net", @@ -68,14 +68,14 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ URL: "https://logs.not-grafana.net", @@ -93,14 +93,14 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ URL: "https://logs.grafana.net/abc/../some/path", @@ -118,14 +118,14 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ URL: "https://logs.grafana.net", diff --git a/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware.go index 913ad4e0bf5..723c9788944 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware.go @@ -7,26 +7,23 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/plugins" ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ) const forwardPluginRequestHTTPHeaders = "forward-plugin-request-http-headers" -// NewHTTPClientMiddleware creates a new plugins.ClientMiddleware +// NewHTTPClientMiddleware creates a new backend.HandlerMiddleware // that will forward plugin request headers as outgoing HTTP headers. -func NewHTTPClientMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewHTTPClientMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &HTTPClientMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type HTTPClientMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *HTTPClientMiddleware) applyHeaders(ctx context.Context, pReq any) context.Context { @@ -69,30 +66,30 @@ func (m *HTTPClientMiddleware) applyHeaders(ctx context.Context, pReq any) conte func (m *HTTPClientMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } ctx = m.applyHeaders(ctx, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *HTTPClientMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } ctx = m.applyHeaders(ctx, req) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *HTTPClientMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } ctx = m.applyHeaders(ctx, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware_test.go index 58f3faf3c99..a61046b9150 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware_test.go @@ -7,8 +7,8 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/proxyutil" @@ -23,9 +23,9 @@ func TestHTTPClientMiddleware(t *testing.T) { require.NoError(t, err) t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewHTTPClientMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewHTTPClientMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -33,7 +33,7 @@ func TestHTTPClientMiddleware(t *testing.T) { } t.Run("Should not forward headers when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "val"}, }) @@ -53,7 +53,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not forward headers when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"val"}}, }, nopCallResourceSender) @@ -73,7 +73,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not forward headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "val"}, }) @@ -94,9 +94,9 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewHTTPClientMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewHTTPClientMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -104,7 +104,7 @@ func TestHTTPClientMiddleware(t *testing.T) { } t.Run("Should not forward headers when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -124,7 +124,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not forward headers when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -144,7 +144,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not forward headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -184,9 +184,9 @@ func TestHTTPClientMiddleware(t *testing.T) { } t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewHTTPClientMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewHTTPClientMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -194,7 +194,7 @@ func TestHTTPClientMiddleware(t *testing.T) { } t.Run("Should forward headers when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: headers, }) @@ -222,7 +222,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should forward headers when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: crHeaders, }, nopCallResourceSender) @@ -250,7 +250,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should forward headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: headers, }) @@ -278,7 +278,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not overwrite an existing header", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: headers, }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go index 526c13b95fa..ab1eb9f751a 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go @@ -7,21 +7,18 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/instrumentationutils" plog "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) -// NewLoggerMiddleware creates a new plugins.ClientMiddleware that will +// NewLoggerMiddleware creates a new backend.HandlerMiddleware that will // log requests. -func NewLoggerMiddleware(logger plog.Logger, pluginRegistry registry.Service) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewLoggerMiddleware(logger plog.Logger, pluginRegistry registry.Service) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &LoggerMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), logger: logger, pluginRegistry: pluginRegistry, } @@ -29,7 +26,7 @@ func NewLoggerMiddleware(logger plog.Logger, pluginRegistry registry.Service) pl } type LoggerMiddleware struct { - baseMiddleware + backend.BaseHandler logger plog.Logger pluginRegistry registry.Service } @@ -77,13 +74,13 @@ func (m *LoggerMiddleware) logRequest(ctx context.Context, pCtx backend.PluginCo func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } var resp *backend.QueryDataResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.QueryData(ctx, req) + resp, innerErr = m.BaseHandler.QueryData(ctx, req) if innerErr != nil { return instrumentationutils.RequestStatusFromError(innerErr), innerErr @@ -110,11 +107,11 @@ func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryData func (m *LoggerMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { - innerErr := m.next.CallResource(ctx, req, sender) + innerErr := m.BaseHandler.CallResource(ctx, req, sender) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -123,13 +120,13 @@ func (m *LoggerMiddleware) CallResource(ctx context.Context, req *backend.CallRe func (m *LoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } var resp *backend.CheckHealthResult err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.CheckHealth(ctx, req) + resp, innerErr = m.BaseHandler.CheckHealth(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -138,13 +135,13 @@ func (m *LoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHe func (m *LoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { if req == nil { - return m.next.CollectMetrics(ctx, req) + return m.BaseHandler.CollectMetrics(ctx, req) } var resp *backend.CollectMetricsResult err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.CollectMetrics(ctx, req) + resp, innerErr = m.BaseHandler.CollectMetrics(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -153,13 +150,13 @@ func (m *LoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.Coll func (m *LoggerMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { if req == nil { - return m.next.SubscribeStream(ctx, req) + return m.BaseHandler.SubscribeStream(ctx, req) } var resp *backend.SubscribeStreamResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.SubscribeStream(ctx, req) + resp, innerErr = m.BaseHandler.SubscribeStream(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -168,13 +165,13 @@ func (m *LoggerMiddleware) SubscribeStream(ctx context.Context, req *backend.Sub func (m *LoggerMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { if req == nil { - return m.next.PublishStream(ctx, req) + return m.BaseHandler.PublishStream(ctx, req) } var resp *backend.PublishStreamResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.PublishStream(ctx, req) + resp, innerErr = m.BaseHandler.PublishStream(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -183,11 +180,11 @@ func (m *LoggerMiddleware) PublishStream(ctx context.Context, req *backend.Publi func (m *LoggerMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { if req == nil { - return m.next.RunStream(ctx, req, sender) + return m.BaseHandler.RunStream(ctx, req, sender) } err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { - innerErr := m.next.RunStream(ctx, req, sender) + innerErr := m.BaseHandler.RunStream(ctx, req, sender) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -196,13 +193,13 @@ func (m *LoggerMiddleware) RunStream(ctx context.Context, req *backend.RunStream func (m *LoggerMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { if req == nil { - return m.next.ValidateAdmission(ctx, req) + return m.BaseHandler.ValidateAdmission(ctx, req) } var resp *backend.ValidationResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.ValidateAdmission(ctx, req) + resp, innerErr = m.BaseHandler.ValidateAdmission(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -211,13 +208,13 @@ func (m *LoggerMiddleware) ValidateAdmission(ctx context.Context, req *backend.A func (m *LoggerMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { if req == nil { - return m.next.MutateAdmission(ctx, req) + return m.BaseHandler.MutateAdmission(ctx, req) } var resp *backend.MutationResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.MutateAdmission(ctx, req) + resp, innerErr = m.BaseHandler.MutateAdmission(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -226,13 +223,13 @@ func (m *LoggerMiddleware) MutateAdmission(ctx context.Context, req *backend.Adm func (m *LoggerMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { if req == nil { - return m.next.ConvertObjects(ctx, req) + return m.BaseHandler.ConvertObjects(ctx, req) } var resp *backend.ConversionResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.ConvertObjects(ctx, req) + resp, innerErr = m.BaseHandler.ConvertObjects(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go index a93c0a13a2a..29e7b1ec998 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go @@ -25,7 +25,7 @@ type pluginMetrics struct { // MetricsMiddleware is a middleware that instruments plugin requests. // It tracks requests count, duration and size as prometheus metrics. type MetricsMiddleware struct { - baseMiddleware + backend.BaseHandler pluginMetrics pluginRegistry registry.Service } @@ -75,12 +75,10 @@ func newMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry r } // NewMetricsMiddleware returns a new MetricsMiddleware. -func NewMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service) plugins.ClientMiddleware { +func NewMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service) backend.HandlerMiddleware { imw := newMetricsMiddleware(promRegisterer, pluginRegistry) - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - imw.baseMiddleware = baseMiddleware{ - next: next, - } + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { + imw.BaseHandler = backend.NewBaseHandler(next) return imw }) } @@ -154,7 +152,7 @@ func (m *MetricsMiddleware) QueryData(ctx context.Context, req *backend.QueryDat var resp *backend.QueryDataResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.QueryData(ctx, req) + resp, innerErr = m.BaseHandler.QueryData(ctx, req) return instrumentationutils.RequestStatusFromQueryDataResponse(resp, innerErr), innerErr }) @@ -166,7 +164,7 @@ func (m *MetricsMiddleware) CallResource(ctx context.Context, req *backend.CallR return err } return m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { - innerErr := m.next.CallResource(ctx, req, sender) + innerErr := m.BaseHandler.CallResource(ctx, req, sender) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) } @@ -175,7 +173,7 @@ func (m *MetricsMiddleware) CheckHealth(ctx context.Context, req *backend.CheckH var resp *backend.CheckHealthResult err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.CheckHealth(ctx, req) + resp, innerErr = m.BaseHandler.CheckHealth(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -186,7 +184,7 @@ func (m *MetricsMiddleware) CollectMetrics(ctx context.Context, req *backend.Col var resp *backend.CollectMetricsResult err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.CollectMetrics(ctx, req) + resp, innerErr = m.BaseHandler.CollectMetrics(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) return resp, err @@ -196,7 +194,7 @@ func (m *MetricsMiddleware) SubscribeStream(ctx context.Context, req *backend.Su var resp *backend.SubscribeStreamResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.SubscribeStream(ctx, req) + resp, innerErr = m.BaseHandler.SubscribeStream(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) return resp, err @@ -206,7 +204,7 @@ func (m *MetricsMiddleware) PublishStream(ctx context.Context, req *backend.Publ var resp *backend.PublishStreamResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.PublishStream(ctx, req) + resp, innerErr = m.BaseHandler.PublishStream(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) return resp, err @@ -214,7 +212,7 @@ func (m *MetricsMiddleware) PublishStream(ctx context.Context, req *backend.Publ func (m *MetricsMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { - innerErr := m.next.RunStream(ctx, req, sender) + innerErr := m.BaseHandler.RunStream(ctx, req, sender) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) return err @@ -224,7 +222,7 @@ func (m *MetricsMiddleware) ValidateAdmission(ctx context.Context, req *backend. var resp *backend.ValidationResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.ValidateAdmission(ctx, req) + resp, innerErr = m.BaseHandler.ValidateAdmission(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -235,7 +233,7 @@ func (m *MetricsMiddleware) MutateAdmission(ctx context.Context, req *backend.Ad var resp *backend.MutationResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.MutateAdmission(ctx, req) + resp, innerErr = m.BaseHandler.MutateAdmission(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -246,7 +244,7 @@ func (m *MetricsMiddleware) ConvertObjects(ctx context.Context, req *backend.Con var resp *backend.ConversionResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.ConvertObjects(ctx, req) + resp, innerErr = m.BaseHandler.ConvertObjects(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go index 40c26cef09a..1af03e7af79 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" dto "github.com/prometheus/client_model/go" @@ -15,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/instrumentationutils" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) @@ -34,36 +34,36 @@ func TestInstrumentationMiddleware(t *testing.T) { t.Run("should instrument requests", func(t *testing.T) { for _, tc := range []struct { expEndpoint backend.Endpoint - fn func(cdt *clienttest.ClientDecoratorTest) error + fn func(cdt *handlertest.HandlerMiddlewareTest) error shouldInstrumentRequestSize bool }{ { expEndpoint: backend.EndpointCheckHealth, - fn: func(cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.CheckHealth(context.Background(), &backend.CheckHealthRequest{PluginContext: pCtx}) + fn: func(cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.CheckHealth(context.Background(), &backend.CheckHealthRequest{PluginContext: pCtx}) return err }, shouldInstrumentRequestSize: false, }, { expEndpoint: backend.EndpointCallResource, - fn: func(cdt *clienttest.ClientDecoratorTest) error { - return cdt.Decorator.CallResource(context.Background(), &backend.CallResourceRequest{PluginContext: pCtx}, nopCallResourceSender) + fn: func(cdt *handlertest.HandlerMiddlewareTest) error { + return cdt.MiddlewareHandler.CallResource(context.Background(), &backend.CallResourceRequest{PluginContext: pCtx}, nopCallResourceSender) }, shouldInstrumentRequestSize: true, }, { expEndpoint: backend.EndpointQueryData, - fn: func(cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) + fn: func(cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) return err }, shouldInstrumentRequestSize: true, }, { expEndpoint: backend.EndpointCollectMetrics, - fn: func(cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{PluginContext: pCtx}) + fn: func(cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{PluginContext: pCtx}) return err }, shouldInstrumentRequestSize: false, @@ -77,9 +77,9 @@ func TestInstrumentationMiddleware(t *testing.T) { })) mw := newMetricsMiddleware(promRegistry, pluginsRegistry) - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares( - plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - mw.next = next + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares( + backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { + mw.BaseHandler = backend.NewBaseHandler(next) return mw }), )) @@ -154,10 +154,10 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { JSONData: plugins.JSONData{ID: pluginID, Backend: true}, })) metricsMw := newMetricsMiddleware(promRegistry, pluginsRegistry) - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares( + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares( NewPluginRequestMetaMiddleware(), - plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - metricsMw.next = next + backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { + metricsMw.BaseHandler = backend.NewBaseHandler(next) return metricsMw }), NewStatusSourceMiddleware(), @@ -166,10 +166,10 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { t.Run("Metrics", func(t *testing.T) { metricsMw.pluginMetrics.pluginRequestCounter.Reset() - cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + cdt.TestHandler.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { return &backend.QueryDataResponse{Responses: map[string]backend.DataResponse{"A": downstreamErrorResponse}}, nil } - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) require.NoError(t, err) counter, err := metricsMw.pluginMetrics.pluginRequestCounter.GetMetricWith(newLabels( queryDataErrorCounterLabels, @@ -235,12 +235,12 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { cdt.QueryDataCtx = nil cdt.QueryDataReq = nil }) - cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + cdt.TestHandler.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { cdt.QueryDataCtx = ctx cdt.QueryDataReq = req return &backend.QueryDataResponse{Responses: tc.responses}, nil } - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) require.NoError(t, err) ctxStatusSource := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) require.Equal(t, tc.expStatusSource, ctxStatusSource) diff --git a/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware.go index b37a6f88e95..dcf59a50714 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware.go @@ -7,28 +7,25 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/oauthtoken" ) -// NewOAuthTokenMiddleware creates a new plugins.ClientMiddleware that will -// set OAuth token headers on outgoing plugins.Client requests if the +// NewOAuthTokenMiddleware creates a new backend.HandlerMiddleware that will +// set OAuth token headers on outgoing backend.Handler requests if the // datasource has enabled Forward OAuth Identity (oauthPassThru). -func NewOAuthTokenMiddleware(oAuthTokenService oauthtoken.OAuthTokenService) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewOAuthTokenMiddleware(oAuthTokenService oauthtoken.OAuthTokenService) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &OAuthTokenMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), oAuthTokenService: oAuthTokenService, } }) } type OAuthTokenMiddleware struct { - baseMiddleware + backend.BaseHandler oAuthTokenService oauthtoken.OAuthTokenService } @@ -87,7 +84,7 @@ func (m *OAuthTokenMiddleware) applyToken(ctx context.Context, pCtx backend.Plug func (m *OAuthTokenMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } err := m.applyToken(ctx, req.PluginContext, req) @@ -95,12 +92,12 @@ func (m *OAuthTokenMiddleware) QueryData(ctx context.Context, req *backend.Query return nil, err } - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *OAuthTokenMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } err := m.applyToken(ctx, req.PluginContext, req) @@ -108,12 +105,12 @@ func (m *OAuthTokenMiddleware) CallResource(ctx context.Context, req *backend.Ca return err } - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *OAuthTokenMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } err := m.applyToken(ctx, req.PluginContext, req) @@ -121,5 +118,5 @@ func (m *OAuthTokenMiddleware) CheckHealth(ctx context.Context, req *backend.Che return nil, err } - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware_test.go index 95e0a4212e2..abbf7be4813 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" @@ -23,9 +23,9 @@ func TestOAuthTokenMiddleware(t *testing.T) { req.Header.Set(otherHeader, "test") oAuthTokenService := &oauthtokentest.Service{} - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewOAuthTokenMiddleware(oAuthTokenService)), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewOAuthTokenMiddleware(oAuthTokenService)), ) jsonDataMap := map[string]any{} @@ -39,7 +39,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { } t.Run("Should not forward OAuth Identity when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -50,7 +50,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { }) t.Run("Should not forward OAuth Identity when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -61,7 +61,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { }) t.Run("Should not forward OAuth Identity when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -86,9 +86,9 @@ func TestOAuthTokenMiddleware(t *testing.T) { oAuthTokenService := &oauthtokentest.Service{ Token: token, } - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewOAuthTokenMiddleware(oAuthTokenService)), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewOAuthTokenMiddleware(oAuthTokenService)), ) jsonDataMap := map[string]any{ @@ -104,7 +104,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { } t.Run("Should forward OAuth Identity when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -117,7 +117,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { }) t.Run("Should forward OAuth Identity when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -132,7 +132,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { }) t.Run("Should forward OAuth Identity when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go index 6536df9ee12..d24e5dc1472 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go @@ -5,24 +5,23 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) -// NewPluginRequestMetaMiddleware returns a new plugins.ClientMiddleware that sets up the default +// NewPluginRequestMetaMiddleware returns a new backend.HandlerMiddleware that sets up the default // values for the plugin request meta in the context.Context. All middlewares that are executed // after this one are be able to access plugin request meta via the pluginrequestmeta package. -func NewPluginRequestMetaMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewPluginRequestMetaMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &PluginRequestMetaMiddleware{ - next: next, + BaseHandler: backend.NewBaseHandler(next), defaultStatusSource: pluginrequestmeta.DefaultStatusSource, } }) } type PluginRequestMetaMiddleware struct { - next plugins.Client + backend.BaseHandler defaultStatusSource pluginrequestmeta.StatusSource } @@ -35,53 +34,53 @@ func (m *PluginRequestMetaMiddleware) withDefaultPluginRequestMeta(ctx context.C func (m *PluginRequestMetaMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *PluginRequestMetaMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *PluginRequestMetaMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } func (m *PluginRequestMetaMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.CollectMetrics(ctx, req) + return m.BaseHandler.CollectMetrics(ctx, req) } func (m *PluginRequestMetaMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.SubscribeStream(ctx, req) + return m.BaseHandler.SubscribeStream(ctx, req) } func (m *PluginRequestMetaMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.PublishStream(ctx, req) + return m.BaseHandler.PublishStream(ctx, req) } func (m *PluginRequestMetaMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.RunStream(ctx, req, sender) + return m.BaseHandler.RunStream(ctx, req, sender) } // ValidateAdmission implements backend.AdmissionHandler. func (m *PluginRequestMetaMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.ValidateAdmission(ctx, req) + return m.BaseHandler.ValidateAdmission(ctx, req) } // MutateAdmission implements backend.AdmissionHandler. func (m *PluginRequestMetaMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.MutateAdmission(ctx, req) + return m.BaseHandler.MutateAdmission(ctx, req) } // ConvertObject implements backend.AdmissionHandler. func (m *PluginRequestMetaMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.ConvertObjects(ctx, req) + return m.BaseHandler.ConvertObjects(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go index 79adb858313..b87fcfb84d5 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go @@ -5,34 +5,33 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) func TestPluginRequestMetaMiddleware(t *testing.T) { t.Run("default", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithMiddlewares(NewPluginRequestMetaMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + handlertest.WithMiddlewares(NewPluginRequestMetaMiddleware()), ) - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{}) + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) require.NoError(t, err) ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) require.Equal(t, pluginrequestmeta.StatusSourcePlugin, ss) }) t.Run("other value", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithMiddlewares(plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { + cdt := handlertest.NewHandlerMiddlewareTest(t, + handlertest.WithMiddlewares(backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &PluginRequestMetaMiddleware{ - next: next, + BaseHandler: backend.NewBaseHandler(next), defaultStatusSource: "test", } })), ) - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{}) + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) require.NoError(t, err) ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) require.Equal(t, pluginrequestmeta.StatusSource("test"), ss) diff --git a/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware.go deleted file mode 100644 index b000a549656..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware.go +++ /dev/null @@ -1,52 +0,0 @@ -package clientmiddleware - -import ( - "context" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/util/proxyutil" -) - -// NewResourceResponseMiddleware creates a new plugins.ClientMiddleware -// that will enforce HTTP header rules for backend.CallResourceResponse's. -func NewResourceResponseMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - return &ResourceResponseMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, - } - }) -} - -type ResourceResponseMiddleware struct { - baseMiddleware -} - -func (m *ResourceResponseMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - return m.next.QueryData(ctx, req) -} - -func (m *ResourceResponseMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req == nil || sender == nil { - return m.next.CallResource(ctx, req, sender) - } - - processedStreams := 0 - wrappedSender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { - if processedStreams == 0 { - if res.Headers == nil { - res.Headers = map[string][]string{} - } - - proxyutil.SetProxyResponseHeaders(res.Headers) - } - - processedStreams++ - return sender.Send(res) - }) - - return m.next.CallResource(ctx, req, wrappedSender) -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware_test.go deleted file mode 100644 index 905977b32a4..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package clientmiddleware - -import ( - "context" - "net/http" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" - "github.com/stretchr/testify/require" -) - -func TestResourceResponseMiddleware(t *testing.T) { - t.Run("Should set proxy response headers when calling CallResource", func(t *testing.T) { - crResp := &backend.CallResourceResponse{ - Status: http.StatusOK, - Headers: map[string][]string{ - "X-Custom": {"Should not be deleted"}, - }, - } - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithMiddlewares(NewResourceResponseMiddleware()), - clienttest.WithResourceResponses([]*backend.CallResourceResponse{crResp}), - ) - - var sentResponse *backend.CallResourceResponse - sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { - sentResponse = res - return nil - }) - - err := cdt.Decorator.CallResource(context.Background(), &backend.CallResourceRequest{ - PluginContext: backend.PluginContext{}, - }, sender) - require.NoError(t, err) - - require.NotNil(t, sentResponse) - require.Equal(t, "sandbox", sentResponse.Headers["Content-Security-Policy"][0]) - require.Equal(t, "Should not be deleted", sentResponse.Headers["X-Custom"][0]) - }) -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go index 5cd88c03aa6..d9af8df3887 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go @@ -6,30 +6,27 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) -// NewStatusSourceMiddleware returns a new plugins.ClientMiddleware that sets the status source in the +// NewStatusSourceMiddleware returns a new backend.HandlerMiddleware that sets the status source in the // plugin request meta stored in the context.Context, according to the query data responses returned by QueryError. // If at least one query data response has a "downstream" status source and there isn't one with a "plugin" status source, // the plugin request meta in the context is set to "downstream". -func NewStatusSourceMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewStatusSourceMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &StatusSourceMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type StatusSourceMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *StatusSourceMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - resp, err := m.next.QueryData(ctx, req) + resp, err := m.BaseHandler.QueryData(ctx, req) if resp == nil || len(resp.Responses) == 0 { return resp, err } diff --git a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go index 88d5e7f0534..c3544d4e9df 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) @@ -68,18 +68,18 @@ func TestStatusSourceMiddleware(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithMiddlewares( + cdt := handlertest.NewHandlerMiddlewareTest(t, + handlertest.WithMiddlewares( NewPluginRequestMetaMiddleware(), NewStatusSourceMiddleware(), ), ) - cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + cdt.TestHandler.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { cdt.QueryDataCtx = ctx return tc.queryDataResponse, nil } - _, _ = cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{}) + _, _ = cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) require.Equal(t, tc.expStatusSource, ss) diff --git a/pkg/services/pluginsintegration/clientmiddleware/testing.go b/pkg/services/pluginsintegration/clientmiddleware/testing.go index 391783cc0ef..0409fe9333b 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/testing.go +++ b/pkg/services/pluginsintegration/clientmiddleware/testing.go @@ -1,6 +1,31 @@ package clientmiddleware -import "github.com/grafana/grafana-plugin-sdk-go/backend" +import ( + "net/http" + "net/http/httptest" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" + "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/web" +) + +func WithReqContext(req *http.Request, user *user.SignedInUser) handlertest.HandlerMiddlewareTestOption { + return handlertest.HandlerMiddlewareTestOption(func(cdt *handlertest.HandlerMiddlewareTest) { + reqContext := &contextmodel.ReqContext{ + Context: &web.Context{ + Req: req, + Resp: web.NewResponseWriter(req.Method, httptest.NewRecorder()), + }, + SignedInUser: user, + } + + ctx := ctxkey.Set(req.Context(), reqContext) + *req = *req.WithContext(ctx) + }) +} var nopCallResourceSender = backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { return nil diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go index 30f9b47033a..035ae7f48cb 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go @@ -5,28 +5,25 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/query" ) -// NewTracingHeaderMiddleware creates a new plugins.ClientMiddleware that will -// populate useful tracing headers on outgoing plugins.Client and HTTP +// NewTracingHeaderMiddleware creates a new backend.HandlerMiddleware that will +// populate useful tracing headers on outgoing backend.Handler and HTTP // requests. // Tracing headers are X-Datasource-Uid, X-Dashboard-Uid, // X-Panel-Id, X-Grafana-Org-Id. -func NewTracingHeaderMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewTracingHeaderMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &TracingHeaderMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type TracingHeaderMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *TracingHeaderMiddleware) applyHeaders(ctx context.Context, req backend.ForwardHTTPHeaders) { @@ -49,22 +46,22 @@ func (m *TracingHeaderMiddleware) applyHeaders(ctx context.Context, req backend. func (m *TracingHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } m.applyHeaders(ctx, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *TracingHeaderMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *TracingHeaderMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } m.applyHeaders(ctx, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go index b190dcc86fa..7b1df5a8065 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" ) @@ -25,15 +25,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { } t.Run("tracing headers are not set for query data", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -43,15 +43,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { }) t.Run("tracing headers are not set for health check", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -69,15 +69,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { } t.Run("tracing headers are not set for query data", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -87,15 +87,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { }) t.Run("tracing headers are not set for health check", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -120,15 +120,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { } t.Run("tracing headers are set for query data", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -144,15 +144,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { }) t.Run("tracing headers are set for health check", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware.go index d34d1635053..a4250de0047 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware.go @@ -11,24 +11,23 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/query" ) // NewTracingMiddleware returns a new middleware that creates a new span on every method call. -func NewTracingMiddleware(tracer tracing.Tracer) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewTracingMiddleware(tracer tracing.Tracer) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &TracingMiddleware{ - tracer: tracer, - next: next, + tracer: tracer, + BaseHandler: backend.NewBaseHandler(next), } }) } type TracingMiddleware struct { + backend.BaseHandler tracer tracing.Tracer - next plugins.Client } // setSpanAttributeFromHTTPHeader takes a ReqContext and a span, and adds the specified HTTP header as a span attribute @@ -84,7 +83,7 @@ func (m *TracingMiddleware) QueryData(ctx context.Context, req *backend.QueryDat var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.QueryData(ctx, req) + resp, err := m.BaseHandler.QueryData(ctx, req) return resp, err } @@ -92,7 +91,7 @@ func (m *TracingMiddleware) CallResource(ctx context.Context, req *backend.CallR var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - err = m.next.CallResource(ctx, req, sender) + err = m.BaseHandler.CallResource(ctx, req, sender) return err } @@ -100,7 +99,7 @@ func (m *TracingMiddleware) CheckHealth(ctx context.Context, req *backend.CheckH var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.CheckHealth(ctx, req) + resp, err := m.BaseHandler.CheckHealth(ctx, req) return resp, err } @@ -108,7 +107,7 @@ func (m *TracingMiddleware) CollectMetrics(ctx context.Context, req *backend.Col var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.CollectMetrics(ctx, req) + resp, err := m.BaseHandler.CollectMetrics(ctx, req) return resp, err } @@ -116,7 +115,7 @@ func (m *TracingMiddleware) SubscribeStream(ctx context.Context, req *backend.Su var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.SubscribeStream(ctx, req) + resp, err := m.BaseHandler.SubscribeStream(ctx, req) return resp, err } @@ -124,7 +123,7 @@ func (m *TracingMiddleware) PublishStream(ctx context.Context, req *backend.Publ var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.PublishStream(ctx, req) + resp, err := m.BaseHandler.PublishStream(ctx, req) return resp, err } @@ -132,7 +131,7 @@ func (m *TracingMiddleware) RunStream(ctx context.Context, req *backend.RunStrea var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - err = m.next.RunStream(ctx, req, sender) + err = m.BaseHandler.RunStream(ctx, req, sender) return err } @@ -141,7 +140,7 @@ func (m *TracingMiddleware) ValidateAdmission(ctx context.Context, req *backend. var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.ValidateAdmission(ctx, req) + resp, err := m.BaseHandler.ValidateAdmission(ctx, req) return resp, err } @@ -150,7 +149,7 @@ func (m *TracingMiddleware) MutateAdmission(ctx context.Context, req *backend.Ad var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.MutateAdmission(ctx, req) + resp, err := m.BaseHandler.MutateAdmission(ctx, req) return resp, err } @@ -159,6 +158,6 @@ func (m *TracingMiddleware) ConvertObjects(ctx context.Context, req *backend.Con var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.ConvertObjects(ctx, req) + resp, err := m.BaseHandler.ConvertObjects(ctx, req) return resp, err } diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware_test.go index 249a66bd94f..78441fa5b8d 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" @@ -16,8 +17,6 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.17.0" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" @@ -30,13 +29,13 @@ func TestTracingMiddleware(t *testing.T) { for _, tc := range []struct { name string - run func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error + run func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error expSpanName string }{ { name: "QueryData", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: pluginCtx, }) return err @@ -45,8 +44,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "CallResource", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - return cdt.Decorator.CallResource(context.Background(), &backend.CallResourceRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + return cdt.MiddlewareHandler.CallResource(context.Background(), &backend.CallResourceRequest{ PluginContext: pluginCtx, }, nopCallResourceSender) }, @@ -54,8 +53,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "CheckHealth", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, }) return err @@ -64,8 +63,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "CollectMetrics", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{ PluginContext: pluginCtx, }) return err @@ -74,8 +73,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "SubscribeStream", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.SubscribeStream(context.Background(), &backend.SubscribeStreamRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.SubscribeStream(context.Background(), &backend.SubscribeStreamRequest{ PluginContext: pluginCtx, }) return err @@ -84,8 +83,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "PublishStream", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.PublishStream(context.Background(), &backend.PublishStreamRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.PublishStream(context.Background(), &backend.PublishStreamRequest{ PluginContext: pluginCtx, }) return err @@ -94,8 +93,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "RunStream", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - return cdt.Decorator.RunStream(context.Background(), &backend.RunStreamRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + return cdt.MiddlewareHandler.RunStream(context.Background(), &backend.RunStreamRequest{ PluginContext: pluginCtx, }, &backend.StreamSender{}) }, @@ -107,9 +106,9 @@ func TestTracingMiddleware(t *testing.T) { spanRecorder := tracetest.NewSpanRecorder() tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) - cdt := clienttest.NewClientDecoratorTest( + cdt := handlertest.NewHandlerMiddlewareTest( t, - clienttest.WithMiddlewares(NewTracingMiddleware(tracer)), + handlertest.WithMiddlewares(NewTracingMiddleware(tracer)), ) err := tc.run(pluginCtx, cdt) @@ -126,9 +125,9 @@ func TestTracingMiddleware(t *testing.T) { spanRecorder := tracetest.NewSpanRecorder() tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) - cdt := clienttest.NewClientDecoratorTest( + cdt := handlertest.NewHandlerMiddlewareTest( t, - clienttest.WithMiddlewares( + handlertest.WithMiddlewares( NewTracingMiddleware(tracer), newAlwaysErrorMiddleware(errors.New("ops")), ), @@ -150,9 +149,9 @@ func TestTracingMiddleware(t *testing.T) { spanRecorder := tracetest.NewSpanRecorder() tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) - cdt := clienttest.NewClientDecoratorTest( + cdt := handlertest.NewHandlerMiddlewareTest( t, - clienttest.WithMiddlewares( + handlertest.WithMiddlewares( NewTracingMiddleware(tracer), newAlwaysPanicMiddleware("panic!"), ), @@ -314,12 +313,12 @@ func TestTracingMiddlewareAttributes(t *testing.T) { spanRecorder := tracetest.NewSpanRecorder() tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) - cdt := clienttest.NewClientDecoratorTest( + cdt := handlertest.NewHandlerMiddlewareTest( t, - clienttest.WithMiddlewares(NewTracingMiddleware(tracer)), + handlertest.WithMiddlewares(NewTracingMiddleware(tracer)), ) - _, err := cdt.Decorator.QueryData(ctx, req) + _, err := cdt.MiddlewareHandler.QueryData(ctx, req) require.NoError(t, err) spans := spanRecorder.Ended() require.Len(t, spans, 1, "must have 1 span") @@ -403,8 +402,8 @@ func (m *alwaysErrorFuncMiddleware) ConvertObjects(ctx context.Context, req *bac } // newAlwaysErrorMiddleware returns a new middleware that always returns the specified error. -func newAlwaysErrorMiddleware(err error) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func newAlwaysErrorMiddleware(err error) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &alwaysErrorFuncMiddleware{func() error { return err }} @@ -412,8 +411,8 @@ func newAlwaysErrorMiddleware(err error) plugins.ClientMiddleware { } // newAlwaysPanicMiddleware returns a new middleware that always panics with the specified message, -func newAlwaysPanicMiddleware(message string) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func newAlwaysPanicMiddleware(message string) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &alwaysErrorFuncMiddleware{func() error { panic(message) }} diff --git a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go index 214b1305979..c3dbde59567 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go @@ -6,25 +6,22 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/util/proxyutil" ) -// NewUserHeaderMiddleware creates a new plugins.ClientMiddleware that will -// populate the X-Grafana-User header on outgoing plugins.Client requests. -func NewUserHeaderMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +// NewUserHeaderMiddleware creates a new backend.HandlerMiddleware that will +// populate the X-Grafana-User header on outgoing backend.Handler requests. +func NewUserHeaderMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &UserHeaderMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type UserHeaderMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *UserHeaderMiddleware) applyUserHeader(ctx context.Context, h backend.ForwardHTTPHeaders) { @@ -42,30 +39,30 @@ func (m *UserHeaderMiddleware) applyUserHeader(ctx context.Context, h backend.Fo func (m *UserHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } m.applyUserHeader(ctx, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *UserHeaderMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } m.applyUserHeader(ctx, req) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *UserHeaderMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } m.applyUserHeader(ctx, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go index 9fedd33a0e2..327705bd1f6 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/stretchr/testify/require" @@ -17,12 +17,12 @@ func TestUserHeaderMiddleware(t *testing.T) { require.NoError(t, err) t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + handlertest.WithMiddlewares(NewUserHeaderMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -30,7 +30,7 @@ func TestUserHeaderMiddleware(t *testing.T) { } t.Run("Should not forward user header when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -40,7 +40,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should not forward user header when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -50,7 +50,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should not forward user header when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -61,12 +61,12 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + handlertest.WithMiddlewares(NewUserHeaderMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -74,7 +74,7 @@ func TestUserHeaderMiddleware(t *testing.T) { } t.Run("Should not forward user header when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -84,7 +84,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should not forward user header when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -94,7 +94,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should not forward user header when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -110,11 +110,11 @@ func TestUserHeaderMiddleware(t *testing.T) { require.NoError(t, err) t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ Login: "admin", }), - clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + handlertest.WithMiddlewares(NewUserHeaderMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -122,7 +122,7 @@ func TestUserHeaderMiddleware(t *testing.T) { } t.Run("Should forward user header when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -133,7 +133,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should forward user header when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -144,7 +144,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should forward user header when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -156,11 +156,11 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ Login: "admin", }), - clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + handlertest.WithMiddlewares(NewUserHeaderMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -168,7 +168,7 @@ func TestUserHeaderMiddleware(t *testing.T) { } t.Run("Should forward user header when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -179,7 +179,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should forward user header when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -190,7 +190,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should forward user header when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index a699982efd8..36c20fa8cbc 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -3,6 +3,7 @@ package pluginsintegration import ( "github.com/google/wire" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/prometheus/client_golang/prometheus" "github.com/grafana/grafana/pkg/infra/tracing" @@ -139,13 +140,13 @@ var WireExtensionSet = wire.NewSet( wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), finder.ProvideLocalFinder, wire.Bind(new(finder.Finder), new(*finder.Local)), - ProvideClientDecorator, - wire.Bind(new(plugins.Client), new(*client.Decorator)), + ProvideClientWithMiddlewares, + wire.Bind(new(plugins.Client), new(*backend.MiddlewareHandler)), managedplugins.NewNoop, wire.Bind(new(managedplugins.Manager), new(*managedplugins.Noop)), ) -func ProvideClientDecorator( +func ProvideClientWithMiddlewares( cfg *setting.Cfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, @@ -153,23 +154,23 @@ func ProvideClientDecorator( cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, -) (*client.Decorator, error) { - return NewClientDecorator(cfg, pluginRegistry, oAuthTokenService, tracer, cachingService, features, promRegisterer, pluginRegistry) +) (*backend.MiddlewareHandler, error) { + return NewMiddlewareHandler(cfg, pluginRegistry, oAuthTokenService, tracer, cachingService, features, promRegisterer, pluginRegistry) } -func NewClientDecorator( +func NewMiddlewareHandler( cfg *setting.Cfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service, -) (*client.Decorator, error) { +) (*backend.MiddlewareHandler, error) { c := client.ProvideService(pluginRegistry) middlewares := CreateMiddlewares(cfg, oAuthTokenService, tracer, cachingService, features, promRegisterer, registry) - return client.NewDecorator(c, middlewares...) + return backend.HandlerFromMiddlewares(c, middlewares...) } -func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service) []plugins.ClientMiddleware { - middlewares := []plugins.ClientMiddleware{ +func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service) []backend.HandlerMiddleware { + middlewares := []backend.HandlerMiddleware{ clientmiddleware.NewPluginRequestMetaMiddleware(), clientmiddleware.NewTracingMiddleware(tracer), clientmiddleware.NewMetricsMiddleware(promRegisterer, registry), @@ -187,7 +188,6 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken clientmiddleware.NewClearAuthHeadersMiddleware(), clientmiddleware.NewOAuthTokenMiddleware(oAuthTokenService), clientmiddleware.NewCookiesMiddleware(skipCookiesNames), - clientmiddleware.NewResourceResponseMiddleware(), clientmiddleware.NewCachingMiddlewareWithFeatureManager(cachingService, features), clientmiddleware.NewForwardIDMiddleware(), ) From bb41ff267bdf9d050726a62af5bde36f75874ec7 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:41:44 +0200 Subject: [PATCH 083/174] Alerting docs: updates to default and advanced options (#93999) * Alerting docs: updates to default and advanced options * feedback sonia --- .../create-grafana-managed-rule.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 2491890306b..33d5b94b1df 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -86,23 +86,23 @@ Multiple alert instances can be created as a result of one alert rule (also know For Grafana Cloud Free Forever, you can create up to 100 free Grafana-managed alert rules with each alert rule having a maximum of 1000 alert instances. {{% /admonition %}} -Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. +Grafana-managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. -If you delete an alerting resource created in the UI, you can no longer retrieve it. +If you delete an alert resource created in the UI, you can no longer retrieve it. To make a backup of your configuration and to be able to restore deleted alerting resources, create your alerting resources using file provisioning, Terraform, or the Alerting API. ## Before you begin -You can use Simple or Advanced modes for Grafana-managed alert rule creation. The Simple mode streamlines rule creation with a cleaner header and a single query and condition. For more complex rules, switch to Advanced mode to add multiple queries and expressions. +You can use default or advanced options for Grafana-managed alert rule creation. The default options streamline rule creation with a cleaner header and a single query and condition. For more complex rules, use advanced options to add multiple queries and expressions. -Simple and advanced modes are enabled by default for Grafana Cloud users and this feature is being rolled out progressively. +Default and advanced options are enabled by default for Grafana Cloud users and this feature is being rolled out progressively. For OSS users,enable the `alertingQueryAndExpressionsStepMode` feature toggle. {{% admonition type="note" %}} -Once you have created an alert rule using one of the modes, the system defaults to this mode for the next alert rule you create. +Once you have created an alert rule using one of the options, the system defaults to this option for the next alert rule you create. -You can toggle between the two options. However, if you want to switch from Advanced to Simple mode and you have more than one query or a custom recovery threshold, your data is removed as it's not possible to transform these queries and expressions to the Simple mode. +You can toggle between the two options. However, if you want to switch from advanced options to the default, it may be that your query and expressions cannot be converted. In this case, a warning message checks whether you want to continue to reset to default settings. {{% /admonition %}} ## Steps @@ -122,7 +122,7 @@ To get started quickly, refer to our [tutorial on getting started with Grafana a Define a query to get the data you want to measure and a condition that needs to be met before an alert rule fires. -{{< collapse title="Simple mode" >}} +{{< collapse title="Default options" >}} 1. Add a query. 1. Add an alert condition. @@ -132,7 +132,7 @@ Define a query to get the data you want to measure and a condition that needs to 1. Click **Preview** to verify. {{< /collapse >}} -{{< collapse title="Advanced mode" >}} +{{< collapse title="Advanced options" >}} 1. Select a data source. 1. From the **Options** dropdown, specify a [time range](ref:time-units-and-relative-ranges). From 0c22aac7f0aaebbca376df00ab76c55ddd3418ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 30 Sep 2024 16:46:43 +0200 Subject: [PATCH 084/174] Dashboards: Add support for systemPanelFilterVar and systemDynamicRowSizeVar variables in scenes (#93670) Co-authored-by: kay delaney --- .betterer.results | 3 + .../dashboard-scene/scene/DashboardScene.tsx | 34 +++++++++ .../scene/DashboardSceneRenderer.tsx | 9 ++- .../scene/PanelSearchLayout.tsx | 73 +++++++++++++++++++ public/locales/en-US/grafana.json | 3 + public/locales/pseudo-LOCALE/grafana.json | 3 + 6 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx diff --git a/.betterer.results b/.betterer.results index 0f98445256b..da8a4d5f5fc 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2774,6 +2774,9 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index da51aa2499a..205f391c2e7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -71,6 +71,8 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana import { DashboardLayoutManager } from './types'; export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta', 'preload']; +export const PANEL_SEARCH_VAR = 'systemPanelFilterVar'; +export const PANELS_PER_ROW_VAR = 'systemDynamicRowSizeVar'; export interface DashboardSceneState extends SceneObjectState { /** The title */ @@ -119,6 +121,10 @@ export interface DashboardSceneState extends SceneObjectState { kioskMode?: KioskMode; /** Share view */ shareView?: string; + /** Renders panels in grid and filtered */ + panelSearch?: string; + /** How many panels to show per row for search results */ + panelsPerRow?: number; } export class DashboardScene extends SceneObjectBase { @@ -188,6 +194,8 @@ export class DashboardScene extends SceneObjectBase { window.__grafanaSceneContext = this; + this._initializePanelSearch(); + if (this.state.isEditing) { this._initialUrlState = locationService.getLocation(); this._changeTracker.startTrackingChanges(); @@ -221,6 +229,19 @@ export class DashboardScene extends SceneObjectBase { }; } + private _initializePanelSearch() { + const systemPanelFilter = sceneGraph.lookupVariable(PANEL_SEARCH_VAR, this)?.getValue(); + if (typeof systemPanelFilter === 'string') { + this.setState({ panelSearch: systemPanelFilter }); + } + + const panelsPerRow = sceneGraph.lookupVariable(PANELS_PER_ROW_VAR, this)?.getValue(); + if (typeof panelsPerRow === 'string') { + const perRow = Number.parseInt(panelsPerRow, 10); + this.setState({ panelsPerRow: Number.isInteger(perRow) ? perRow : undefined }); + } + } + public onEnterEditMode = (fromExplore = false) => { this._fromExplore = fromExplore; // Save this state @@ -674,6 +695,19 @@ export class DashboardVariableDependency implements SceneVariableDependencyConfi appEvents.publish(new VariablesChanged({ refreshAll: true, panelIds: [] })); } + if (variable.state.name === PANEL_SEARCH_VAR) { + const searchValue = variable.getValue(); + if (typeof searchValue === 'string') { + this._dashboard.setState({ panelSearch: searchValue }); + } + } else if (variable.state.name === PANELS_PER_ROW_VAR) { + const panelsPerRow = variable.getValue(); + if (typeof panelsPerRow === 'string') { + const perRow = Number.parseInt(panelsPerRow, 10); + this._dashboard.setState({ panelsPerRow: Number.isInteger(perRow) ? perRow : undefined }); + } + } + /** * Propagate variable changes to repeat row behavior as it does not get it when it's nested under local value * The first repeated row has the row repeater behavior but it also has a local SceneVariableSet with a local variable value diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 49571a3ffa8..ae71d1e7d16 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -15,9 +15,11 @@ import { useSelector } from 'app/types'; import { DashboardScene } from './DashboardScene'; import { NavToolbarActions } from './NavToolbarActions'; +import { PanelSearchLayout } from './PanelSearchLayout'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene } = model.useState(); + const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } = + model.useState(); const headerHeight = useChromeHeaderHeight(); const styles = useStyles2(getStyles, headerHeight); const location = useLocation(); @@ -63,13 +65,16 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps; - let body = [withPanels]; + let body: React.ReactNode = [withPanels]; if (notFound) { body = [notFound]; } else if (isEmpty) { body = [emptyState, withPanels]; + } else if (panelSearch || panelsPerRow) { + body = ; } + return ( {editPanel && } diff --git a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx new file mode 100644 index 00000000000..343ccef5d8d --- /dev/null +++ b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx @@ -0,0 +1,73 @@ +import { css } from '@emotion/css'; +import classNames from 'classnames'; +import { useEffect } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { SceneGridLayout, VizPanel, sceneGraph } from '@grafana/scenes'; +import { useStyles2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +import { activateInActiveParents } from '../utils/utils'; + +import { DashboardGridItem } from './DashboardGridItem'; +import { DashboardScene } from './DashboardScene'; + +export interface Props { + dashboard: DashboardScene; + panelSearch?: string; + panelsPerRow?: number; +} + +const panelsPerRowCSSVar = '--panels-per-row'; + +export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: Props) { + const { body } = dashboard.state; + const panels: VizPanel[] = []; + const styles = useStyles2(getStyles); + + if (!(body instanceof SceneGridLayout)) { + return Unsupported layout; + } + + for (const gridItem of body.state.children) { + if (gridItem instanceof DashboardGridItem) { + const panel = gridItem.state.body; + const interpolatedTitle = sceneGraph.interpolate(dashboard, panel.state.title).toLowerCase(); + const interpolatedSearchString = sceneGraph.interpolate(dashboard, panelSearch).toLowerCase(); + if (interpolatedTitle.includes(interpolatedSearchString)) { + panels.push(gridItem.state.body); + } + } + } + + return ( +
} + > + {panels.map((panel) => ( + + ))} +
+ ); +} + +function PanelSearchHit({ panel }: { panel: VizPanel }) { + useEffect(() => activateInActiveParents(panel), [panel]); + + return ; +} + +function getStyles(theme: GrafanaTheme2) { + return { + grid: css({ + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', + gap: theme.spacing(1), + gridAutoRows: '320px', + }), + perRow: css({ + gridTemplateColumns: `repeat(var(${panelsPerRowCSSVar}, 3), 1fr)`, + }), + }; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7e370221b5d..edc47d01564 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1900,6 +1900,9 @@ "view": "View" } }, + "panel-search": { + "unsupported-layout": "Unsupported layout" + }, "playlist-edit": { "error-prefix": "Error loading playlist:", "form": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index dad1f0097d0..a1be240c152 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1900,6 +1900,9 @@ "view": "Vįęŵ" } }, + "panel-search": { + "unsupported-layout": "Ůʼnşūppőřŧęđ ľäyőūŧ" + }, "playlist-edit": { "error-prefix": "Ēřřőř ľőäđįʼnģ pľäyľįşŧ:", "form": { From a1cedb416054651fc8c0dd069d9d1fec59bbb380 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 30 Sep 2024 17:05:01 +0200 Subject: [PATCH 085/174] Auto triager: Update labels for dashboards squad (#93989) --- .github/commands.json | 232 ++++++++++++++++++++++++++++++------------ 1 file changed, 168 insertions(+), 64 deletions(-) diff --git a/.github/commands.json b/.github/commands.json index fdf90eb4722..af047be4341 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -475,14 +475,6 @@ "url": "https://github.com/orgs/grafana/projects/665" } }, - { - "type": "label", - "name": "area/dashboard", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "type/build-packaging", @@ -507,14 +499,6 @@ "url": "https://github.com/orgs/grafana/projects/56" } }, - { - "type": "label", - "name": "area/dashboard/templating", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "area/auth", @@ -579,14 +563,6 @@ "url": "https://github.com/orgs/grafana/projects/203" } }, - { - "type": "label", - "name": "area/dashboards/panel", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "area/public-dashboards", @@ -635,14 +611,6 @@ "url": "https://github.com/orgs/grafana/projects/112" } }, - { - "type": "label", - "name": "area/dashboard/data-links", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "area/backend/api", @@ -675,22 +643,6 @@ "url": "https://github.com/orgs/grafana/projects/221" } }, - { - "type": "label", - "name": "area/dashboard/snapshot", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, - { - "type": "label", - "name": "area/dashboard/folders", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "area/configuration", @@ -715,14 +667,6 @@ "url": "https://github.com/orgs/grafana/projects/660" } }, - { - "type": "label", - "name": "area/dashboard/timerange", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "datasource/MSSQL", @@ -827,6 +771,174 @@ "url": "https://github.com/orgs/grafana/projects/202" } }, + { + "type": "label", + "name": "area/dashboard/links", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/templating", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/variables", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboards/panel", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/data-links", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/rows", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/edit", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/panel/edit", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/scenes", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/snapshot", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/folders", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/timerange", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/repeating", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/annotations", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/library-panel", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/kiosk", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/tv", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/import", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/panel/field-override", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/panel/repeat", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/settings", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, { "type": "label", "name": "area/backend/db/sqlite", @@ -859,14 +971,6 @@ "url": "https://github.com/orgs/grafana/projects/78" } }, - { - "type": "label", - "name": "area/dashboard/links", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "datasource/Zipkin", From 8b215d60acac86fb6ec0588d261e444f439b88be Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:25:58 +0200 Subject: [PATCH 086/174] Dashboard Scene: Add sceneGraph missing dependency (#94014) Add sceneGraph missing dependency --- public/app/features/dashboard-scene/scene/DashboardScene.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 205f391c2e7..cad6daa473a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -12,6 +12,7 @@ import { } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { + sceneGraph, SceneGridRow, SceneObject, SceneObjectBase, From 362b5a1c22990568a3ab4be759510163f00bf37b Mon Sep 17 00:00:00 2001 From: Steven Dungan <114922977+stevendungan@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:46:21 -0400 Subject: [PATCH 087/174] Docs - direction param for Loki in Explore (#91905) Co-authored-by: Jack Baldry --- docs/sources/datasources/loki/query-editor/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/datasources/loki/query-editor/index.md b/docs/sources/datasources/loki/query-editor/index.md index 3ff47a25215..37750aadd10 100644 --- a/docs/sources/datasources/loki/query-editor/index.md +++ b/docs/sources/datasources/loki/query-editor/index.md @@ -171,6 +171,8 @@ The following options are the same for both **Builder** and **Code** mode: - **Line limit** -Defines the upper limit for the number of log lines returned by a query. The default is `1000` +- **Direction** - Determines the search order. **Backward** is a backward search starting at the end of the time range. **Forward** is a forward search starting at the beginning of the time range. The default is **Backward** + - **Step** Sets the step parameter of Loki metrics queries. The default value equals to the value of `$__interval` variable, which is calculated using the time range and the width of the graph (the number of pixels). - **Resolution** Deprecated. Sets the step parameter of Loki metrics range queries. With a resolution of `1/1`, each pixel corresponds to one data point. `1/2` retrieves one data point for every other pixel, `1/10` retrieves one data point per 10 pixels, and so on. Lower resolutions perform better. From 7bb3fe3da1339ece7d4ce3dd79ebbe05ca0321f0 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Mon, 30 Sep 2024 17:54:12 +0200 Subject: [PATCH 088/174] CloudMigrations: Remove unused code from sync migration approach (#94016) --- .../cloudmigration/gmsclient/client.go | 1 - .../cloudmigration/gmsclient/gms_client.go | 80 ------------------- 2 files changed, 81 deletions(-) diff --git a/pkg/services/cloudmigration/gmsclient/client.go b/pkg/services/cloudmigration/gmsclient/client.go index d15b03a9e95..91a7f9c4eab 100644 --- a/pkg/services/cloudmigration/gmsclient/client.go +++ b/pkg/services/cloudmigration/gmsclient/client.go @@ -8,7 +8,6 @@ import ( type Client interface { ValidateKey(context.Context, cloudmigration.CloudMigrationSession) error - MigrateData(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.MigrateDataRequest) (*cloudmigration.MigrateDataResponse, error) StartSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error) GetSnapshotStatus(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot, int) (*cloudmigration.GetSnapshotStatusResponse, error) CreatePresignedUploadUrl(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot) (string, error) diff --git a/pkg/services/cloudmigration/gmsclient/gms_client.go b/pkg/services/cloudmigration/gmsclient/gms_client.go index 935ee003e53..9cbe60d97e3 100644 --- a/pkg/services/cloudmigration/gmsclient/gms_client.go +++ b/pkg/services/cloudmigration/gmsclient/gms_client.go @@ -71,52 +71,6 @@ func (c *gmsClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.Cloud return nil } -// Deprecated -func (c *gmsClientImpl) MigrateData(ctx context.Context, cm cloudmigration.CloudMigrationSession, request cloudmigration.MigrateDataRequest) (result *cloudmigration.MigrateDataResponse, err error) { - path := fmt.Sprintf("%s/api/v1/migrate-data", c.buildBasePath(cm.ClusterSlug)) - - reqDTO := convertRequestToDTO(request) - body, err := json.Marshal(reqDTO) - if err != nil { - return nil, fmt.Errorf("error marshaling request: %w", err) - } - - // Send the request to GMS with the associated auth token - req, err := http.NewRequest(http.MethodPost, path, bytes.NewReader(body)) - if err != nil { - c.log.Error("error creating http request for cloud migration run", "err", err.Error()) - return nil, fmt.Errorf("http request error: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %d:%s", cm.StackID, cm.AuthToken)) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - c.log.Error("error sending http request for cloud migration run", "err", err.Error()) - return nil, fmt.Errorf("http request error: %w", err) - } - defer func() { - if closeErr := resp.Body.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing response body: %w", closeErr)) - } - }() - - if resp.StatusCode >= 400 { - c.log.Error("received error response for cloud migration run", "statusCode", resp.StatusCode) - return nil, fmt.Errorf("http request error: %w", err) - } - - var respDTO MigrateDataResponseDTO - if err := json.NewDecoder(resp.Body).Decode(&respDTO); err != nil { - c.log.Error("unmarshalling response body", "err", err.Error()) - return nil, fmt.Errorf("unmarshalling migration run response: %w", err) - } - - res := convertResponseFromDTO(respDTO) - return &res, nil -} - func (c *gmsClientImpl) StartSnapshot(ctx context.Context, session cloudmigration.CloudMigrationSession) (out *cloudmigration.StartSnapshotResponse, err error) { path := fmt.Sprintf("%s/api/v1/start-snapshot", c.buildBasePath(session.ClusterSlug)) @@ -302,37 +256,3 @@ func (c *gmsClientImpl) buildBasePath(clusterSlug string) string { } return fmt.Sprintf("https://cms-%s.%s/cloud-migrations", clusterSlug, domain) } - -func convertRequestToDTO(request cloudmigration.MigrateDataRequest) MigrateDataRequestDTO { - items := make([]MigrateDataRequestItemDTO, len(request.Items)) - for i := 0; i < len(request.Items); i++ { - item := request.Items[i] - items[i] = MigrateDataRequestItemDTO{ - Type: MigrateDataType(item.Type), - RefID: item.RefID, - Name: item.Name, - Data: item.Data, - } - } - r := MigrateDataRequestDTO{ - Items: items, - } - return r -} - -func convertResponseFromDTO(result MigrateDataResponseDTO) cloudmigration.MigrateDataResponse { - items := make([]cloudmigration.CloudMigrationResource, len(result.Items)) - for i := 0; i < len(result.Items); i++ { - item := result.Items[i] - items[i] = cloudmigration.CloudMigrationResource{ - Type: cloudmigration.MigrateDataType(item.Type), - RefID: item.RefID, - Status: cloudmigration.ItemStatus(item.Status), - Error: item.Error, - } - } - return cloudmigration.MigrateDataResponse{ - RunUID: result.RunUID, - Items: items, - } -} From fcbaf188c2146f26d8f5134600912533e23291c5 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:19:54 +0100 Subject: [PATCH 089/174] I18n: Download translations from Crowdin (#94013) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 3 +++ public/locales/es-ES/grafana.json | 3 +++ public/locales/fr-FR/grafana.json | 3 +++ public/locales/pt-BR/grafana.json | 3 +++ public/locales/zh-Hans/grafana.json | 3 +++ 5 files changed, 15 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 20f666b7c50..cd5e88fc6cc 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1900,6 +1900,9 @@ "view": "Anzeigen" } }, + "panel-search": { + "unsupported-layout": "" + }, "playlist-edit": { "error-prefix": "Fehler beim Laden der Wiedergabeliste:", "form": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 3bbdc263417..325dfe0ccc6 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1900,6 +1900,9 @@ "view": "Vista" } }, + "panel-search": { + "unsupported-layout": "" + }, "playlist-edit": { "error-prefix": "Error al cargar la lista de reproducción:", "form": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index e7e2be6f6bd..cb2c93d379e 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1900,6 +1900,9 @@ "view": "Afficher" } }, + "panel-search": { + "unsupported-layout": "" + }, "playlist-edit": { "error-prefix": "Erreur lors du chargement de la playlist :", "form": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 791b5b97fa9..cc230ebd186 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1900,6 +1900,9 @@ "view": "Visualizar" } }, + "panel-search": { + "unsupported-layout": "" + }, "playlist-edit": { "error-prefix": "Erro ao carregar lista de reprodução:", "form": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index ea860460b48..120535a2b8d 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1890,6 +1890,9 @@ "view": "查看" } }, + "panel-search": { + "unsupported-layout": "" + }, "playlist-edit": { "error-prefix": "加载播放列表时发生错误:", "form": { From 80611b381c1e4a2ab8036ecbb62a70940e6617f3 Mon Sep 17 00:00:00 2001 From: Santiago Date: Mon, 30 Sep 2024 13:28:30 -0300 Subject: [PATCH 090/174] Alerting: Decrypt secure settings when testing receivers in the remote Alertmanager (#93864) * Alerting: Decrypt secure settings when testing receivers in the remote Alertmanager * go work sync * make update-workspace * point to latest main in grafana/alerting * unit test * import definitions only once --- go.mod | 2 +- go.sum | 4 +- go.work.sum | 2 + .../api/tooling/definitions/alertmanager.go | 17 ++---- pkg/services/ngalert/remote/alertmanager.go | 11 +++- .../ngalert/remote/alertmanager_test.go | 59 +++++++++++++++++++ 6 files changed, 78 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 4496998f90f..1d8d0652273 100644 --- a/go.mod +++ b/go.mod @@ -73,7 +73,7 @@ require ( github.com/googleapis/gax-go/v2 v2.13.0 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240926144415-27f4e81b4b6b // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20240927162124-918609743768 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 // @grafana/identity-access-team github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad diff --git a/go.sum b/go.sum index 92daabd7e00..4bbb15008f0 100644 --- a/go.sum +++ b/go.sum @@ -2256,8 +2256,8 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240926144415-27f4e81b4b6b h1:UO4mv94pG1kzKCgBKh20TXdACBCAK2vYjV3Q2MlcpEQ= -github.com/grafana/alerting v0.0.0-20240926144415-27f4e81b4b6b/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= +github.com/grafana/alerting v0.0.0-20240927162124-918609743768 h1:rZoJB3Myo+aKrTIAfn2E4MfgGime/KbH5sxm/67ppSM= +github.com/grafana/alerting v0.0.0-20240927162124-918609743768/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 h1:zYF/RBulpvMqPYR3gbzJZ8t/j/Eymn5FNidSYkueNCA= github.com/grafana/authlib v0.0.0-20240919120951-58259833c564/go.mod h1:PFzXbCrn0GIpN4KwT6NP1l5Z1CPLfmKHnYx8rZzQcyY= github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= diff --git a/go.work.sum b/go.work.sum index e993d1985f4..9fd96d0bd66 100644 --- a/go.work.sum +++ b/go.work.sum @@ -565,6 +565,8 @@ github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18 h1:3cQ+d+fkNL2Eqp github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10 h1:oDbLKM34O+JUF9EQFS+9aYhdYoeNfUpXqNjFCLIxwF4= github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= +github.com/grafana/alerting v0.0.0-20240926233713-446ddd356f8d h1:HOK6RWTuVldWFtNbWHxPlTa2shZ+WsNJsxoRJhX56Zg= +github.com/grafana/alerting v0.0.0-20240926233713-446ddd356f8d/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc= diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 044d74c3ba4..304a4edcae9 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -2,7 +2,6 @@ package definitions import ( "context" - "encoding/base64" "encoding/json" "fmt" "time" @@ -680,19 +679,11 @@ func (c *PostableUserConfig) Decrypt(decryptFn func(payload []byte) ([]byte, err // Iterate through receivers and decrypt secure settings. for _, rcv := range newCfg.AlertmanagerConfig.Receivers { for _, gmr := range rcv.PostableGrafanaReceivers.GrafanaManagedReceivers { - for k, v := range gmr.SecureSettings { - decoded, err := base64.StdEncoding.DecodeString(v) - if err != nil { - return PostableUserConfig{}, fmt.Errorf("failed to decode value for key '%s': %w", k, err) - } - - decrypted, err := decryptFn(decoded) - if err != nil { - return PostableUserConfig{}, fmt.Errorf("failed to decrypt value for key '%s': %w", k, err) - } - - gmr.SecureSettings[k] = string(decrypted) + decrypted, err := gmr.DecryptSecureSettings(decryptFn) + if err != nil { + return PostableUserConfig{}, err } + gmr.SecureSettings = decrypted } } return *newCfg, nil diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 48350b02ff1..36c6e631b67 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -527,17 +527,26 @@ func (am *Alertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver, } func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) { + fn := func(payload []byte) ([]byte, error) { + return am.decrypt(ctx, payload) + } + receivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) for _, r := range c.Receivers { integrations := make([]*alertingNotify.GrafanaIntegrationConfig, 0, len(r.GrafanaManagedReceivers)) for _, gr := range r.PostableGrafanaReceivers.GrafanaManagedReceivers { + decrypted, err := gr.DecryptSecureSettings(fn) + if err != nil { + return nil, 0, err + } + integrations = append(integrations, &alertingNotify.GrafanaIntegrationConfig{ UID: gr.UID, Name: gr.Name, Type: gr.Type, DisableResolveMessage: gr.DisableResolveMessage, Settings: json.RawMessage(gr.Settings), - SecureSettings: gr.SecureSettings, + SecureSettings: decrypted, }) } receivers = append(receivers, &alertingNotify.APIReceiver{ diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 5d6b9644e34..5e95752ea0e 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -335,6 +335,65 @@ func TestCompareAndSendConfiguration(t *testing.T) { } } +func Test_TestReceiversDecryptsSecureSettings(t *testing.T) { + const testKey = "test-key" + const testValue = "test-value" + decryptFn := func(_ context.Context, payload []byte) ([]byte, error) { + if string(payload) == testValue { + return []byte(testValue), nil + } + return nil, errTest + } + + var got apimodels.TestReceiversConfigBodyParams + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + require.NoError(t, json.NewDecoder(r.Body).Decode(&got)) + require.NoError(t, r.Body.Close()) + _, err := w.Write([]byte(`{"status": "success"}`)) + require.NoError(t, err) + })) + + fstore := notifier.NewFileStore(1, ngfakes.NewFakeKVStore(t)) + m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) + cfg := AlertmanagerConfig{ + OrgID: 1, + TenantID: "test", + URL: server.URL, + DefaultConfig: defaultGrafanaConfig, + } + + am, err := NewAlertmanager(cfg, + fstore, + decryptFn, + NoopAutogenFn, + m, + tracing.InitializeTracerForTest(), + ) + + require.NoError(t, err) + params := apimodels.TestReceiversConfigBodyParams{ + Alert: &apimodels.TestReceiversConfigAlertParams{}, + Receivers: []*definition.PostableApiReceiver{ + { + PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{ + { + SecureSettings: map[string]string{ + testKey: base64.StdEncoding.EncodeToString([]byte(testValue)), + }, + }, + }, + }, + }, + }, + } + + _, _, err = am.TestReceivers(context.Background(), params) + require.NoError(t, err) + require.Equal(t, map[string]string{testKey: testValue}, got.Receivers[0].PostableGrafanaReceivers.GrafanaManagedReceivers[0].SecureSettings) +} + func Test_isDefaultConfiguration(t *testing.T) { parsedDefaultConfig, _ := notifier.Load([]byte(defaultGrafanaConfig)) parsedTestConfig, _ := notifier.Load([]byte(testGrafanaConfig)) From aa77023008b4afd67c310e4b253b99cd9fc16ce3 Mon Sep 17 00:00:00 2001 From: Santiago Date: Mon, 30 Sep 2024 13:50:35 -0300 Subject: [PATCH 091/174] Alerting: Fix panics when attempting to create an Alertmanager after failing (#94023) --- go.mod | 14 +++++----- go.sum | 28 +++++++++---------- go.work | 2 ++ go.work.sum | 2 ++ pkg/aggregator/go.mod | 10 +++---- pkg/aggregator/go.sum | 20 ++++++------- pkg/apimachinery/go.mod | 4 +-- pkg/apimachinery/go.sum | 8 +++--- pkg/apiserver/go.mod | 8 +++--- pkg/apiserver/go.sum | 16 +++++------ pkg/build/go.mod | 8 +++--- pkg/build/go.sum | 16 +++++------ pkg/promlib/go.mod | 8 +++--- pkg/promlib/go.sum | 20 ++++++------- pkg/semconv/go.mod | 2 +- pkg/semconv/go.sum | 4 +-- pkg/services/ngalert/metrics/alertmanager.go | 2 +- pkg/services/ngalert/notifier/alertmanager.go | 2 +- 18 files changed, 89 insertions(+), 85 deletions(-) diff --git a/go.mod b/go.mod index 1d8d0652273..4ddcc9373a5 100644 --- a/go.mod +++ b/go.mod @@ -73,7 +73,7 @@ require ( github.com/googleapis/gax-go/v2 v2.13.0 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240927162124-918609743768 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 // @grafana/identity-access-team github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad @@ -159,15 +159,15 @@ require ( github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group go.opentelemetry.io/collector/pdata v1.6.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // @grafana/plugins-platform-backend - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 // @grafana/grafana-operator-experience-squad + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // @grafana/grafana-operator-experience-squad go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel v1.29.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.30.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.29.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/trace v1.29.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.30.0 // @grafana/grafana-backend-group go.uber.org/atomic v1.11.0 // @grafana/alerting-backend go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage gocloud.dev v0.39.0 // @grafana/grafana-app-platform-squad @@ -430,8 +430,8 @@ require ( go.etcd.io/etcd/client/v3 v3.5.14 // indirect go.mongodb.org/mongo-driver v1.16.1 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -498,7 +498,7 @@ replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240523142 // Use our fork of the upstream alertmanagers. // This is required in order to get notification delivery errors from the receivers API. -replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/go.sum b/go.sum index 4bbb15008f0..484ef4765af 100644 --- a/go.sum +++ b/go.sum @@ -2256,8 +2256,8 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240927162124-918609743768 h1:rZoJB3Myo+aKrTIAfn2E4MfgGime/KbH5sxm/67ppSM= -github.com/grafana/alerting v0.0.0-20240927162124-918609743768/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= +github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e h1:RttFYx5+RTNuMPlaftx8i9f91kwUi9LdxsoPLHnticU= +github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 h1:zYF/RBulpvMqPYR3gbzJZ8t/j/Eymn5FNidSYkueNCA= github.com/grafana/authlib v0.0.0-20240919120951-58259833c564/go.mod h1:PFzXbCrn0GIpN4KwT6NP1l5Z1CPLfmKHnYx8rZzQcyY= github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= @@ -2315,8 +2315,8 @@ github.com/grafana/grafana/pkg/util/xorm v0.0.1 h1:72QZjxWIWpSeOF8ob4aMV058kfgZy github.com/grafana/grafana/pkg/util/xorm v0.0.1/go.mod h1:eNfbB9f2jM8o9RfwqwjY8SYm5tvowJ8Ly+iE4P9rXII= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 h1:AJKOtDKAOg8XNFnIZSmqqqutoTSxVlRs6vekL2p2KEY= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= +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/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/pyroscope/api v0.3.0 h1:WcVKNZ8JlriJnD28wTkZray0wGo8dGkizSJXnbG7Gd8= @@ -3341,14 +3341,14 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 h1:IVtyPth4Rs5P8wIf0mP2KVKFNTJ4paX9qQ4Hkh5gFdc= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= 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.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -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.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 h1:+YPiqF5rR6PqHBlmEFLPumbSP0gY0WmCGFayXRcCLvs= go.opentelemetry.io/contrib/propagators/jaeger v1.29.0/go.mod h1:6PD7q7qquWSp3Z4HeM3e/2ipRubaY1rXZO8NIHVDZjs= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 h1:qKi9ntCcronqWqfuKxqrxZlZd82jXJEgGiAWH1+phxo= @@ -3360,8 +3360,8 @@ go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZV go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= @@ -3378,8 +3378,8 @@ go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xC go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= @@ -3393,8 +3393,8 @@ go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40 go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= diff --git a/go.work b/go.work index 846ebb7016d..e75f9e25b4e 100644 --- a/go.work +++ b/go.work @@ -23,3 +23,5 @@ use ( replace xorm.io/xorm => ./pkg/util/xorm replace github.com/getkin/kin-openapi => github.com/getkin/kin-openapi v0.125.0 + +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 diff --git a/go.work.sum b/go.work.sum index 9fd96d0bd66..6e0e1279285 100644 --- a/go.work.sum +++ b/go.work.sum @@ -568,6 +568,8 @@ github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10/go.mod h1:GMLi6d0 github.com/grafana/alerting v0.0.0-20240926233713-446ddd356f8d h1:HOK6RWTuVldWFtNbWHxPlTa2shZ+WsNJsxoRJhX56Zg= github.com/grafana/alerting v0.0.0-20240926233713-446ddd356f8d/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 h1:AJKOtDKAOg8XNFnIZSmqqqutoTSxVlRs6vekL2p2KEY= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc= github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26/go.mod h1:Pn9nfzCk7nV0mvNgwusgCjCROZP6nm4GpwTnmEhLT24= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index a895aa18a63..f71917ffb66 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -9,7 +9,7 @@ require ( github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel v1.30.0 k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 k8s.io/apiserver v0.31.0 @@ -119,15 +119,15 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v3 v3.5.14 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 758a214e167..2e636ea7f64 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -344,30 +344,30 @@ go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 h1:IVtyPth4Rs5P8wIf0mP2KVKFNTJ4paX9qQ4Hkh5gFdc= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 h1:+YPiqF5rR6PqHBlmEFLPumbSP0gY0WmCGFayXRcCLvs= go.opentelemetry.io/contrib/propagators/jaeger v1.29.0/go.mod h1:6PD7q7qquWSp3Z4HeM3e/2ipRubaY1rXZO8NIHVDZjs= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 h1:qKi9ntCcronqWqfuKxqrxZlZd82jXJEgGiAWH1+phxo= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0/go.mod h1:1kbAgQa5lgYC3rC6cE3jSxQ/Q13l33wv/WI8U+htwag= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 5783e2404e8..22e1be6cc40 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -32,8 +32,8 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/otel v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index ef6c84128f3..fbd7b2d0aba 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -68,10 +68,10 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 4f90c6b5b1a..6e41f937b51 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -8,7 +8,7 @@ require ( github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 github.com/prometheus/client_golang v1.20.3 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/otel/trace v1.29.0 + go.opentelemetry.io/otel/trace v1.30.0 k8s.io/apimachinery v0.31.0 k8s.io/apiserver v0.31.0 k8s.io/component-base v0.31.0 @@ -63,11 +63,11 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v3 v3.5.14 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel v1.30.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 993ed571af9..ae07c35d4b7 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -190,20 +190,20 @@ go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index b3b23bfd27b..eb21e9948a2 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -33,9 +33,9 @@ require ( github.com/urfave/cli v1.22.15 // @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect; @grafana/plugins-platform-backend - go.opentelemetry.io/otel v1.29.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.30.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.29.0 // indirect; @grafana/grafana-backend-group - go.opentelemetry.io/otel/trace v1.29.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.30.0 // indirect; @grafana/grafana-backend-group golang.org/x/crypto v0.27.0 // indirect; @grafana/grafana-backend-group golang.org/x/mod v0.20.0 // @grafana/grafana-backend-group golang.org/x/net v0.29.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources @@ -82,8 +82,8 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/sys v0.25.0 // indirect google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect; @grafana/grafana-backend-group diff --git a/pkg/build/go.sum b/pkg/build/go.sum index b81b24f1e33..e348659ac5e 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -229,10 +229,10 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 h1:oM0GTNKGlc5qHctWeIGTVyda4iFFalOzMZ3Ehj5rwB4= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88/go.mod h1:JGG8ebaMO5nXOPnvKEl+DiA4MGwFjCbjsxT1WHIEBPY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha h1:z2s6Zba+OUyayRv5m1AXWNUTGh57K1iMhy6emU5QT5Y= @@ -245,14 +245,14 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0J go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc= go.opentelemetry.io/otel/log v0.2.0-alpha h1:ixOPvMzserpqA07SENHvRzkZOsnG0XbPr74hv1AQ+n0= go.opentelemetry.io/otel/log v0.2.0-alpha/go.mod h1:vbFZc65yq4c4ssvXY43y/nIqkNJLxORrqw0L85P59LA= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/sdk/log v0.2.0-alpha h1:jGTkL/jroJ31jnP6jDl34N/mDOfRGGYZHcHsCM+5kWA= go.opentelemetry.io/otel/sdk/log v0.2.0-alpha/go.mod h1:Hd8Lw9FPGUM3pfY7iGMRvFaC2Nyau4Ajb5WnQ9OdIho= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 81f09839646..8d029b16294 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -11,8 +11,8 @@ require ( github.com/prometheus/common v0.55.0 github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/otel v1.29.0 - go.opentelemetry.io/otel/trace v1.29.0 + go.opentelemetry.io/otel v1.30.0 + go.opentelemetry.io/otel/trace v1.30.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa k8s.io/apimachinery v0.31.0 ) @@ -102,12 +102,12 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 9d75718ccf1..c7afbecfc64 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -264,30 +264,30 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 h1:IVtyPth4Rs5P8wIf0mP2KVKFNTJ4paX9qQ4Hkh5gFdc= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 h1:+YPiqF5rR6PqHBlmEFLPumbSP0gY0WmCGFayXRcCLvs= go.opentelemetry.io/contrib/propagators/jaeger v1.29.0/go.mod h1:6PD7q7qquWSp3Z4HeM3e/2ipRubaY1rXZO8NIHVDZjs= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 h1:qKi9ntCcronqWqfuKxqrxZlZd82jXJEgGiAWH1+phxo= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0/go.mod h1:1kbAgQa5lgYC3rC6cE3jSxQ/Q13l33wv/WI8U+htwag= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index a8a19c15a1c..cacc8f096dd 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -2,7 +2,7 @@ module github.com/grafana/grafana/pkg/semconv go 1.23.1 -require go.opentelemetry.io/otel v1.29.0 +require go.opentelemetry.io/otel v1.30.0 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/pkg/semconv/go.sum b/pkg/semconv/go.sum index bf61a1563d6..0b9bd1ddf2b 100644 --- a/pkg/semconv/go.sum +++ b/pkg/semconv/go.sum @@ -6,7 +6,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/services/ngalert/metrics/alertmanager.go b/pkg/services/ngalert/metrics/alertmanager.go index dca5c187e32..1386ba5f3b7 100644 --- a/pkg/services/ngalert/metrics/alertmanager.go +++ b/pkg/services/ngalert/metrics/alertmanager.go @@ -20,7 +20,7 @@ func NewAlertmanagerMetrics(r prometheus.Registerer, l log.Logger) *Alertmanager other := prometheus.WrapRegistererWithPrefix(fmt.Sprintf("%s_%s_", Namespace, Subsystem), r) return &Alertmanager{ Registerer: r, - Alerts: metrics.NewAlerts(other), + Alerts: metrics.NewAlerts(other, l), AlertmanagerConfigMetrics: NewAlertmanagerConfigMetrics(r, l), } } diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index ad9e74cd811..264d8da6a60 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -135,7 +135,7 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A } l := log.New("ngalert.notifier.alertmanager", "org", orgID) - gam, err := alertingNotify.NewGrafanaAlertmanager("orgID", orgID, amcfg, peer, l, alertingNotify.NewGrafanaAlertmanagerMetrics(m.Registerer)) + gam, err := alertingNotify.NewGrafanaAlertmanager("orgID", orgID, amcfg, peer, l, alertingNotify.NewGrafanaAlertmanagerMetrics(m.Registerer, l)) if err != nil { return nil, err } From 52f208d3ac7ec94ba78386aca0205428feb27f00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:51:41 +0100 Subject: [PATCH 092/174] Bump github.com/beevik/etree from 1.2.0 to 1.4.1 (#90707) Bumps [github.com/beevik/etree](https://github.com/beevik/etree) from 1.2.0 to 1.4.1. - [Release notes](https://github.com/beevik/etree/releases) - [Changelog](https://github.com/beevik/etree/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/beevik/etree/compare/v1.2.0...v1.4.1) --- updated-dependencies: - dependency-name: github.com/beevik/etree dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mihaly Gyongyosi --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4ddcc9373a5..5733ebf4867 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/apache/arrow/go/v15 v15.0.2 // @grafana/observability-metrics github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad github.com/aws/aws-sdk-go v1.55.5 // @grafana/aws-datasources - github.com/beevik/etree v1.2.0 // @grafana/grafana-backend-group + github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-release-guild github.com/blugelabs/bluge v0.1.9 // @grafana/grafana-backend-group diff --git a/go.sum b/go.sum index 484ef4765af..ea4b29b5808 100644 --- a/go.sum +++ b/go.sum @@ -1603,8 +1603,8 @@ github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xW github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= -github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= -github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= +github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI= +github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= From cebcb38df28a0cd2c0b4c661fe190b649a81b585 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 30 Sep 2024 20:47:16 +0200 Subject: [PATCH 093/174] Alerting: Style nits for the simple query mode (#93930) * Style nits for the simple query mode * update translations * update text * update translations * update disable word to deactivate * update preview text when not advanced options * update text * update text --- .betterer.results | 3 +- .../rule-editor/RuleEditorSection.tsx | 5 +++ .../QueryAndExpressionsStep.tsx | 21 ++++++++-- .../SimpleCondition.tsx | 38 +++++++++++++++++-- public/locales/en-US/grafana.json | 7 ++++ public/locales/pseudo-LOCALE/grafana.json | 7 ++++ 6 files changed, 72 insertions(+), 9 deletions(-) diff --git a/.betterer.results b/.betterer.results index da8a4d5f5fc..7f27fd80d8f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2089,8 +2089,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], diff --git a/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx b/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx index 3c0083fe47c..984757e6c1c 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx @@ -45,6 +45,7 @@ export const RuleEditorSection = ({ label="Advanced options" showLabel transparent + className={styles.reverse} /> )} @@ -74,4 +75,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ fullWidth: css({ width: '100%', }), + reverse: css({ + flexDirection: 'row-reverse', + gap: theme.spacing(1), + }), }); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 794c9be09ec..535f13d37f4 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -20,6 +20,7 @@ import { useStyles2, } from '@grafana/ui'; import { Text } from '@grafana/ui/src/components/Text/Text'; +import { t, Trans } from 'app/core/internationalization'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { @@ -655,7 +656,9 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P onClick={() => runQueriesPreview()} disabled={emptyQueries} > - Preview + {isAdvancedMode + ? t('alerting.queryAndExpressionsStep.preview', 'Preview') + : t('alerting.queryAndExpressionsStep.previewCondition', 'Preview alert rule condition')} )} @@ -673,9 +676,19 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P + + + The selected queries and expressions cannot be converted to default. If you deactivate advanced options, + your query and condition will be reset to default settings. + + +
+
+ } + confirmText="Deactivate" icon="exclamation-triangle" onConfirm={() => { setValue('editorSettings', { simplifiedQueryEditor: true }); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx index 6dcd0204d5f..12b967cd743 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx @@ -103,7 +103,7 @@ export const SimpleConditionEditor = ({ Alert condition - + onEvaluateValueChange(event, 0)} /> -
+
TO
({ + buttonSelectText: css({ + color: theme.colors.primary.text, + fontSize: theme.typography.bodySmall.fontSize, + textTransform: 'uppercase', + padding: `0 ${theme.spacing(1)}`, + }), condition: { wrapper: css({ display: 'flex', @@ -231,11 +242,32 @@ const getStyles = (theme: GrafanaTheme2) => ({ height: 'fit-content', borderRadius: theme.shape.radius.default, }), + container: css({ + display: 'flex', + flexDirection: 'row', + padding: theme.spacing(1), + flex: 1, + width: '100%', + }), header: css({ background: theme.colors.background.secondary, padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, borderBottom: `solid 1px ${theme.colors.border.weak}`, flex: 1, }), + button: css({ + height: '32px', + color: theme.colors.primary.text, + fontSize: theme.typography.bodySmall.fontSize, + textTransform: 'uppercase', + display: 'flex', + alignItems: 'center', + borderRadius: theme.shape.radius.default, + fontWeight: theme.typography.fontWeightBold, + border: `1px solid ${theme.colors.border.medium}`, + whiteSpace: 'nowrap', + padding: `0 ${theme.spacing(1)}`, + backgroundColor: theme.colors.background.primary, + }), }, }); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index edc47d01564..310d59feacb 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -216,6 +216,13 @@ "badge-tooltip-provenance": "This resource has been provisioned via {{provenance}} and cannot be edited through the UI", "badge-tooltip-standard": "This resource has been provisioned and cannot be edited through the UI" }, + "queryAndExpressionsStep": { + "disableAdvancedOptions": { + "text": "The selected queries and expressions cannot be converted to default. If you deactivate advanced options, your query and condition will be reset to default settings." + }, + "preview": "Preview", + "previewCondition": "Preview alert rule condition" + }, "rule-groups": { "delete": { "success": "Successfully deleted rule group" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index a1be240c152..b8107773274 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -216,6 +216,13 @@ "badge-tooltip-provenance": "Ŧĥįş řęşőūřčę ĥäş þęęʼn přővįşįőʼnęđ vįä {{provenance}} äʼnđ čäʼnʼnőŧ þę ęđįŧęđ ŧĥřőūģĥ ŧĥę ŮĨ", "badge-tooltip-standard": "Ŧĥįş řęşőūřčę ĥäş þęęʼn přővįşįőʼnęđ äʼnđ čäʼnʼnőŧ þę ęđįŧęđ ŧĥřőūģĥ ŧĥę ŮĨ" }, + "queryAndExpressionsStep": { + "disableAdvancedOptions": { + "text": "Ŧĥę şęľęčŧęđ qūęřįęş äʼnđ ęχpřęşşįőʼnş čäʼnʼnőŧ þę čőʼnvęřŧęđ ŧő đęƒäūľŧ. Ĩƒ yőū đęäčŧįväŧę äđväʼnčęđ őpŧįőʼnş, yőūř qūęřy äʼnđ čőʼnđįŧįőʼn ŵįľľ þę řęşęŧ ŧő đęƒäūľŧ şęŧŧįʼnģş." + }, + "preview": "Přęvįęŵ", + "previewCondition": "Přęvįęŵ äľęřŧ řūľę čőʼnđįŧįőʼn" + }, "rule-groups": { "delete": { "success": "Ŝūččęşşƒūľľy đęľęŧęđ řūľę ģřőūp" From 6a3eb276efb8ba7a2ccb9cb79977e2def76d406d Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Mon, 30 Sep 2024 13:46:14 -0600 Subject: [PATCH 094/174] Grafana Indexing PoC: Adds feature flag and gRPC endpoint (#93356) * adds Filter gRPC and make protobuf * adds route for querying the filter gRPC * wires up Filter gRPC call * [WIP] index from start * renames gRPC endpoint to "Search" * adds /apis/search route into k8s routes. Hacky for now. * updates readme - wrong casing * adds feature toggle for unified storage search * hides US search behind feature flag. Clean up print statements. * removes indexer - will be added in another PR * Search: Add API Builder * adds required method * implementing UpdateAPIGroupInfo (WIP) * adds groupversion * commenting out for now * remove unneeded code from experimenting and update register.go to match interface required * namespaces search route --------- Co-authored-by: leonorfmartins Co-authored-by: Todd Treece --- go.mod | 8 +- go.sum | 15 +- go.work.sum | 33 ++ .../src/types/featureToggles.gen.ts | 1 + pkg/registry/apis/apis.go | 2 + pkg/registry/apis/dashboard/legacy/storage.go | 4 + pkg/registry/apis/search/register.go | 103 ++++ pkg/registry/apis/wireset.go | 2 + pkg/services/featuremgmt/registry.go | 8 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 16 +- pkg/storage/unified/resource/noop.go | 4 + pkg/storage/unified/resource/resource.pb.go | 512 +++++++++++------- pkg/storage/unified/resource/resource.proto | 28 +- .../unified/resource/resource_grpc.pb.go | 38 ++ pkg/storage/unified/resource/server.go | 30 +- pkg/storage/unified/sql/server.go | 4 + 18 files changed, 599 insertions(+), 214 deletions(-) create mode 100644 pkg/registry/apis/search/register.go diff --git a/go.mod b/go.mod index 5733ebf4867..8c1da0d6802 100644 --- a/go.mod +++ b/go.mod @@ -225,7 +225,7 @@ require ( github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/RoaringBitmap/roaring v0.9.4 // indirect + github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect @@ -239,12 +239,12 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.2.0 // indirect + github.com/bits-and-blooms/bitset v1.12.0 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/segment v0.9.0 // indirect + github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect - github.com/blevesearch/vellum v1.0.7 // indirect + github.com/blevesearch/vellum v1.0.10 // indirect github.com/blugelabs/ice v1.0.0 // indirect github.com/bufbuild/protocompile v0.4.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect diff --git a/go.sum b/go.sum index ea4b29b5808..f7d7f5ef080 100644 --- a/go.sum +++ b/go.sum @@ -1466,8 +1466,8 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMz github.com/RoaringBitmap/gocroaring v0.4.0/go.mod h1:NieMwz7ZqwU2DD73/vvYwv7r4eWBKuPVSXZIpsaMwCI= github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76/go.mod h1:oM0MHmQ3nDsq609SS36p+oYbRi16+oVvU2Bw4Ipv0SE= github.com/RoaringBitmap/roaring v0.9.1/go.mod h1:h1B7iIUOmnAeb5ytYMvnHJwxMc6LUrwBnzXWRuqTQUc= -github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo= -github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= +github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= +github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= @@ -1614,23 +1614,24 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= -github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= +github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= +github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+eMD5xZ2OyQ= -github.com/blevesearch/vellum v1.0.7 h1:+vn8rfyCRHxKVRgDLeR0FAXej2+6mEb5Q15aQE/XESQ= -github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= +github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= +github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= github.com/blugelabs/bluge v0.1.9 h1:bPgXlcsWugrXNjzeoLdOnvfJpHsyODKpYaAndayl/SM= github.com/blugelabs/bluge v0.1.9/go.mod h1:5d7LktUkQgvbh5Bmi6tPWtvo4+6uRTm6gAwP+5z6FqQ= github.com/blugelabs/bluge_segment_api v0.2.0 h1:cCX1Y2y8v0LZ7+EEJ6gH7dW6TtVTW4RhG0vp3R+N2Lo= diff --git a/go.work.sum b/go.work.sum index 6e0e1279285..0b97fda9088 100644 --- a/go.work.sum +++ b/go.work.sum @@ -308,6 +308,8 @@ github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJs github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= github.com/RoaringBitmap/gocroaring v0.4.0 h1:5nufXUgWpBEUNEJXw7926YAA58ZAQRpWPrQV1xCoSjc= github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 h1:ZYlhPbqQFU+AHfgtCdHGDTtRW1a8geZyiE8c6Q+Sl1s= +github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo= +github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= @@ -333,6 +335,8 @@ github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= @@ -368,6 +372,19 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA= +github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA= +github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= +github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI= +github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ= +github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= +github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac= +github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A= +github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg= +github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc= +github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc= +github.com/blevesearch/vellum v1.0.7 h1:+vn8rfyCRHxKVRgDLeR0FAXej2+6mEb5Q15aQE/XESQ= +github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= @@ -417,6 +434,10 @@ github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjs github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= +github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps= +github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= +github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o= +github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -474,6 +495,8 @@ github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8 github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s= github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= @@ -527,6 +550,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198 h1:FSii2UQeSLngl3jFoR4tUKZLprO7qUlh/TKKticc0BM= github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= @@ -648,6 +673,8 @@ github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8= @@ -681,6 +708,8 @@ github.com/matryer/moq v0.3.3 h1:pScMH9VyrdT4S93yiLpVyU8rCDqGQr24uOyBxmktG5Q= github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= github.com/matryer/moq v0.5.0 h1:h2PJUYjZSiyEahzVogDRmrgL9Bsx9xYAl8l+LPfmwL8= github.com/matryer/moq v0.5.0/go.mod h1:39GTnrD0mVWHPvWdYj5ki/lxfhLQEtHcLh+tWoYF/iE= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= @@ -809,6 +838,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCL github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/scottlepp/go-duck v0.3.1 h1:UDm7AepGq7urXUAtF/Nxbrth6Te+k42pz/6jdpC3gp8= +github.com/scottlepp/go-duck v0.3.1/go.mod h1:cQRP0clf3eX6QUizCDTNMqShtW8Ek7ovnURCGOrJgyU= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= @@ -896,6 +927,8 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.einride.tech/aip v0.67.1 h1:d/4TW92OxXBngkSOwWS2CH5rez869KpKMaN44mdxkFI= go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI= +go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= +go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= go.opentelemetry.io/collector v0.97.0 h1:qyOju13byHIKEK/JehmTiGMj4pFLa4kDyrOCtTmjHU0= go.opentelemetry.io/collector v0.97.0/go.mod h1:V6xquYAaO2VHVu4DBK28JYuikRdZajh7DH5Vl/Y8NiA= go.opentelemetry.io/collector/component v0.97.0 h1:vanKhXl5nptN8igRH4PqVYHOILif653vaPIKv6LCZCI= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 55014a72992..8853cdc663a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -217,4 +217,5 @@ export interface FeatureToggles { improvedExternalSessionHandling?: boolean; useSessionStorageForRedirection?: boolean; rolePickerDrawer?: boolean; + unifiedStorageSearch?: boolean; } diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 1702c9c338e..5c56639efc2 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/playlist" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/scope" + "github.com/grafana/grafana/pkg/registry/apis/search" ) var ( @@ -37,6 +38,7 @@ func ProvideRegistryServiceSink( _ *scope.ScopeAPIBuilder, _ *query.QueryAPIBuilder, _ *notifications.NotificationsAPIBuilder, + _ *search.SearchAPIBuilder, ) *Service { return &Service{} } diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index 03cf09e225d..2f96178ca7f 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -246,6 +246,10 @@ func (a *dashboardSqlAccess) Read(ctx context.Context, req *resource.ReadRequest return a.ReadResource(ctx, req), nil } +func (a *dashboardSqlAccess) Search(ctx context.Context, req *resource.SearchRequest) (*resource.SearchResponse, error) { + return nil, fmt.Errorf("not yet (filter)") +} + func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryRequest) (*resource.HistoryResponse, error) { info, err := claims.ParseNamespace(req.Key.Namespace) if err == nil { diff --git a/pkg/registry/apis/search/register.go b/pkg/registry/apis/search/register.go new file mode 100644 index 00000000000..78b2afbfd07 --- /dev/null +++ b/pkg/registry/apis/search/register.go @@ -0,0 +1,103 @@ +package search + +import ( + "encoding/json" + "net/http" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericapiserver "k8s.io/apiserver/pkg/server" + common "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" + + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var _ builder.APIGroupBuilder = (*SearchAPIBuilder)(nil) + +type SearchAPIBuilder struct { + unified resource.ResourceClient +} + +func NewSearchAPIBuilder( + unified resource.ResourceClient, +) (*SearchAPIBuilder, error) { + return &SearchAPIBuilder{ + unified: unified, + }, nil +} + +func RegisterAPIService( + features featuremgmt.FeatureToggles, + apiregistration builder.APIRegistrar, + unified resource.ResourceClient, +) (*SearchAPIBuilder, error) { + if !(features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) || features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs)) { + return nil, nil + } + builder, err := NewSearchAPIBuilder(unified) + apiregistration.RegisterAPI(builder) + return builder, err +} + +func (b *SearchAPIBuilder) GetGroupVersion() schema.GroupVersion { + return schema.GroupVersion{Group: "search.grafana.app", Version: "v0alpha1"} +} + +func (b *SearchAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { + return nil +} + +func (b *SearchAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{} + } +} + +func (b *SearchAPIBuilder) GetAPIRoutes() *builder.APIRoutes { + return &builder.APIRoutes{ + Namespace: []builder.APIRouteHandler{ + { + Path: "search", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + Tags: []string{"Search"}, + Summary: "Search", + Description: "Search for resources", + }, + }, + }, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + urlQuery := r.URL.Query().Get("query") + searchRequest := &resource.SearchRequest{Query: urlQuery} + res, err := b.unified.Search(r.Context(), searchRequest) + if err != nil { + panic(err) + } + if err := json.NewEncoder(w).Encode(res); err != nil { + panic(err) + } + }), + }, + }, + } +} + +func (b *SearchAPIBuilder) GetAuthorizer() authorizer.Authorizer { + return nil +} + +func (b *SearchAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + return oas, nil +} + +func (b *SearchAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, dualWriteBuilder grafanarest.DualWriteBuilder) error { + apiGroupInfo.PrioritizedVersions = []schema.GroupVersion{b.GetGroupVersion()} + return nil +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index e9d3ac6adbf..0cd14526b5d 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/playlist" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/scope" + "github.com/grafana/grafana/pkg/registry/apis/search" "github.com/grafana/grafana/pkg/registry/apis/service" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" ) @@ -40,4 +41,5 @@ var WireSet = wire.NewSet( scope.RegisterAPIService, notifications.RegisterAPIService, //sso.RegisterAPIService, + search.RegisterAPIService, ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 35f58666dd4..b2eefd0622b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1497,6 +1497,14 @@ var ( Stage: FeatureStageExperimental, Owner: identityAccessTeam, }, + { + Name: "unifiedStorageSearch", + Description: "Enable unified storage search", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, + HideFromAdminPage: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index de789afd8ae..ffdf26a0e3c 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -198,3 +198,4 @@ alertingQueryAndExpressionsStepMode,experimental,@grafana/alerting-squad,false,f improvedExternalSessionHandling,experimental,@grafana/identity-access-team,false,false,false useSessionStorageForRedirection,preview,@grafana/identity-access-team,false,false,false rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false +unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index c89153d1748..6f792352d09 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -802,4 +802,8 @@ const ( // FlagRolePickerDrawer // Enables the new role picker drawer design FlagRolePickerDrawer = "rolePickerDrawer" + + // FlagUnifiedStorageSearch + // Enable unified storage search + FlagUnifiedStorageSearch = "unifiedStorageSearch" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2c4fb70bb06..0868229e1c7 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3024,6 +3024,20 @@ "codeowner": "@grafana/identity-access-team" } }, + { + "metadata": { + "name": "unifiedStorageSearch", + "resourceVersion": "1726771421439", + "creationTimestamp": "2024-09-19T18:43:41Z" + }, + "spec": { + "description": "Enable unified storage search", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "vizActions", @@ -3078,4 +3092,4 @@ } } ] -} \ No newline at end of file +} diff --git a/pkg/storage/unified/resource/noop.go b/pkg/storage/unified/resource/noop.go index 9553c607990..5a1a6a22820 100644 --- a/pkg/storage/unified/resource/noop.go +++ b/pkg/storage/unified/resource/noop.go @@ -35,6 +35,10 @@ func (n *noopService) Read(context.Context, *ReadRequest) (*ReadResponse, error) return nil, ErrNotImplementedYet } +func (n *noopService) Search(context.Context, *SearchRequest) (*SearchResponse, error) { + return nil, ErrNotImplementedYet +} + func (n *noopService) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { return nil, ErrNotImplementedYet } diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 7fa31ba23b1..44e5be68f8c 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -173,7 +173,7 @@ func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { // Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{26, 0} + return file_resource_proto_rawDescGZIP(), []int{28, 0} } type ResourceKey struct { @@ -1614,6 +1614,100 @@ func (x *WatchEvent) GetPrevious() *WatchEvent_Resource { return nil } +type SearchRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` +} + +func (x *SearchRequest) Reset() { + *x = SearchRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchRequest) ProtoMessage() {} + +func (x *SearchRequest) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead. +func (*SearchRequest) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{20} +} + +func (x *SearchRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +type SearchResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*ResourceWrapper `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *SearchResponse) Reset() { + *x = SearchResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchResponse) ProtoMessage() {} + +func (x *SearchResponse) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead. +func (*SearchResponse) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{21} +} + +func (x *SearchResponse) GetItems() []*ResourceWrapper { + if x != nil { + return x.Items + } + return nil +} + type HistoryRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1632,7 +1726,7 @@ type HistoryRequest struct { func (x *HistoryRequest) Reset() { *x = HistoryRequest{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[20] + mi := &file_resource_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1645,7 +1739,7 @@ func (x *HistoryRequest) String() string { func (*HistoryRequest) ProtoMessage() {} func (x *HistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[20] + mi := &file_resource_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1658,7 +1752,7 @@ func (x *HistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HistoryRequest.ProtoReflect.Descriptor instead. func (*HistoryRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{20} + return file_resource_proto_rawDescGZIP(), []int{22} } func (x *HistoryRequest) GetNextPageToken() string { @@ -1706,7 +1800,7 @@ type HistoryResponse struct { func (x *HistoryResponse) Reset() { *x = HistoryResponse{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[21] + mi := &file_resource_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1719,7 +1813,7 @@ func (x *HistoryResponse) String() string { func (*HistoryResponse) ProtoMessage() {} func (x *HistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[21] + mi := &file_resource_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1732,7 +1826,7 @@ func (x *HistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HistoryResponse.ProtoReflect.Descriptor instead. func (*HistoryResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{21} + return file_resource_proto_rawDescGZIP(), []int{23} } func (x *HistoryResponse) GetItems() []*ResourceMeta { @@ -1781,7 +1875,7 @@ type OriginRequest struct { func (x *OriginRequest) Reset() { *x = OriginRequest{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[22] + mi := &file_resource_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1794,7 +1888,7 @@ func (x *OriginRequest) String() string { func (*OriginRequest) ProtoMessage() {} func (x *OriginRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[22] + mi := &file_resource_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1807,7 +1901,7 @@ func (x *OriginRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OriginRequest.ProtoReflect.Descriptor instead. func (*OriginRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{22} + return file_resource_proto_rawDescGZIP(), []int{24} } func (x *OriginRequest) GetNextPageToken() string { @@ -1862,7 +1956,7 @@ type ResourceOriginInfo struct { func (x *ResourceOriginInfo) Reset() { *x = ResourceOriginInfo{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[23] + mi := &file_resource_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1875,7 +1969,7 @@ func (x *ResourceOriginInfo) String() string { func (*ResourceOriginInfo) ProtoMessage() {} func (x *ResourceOriginInfo) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[23] + mi := &file_resource_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1888,7 +1982,7 @@ func (x *ResourceOriginInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceOriginInfo.ProtoReflect.Descriptor instead. func (*ResourceOriginInfo) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{23} + return file_resource_proto_rawDescGZIP(), []int{25} } func (x *ResourceOriginInfo) GetKey() *ResourceKey { @@ -1957,7 +2051,7 @@ type OriginResponse struct { func (x *OriginResponse) Reset() { *x = OriginResponse{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[24] + mi := &file_resource_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1970,7 +2064,7 @@ func (x *OriginResponse) String() string { func (*OriginResponse) ProtoMessage() {} func (x *OriginResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[24] + mi := &file_resource_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1983,7 +2077,7 @@ func (x *OriginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use OriginResponse.ProtoReflect.Descriptor instead. func (*OriginResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{24} + return file_resource_proto_rawDescGZIP(), []int{26} } func (x *OriginResponse) GetItems() []*ResourceOriginInfo { @@ -2025,7 +2119,7 @@ type HealthCheckRequest struct { func (x *HealthCheckRequest) Reset() { *x = HealthCheckRequest{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[25] + mi := &file_resource_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2038,7 +2132,7 @@ func (x *HealthCheckRequest) String() string { func (*HealthCheckRequest) ProtoMessage() {} func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[25] + mi := &file_resource_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2051,7 +2145,7 @@ func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. func (*HealthCheckRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{25} + return file_resource_proto_rawDescGZIP(), []int{27} } func (x *HealthCheckRequest) GetService() string { @@ -2072,7 +2166,7 @@ type HealthCheckResponse struct { func (x *HealthCheckResponse) Reset() { *x = HealthCheckResponse{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[26] + mi := &file_resource_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2085,7 +2179,7 @@ func (x *HealthCheckResponse) String() string { func (*HealthCheckResponse) ProtoMessage() {} func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[26] + mi := &file_resource_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2098,7 +2192,7 @@ func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. func (*HealthCheckResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{26} + return file_resource_proto_rawDescGZIP(), []int{28} } func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { @@ -2120,7 +2214,7 @@ type WatchEvent_Resource struct { func (x *WatchEvent_Resource) Reset() { *x = WatchEvent_Resource{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[27] + mi := &file_resource_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2133,7 +2227,7 @@ func (x *WatchEvent_Resource) String() string { func (*WatchEvent_Resource) ProtoMessage() {} func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[27] + mi := &file_resource_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2347,124 +2441,135 @@ var file_resource_proto_rawDesc = []byte{ 0x09, 0x0a, 0x05, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, - 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x9a, - 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, - 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, - 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, + 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x25, + 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x41, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, + 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, + 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, + 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0xbf, 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, + 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, + 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, + 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x77, - 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, - 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0xbf, 0x01, 0x0a, 0x0f, - 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, - 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x8e, 0x01, - 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, - 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, - 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5, - 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, - 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x23, - 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, - 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, - 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xc4, 0x01, 0x0a, 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, - 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, - 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x2e, 0x0a, - 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, - 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, - 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, - 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, - 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, 0x14, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, - 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, - 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, - 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, - 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, + 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, + 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, + 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x22, 0xc4, 0x01, 0x0a, 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, + 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, + 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, + 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, + 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, + 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, - 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, - 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, - 0x32, 0x8c, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, - 0x65, 0x78, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, + 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, + 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, + 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, + 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xc9, 0x01, 0x0a, 0x0d, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x3b, 0x0a, 0x06, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, + 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, + 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, - 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, - 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, - 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, - 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, + 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, + 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -2480,7 +2585,7 @@ func file_resource_proto_rawDescGZIP() []byte { } var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_resource_proto_goTypes = []any{ (ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch (WatchEvent_Type)(0), // 1: resource.WatchEvent.Type @@ -2505,14 +2610,16 @@ var file_resource_proto_goTypes = []any{ (*ListResponse)(nil), // 20: resource.ListResponse (*WatchRequest)(nil), // 21: resource.WatchRequest (*WatchEvent)(nil), // 22: resource.WatchEvent - (*HistoryRequest)(nil), // 23: resource.HistoryRequest - (*HistoryResponse)(nil), // 24: resource.HistoryResponse - (*OriginRequest)(nil), // 25: resource.OriginRequest - (*ResourceOriginInfo)(nil), // 26: resource.ResourceOriginInfo - (*OriginResponse)(nil), // 27: resource.OriginResponse - (*HealthCheckRequest)(nil), // 28: resource.HealthCheckRequest - (*HealthCheckResponse)(nil), // 29: resource.HealthCheckResponse - (*WatchEvent_Resource)(nil), // 30: resource.WatchEvent.Resource + (*SearchRequest)(nil), // 23: resource.SearchRequest + (*SearchResponse)(nil), // 24: resource.SearchResponse + (*HistoryRequest)(nil), // 25: resource.HistoryRequest + (*HistoryResponse)(nil), // 26: resource.HistoryResponse + (*OriginRequest)(nil), // 27: resource.OriginRequest + (*ResourceOriginInfo)(nil), // 28: resource.ResourceOriginInfo + (*OriginResponse)(nil), // 29: resource.OriginResponse + (*HealthCheckRequest)(nil), // 30: resource.HealthCheckRequest + (*HealthCheckResponse)(nil), // 31: resource.HealthCheckResponse + (*WatchEvent_Resource)(nil), // 32: resource.WatchEvent.Resource } var file_resource_proto_depIdxs = []int32{ 7, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails @@ -2534,39 +2641,42 @@ var file_resource_proto_depIdxs = []int32{ 6, // 16: resource.ListResponse.error:type_name -> resource.ErrorResult 18, // 17: resource.WatchRequest.options:type_name -> resource.ListOptions 1, // 18: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type - 30, // 19: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource - 30, // 20: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource - 3, // 21: resource.HistoryRequest.key:type_name -> resource.ResourceKey - 5, // 22: resource.HistoryResponse.items:type_name -> resource.ResourceMeta - 6, // 23: resource.HistoryResponse.error:type_name -> resource.ErrorResult - 3, // 24: resource.OriginRequest.key:type_name -> resource.ResourceKey - 3, // 25: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey - 26, // 26: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo - 6, // 27: resource.OriginResponse.error:type_name -> resource.ErrorResult - 2, // 28: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus - 15, // 29: resource.ResourceStore.Read:input_type -> resource.ReadRequest - 9, // 30: resource.ResourceStore.Create:input_type -> resource.CreateRequest - 11, // 31: resource.ResourceStore.Update:input_type -> resource.UpdateRequest - 13, // 32: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest - 19, // 33: resource.ResourceStore.List:input_type -> resource.ListRequest - 21, // 34: resource.ResourceStore.Watch:input_type -> resource.WatchRequest - 23, // 35: resource.ResourceIndex.History:input_type -> resource.HistoryRequest - 25, // 36: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest - 28, // 37: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest - 16, // 38: resource.ResourceStore.Read:output_type -> resource.ReadResponse - 10, // 39: resource.ResourceStore.Create:output_type -> resource.CreateResponse - 12, // 40: resource.ResourceStore.Update:output_type -> resource.UpdateResponse - 14, // 41: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse - 20, // 42: resource.ResourceStore.List:output_type -> resource.ListResponse - 22, // 43: resource.ResourceStore.Watch:output_type -> resource.WatchEvent - 24, // 44: resource.ResourceIndex.History:output_type -> resource.HistoryResponse - 27, // 45: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse - 29, // 46: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse - 38, // [38:47] is the sub-list for method output_type - 29, // [29:38] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 32, // 19: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource + 32, // 20: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource + 4, // 21: resource.SearchResponse.items:type_name -> resource.ResourceWrapper + 3, // 22: resource.HistoryRequest.key:type_name -> resource.ResourceKey + 5, // 23: resource.HistoryResponse.items:type_name -> resource.ResourceMeta + 6, // 24: resource.HistoryResponse.error:type_name -> resource.ErrorResult + 3, // 25: resource.OriginRequest.key:type_name -> resource.ResourceKey + 3, // 26: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey + 28, // 27: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo + 6, // 28: resource.OriginResponse.error:type_name -> resource.ErrorResult + 2, // 29: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus + 15, // 30: resource.ResourceStore.Read:input_type -> resource.ReadRequest + 9, // 31: resource.ResourceStore.Create:input_type -> resource.CreateRequest + 11, // 32: resource.ResourceStore.Update:input_type -> resource.UpdateRequest + 13, // 33: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest + 19, // 34: resource.ResourceStore.List:input_type -> resource.ListRequest + 21, // 35: resource.ResourceStore.Watch:input_type -> resource.WatchRequest + 23, // 36: resource.ResourceIndex.Search:input_type -> resource.SearchRequest + 25, // 37: resource.ResourceIndex.History:input_type -> resource.HistoryRequest + 27, // 38: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest + 30, // 39: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest + 16, // 40: resource.ResourceStore.Read:output_type -> resource.ReadResponse + 10, // 41: resource.ResourceStore.Create:output_type -> resource.CreateResponse + 12, // 42: resource.ResourceStore.Update:output_type -> resource.UpdateResponse + 14, // 43: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse + 20, // 44: resource.ResourceStore.List:output_type -> resource.ListResponse + 22, // 45: resource.ResourceStore.Watch:output_type -> resource.WatchEvent + 24, // 46: resource.ResourceIndex.Search:output_type -> resource.SearchResponse + 26, // 47: resource.ResourceIndex.History:output_type -> resource.HistoryResponse + 29, // 48: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse + 31, // 49: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse + 40, // [40:50] is the sub-list for method output_type + 30, // [30:40] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name } func init() { file_resource_proto_init() } @@ -2816,7 +2926,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[20].Exporter = func(v any, i int) any { - switch v := v.(*HistoryRequest); i { + switch v := v.(*SearchRequest); i { case 0: return &v.state case 1: @@ -2828,7 +2938,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[21].Exporter = func(v any, i int) any { - switch v := v.(*HistoryResponse); i { + switch v := v.(*SearchResponse); i { case 0: return &v.state case 1: @@ -2840,7 +2950,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[22].Exporter = func(v any, i int) any { - switch v := v.(*OriginRequest); i { + switch v := v.(*HistoryRequest); i { case 0: return &v.state case 1: @@ -2852,7 +2962,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[23].Exporter = func(v any, i int) any { - switch v := v.(*ResourceOriginInfo); i { + switch v := v.(*HistoryResponse); i { case 0: return &v.state case 1: @@ -2864,7 +2974,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[24].Exporter = func(v any, i int) any { - switch v := v.(*OriginResponse); i { + switch v := v.(*OriginRequest); i { case 0: return &v.state case 1: @@ -2876,7 +2986,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[25].Exporter = func(v any, i int) any { - switch v := v.(*HealthCheckRequest); i { + switch v := v.(*ResourceOriginInfo); i { case 0: return &v.state case 1: @@ -2888,7 +2998,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[26].Exporter = func(v any, i int) any { - switch v := v.(*HealthCheckResponse); i { + switch v := v.(*OriginResponse); i { case 0: return &v.state case 1: @@ -2900,6 +3010,30 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[27].Exporter = func(v any, i int) any { + switch v := v.(*HealthCheckRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[28].Exporter = func(v any, i int) any { + switch v := v.(*HealthCheckResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[29].Exporter = func(v any, i int) any { switch v := v.(*WatchEvent_Resource); i { case 0: return &v.state @@ -2918,7 +3052,7 @@ func file_resource_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_resource_proto_rawDesc, NumEnums: 3, - NumMessages: 28, + NumMessages: 30, NumExtensions: 0, NumServices: 3, }, diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index e298ff77aa9..9859ea51994 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -137,8 +137,8 @@ message CreateRequest { // If name is not set, a unique name will be generated // The resourceVersion should not be set ResourceKey key = 1; - - // The resource JSON. + + // The resource JSON. bytes value = 2; } @@ -157,7 +157,7 @@ message UpdateRequest { // The current resource version int64 resource_version = 2; - // The resource JSON. + // The resource JSON. bytes value = 3; } @@ -245,7 +245,7 @@ message ListRequest { // The resource version int64 resource_version = 2; - + // List options ResourceVersionMatch version_match = 3; @@ -302,8 +302,8 @@ message WatchEvent { ADDED = 1; MODIFIED = 2; DELETED = 3; - BOOKMARK = 4; - ERROR = 5; + BOOKMARK = 4; + ERROR = 5; } message Resource { @@ -324,6 +324,14 @@ message WatchEvent { Resource previous = 4; } +message SearchRequest { + string query = 1; +} + +message SearchResponse { + repeated ResourceWrapper items = 1; +} + message HistoryRequest { // Starting from the requested page (other query parameters must match!) string next_page_token = 1; @@ -418,7 +426,7 @@ message HealthCheckResponse { // This provides the CRUD+List+Watch support needed for a k8s apiserver // The semantics and behaviors of this service are constrained by kubernetes -// This does not understand the resource schemas, only deals with json bytes +// This does not understand the resource schemas, only deals with json bytes // Clients should not use this interface directly; it is for use in API Servers service ResourceStore { rpc Read(ReadRequest) returns (ReadResponse); @@ -427,12 +435,12 @@ service ResourceStore { rpc Delete(DeleteRequest) returns (DeleteResponse); // The results *may* include values that should not be returned to the user - // This will perform best-effort filtering to increase performace. + // This will perform best-effort filtering to increase performace. // NOTE: storage.Interface is ultimatly responsible for the final filtering rpc List(ListRequest) returns (ListResponse); // The results *may* include values that should not be returned to the user - // This will perform best-effort filtering to increase performace. + // This will perform best-effort filtering to increase performace. // NOTE: storage.Interface is ultimatly responsible for the final filtering rpc Watch(WatchRequest) returns (stream WatchEvent); } @@ -440,7 +448,7 @@ service ResourceStore { // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics service ResourceIndex { - // TODO: rpc Search(...) ... eventually a typed response + rpc Search(SearchRequest) returns (SearchResponse); // Show resource history (and trash) rpc History(HistoryRequest) returns (HistoryResponse); diff --git a/pkg/storage/unified/resource/resource_grpc.pb.go b/pkg/storage/unified/resource/resource_grpc.pb.go index 9e1db6eb635..f349db5c566 100644 --- a/pkg/storage/unified/resource/resource_grpc.pb.go +++ b/pkg/storage/unified/resource/resource_grpc.pb.go @@ -348,6 +348,7 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{ } const ( + ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search" ResourceIndex_History_FullMethodName = "/resource.ResourceIndex/History" ResourceIndex_Origin_FullMethodName = "/resource.ResourceIndex/Origin" ) @@ -359,6 +360,7 @@ const ( // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics type ResourceIndexClient interface { + Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) // Show resource history (and trash) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) // Used for efficient provisioning @@ -373,6 +375,16 @@ func NewResourceIndexClient(cc grpc.ClientConnInterface) ResourceIndexClient { return &resourceIndexClient{cc} } +func (c *resourceIndexClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SearchResponse) + err := c.cc.Invoke(ctx, ResourceIndex_Search_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *resourceIndexClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(HistoryResponse) @@ -400,6 +412,7 @@ func (c *resourceIndexClient) Origin(ctx context.Context, in *OriginRequest, opt // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics type ResourceIndexServer interface { + Search(context.Context, *SearchRequest) (*SearchResponse, error) // Show resource history (and trash) History(context.Context, *HistoryRequest) (*HistoryResponse, error) // Used for efficient provisioning @@ -410,6 +423,9 @@ type ResourceIndexServer interface { type UnimplementedResourceIndexServer struct { } +func (UnimplementedResourceIndexServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Search not implemented") +} func (UnimplementedResourceIndexServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method History not implemented") } @@ -428,6 +444,24 @@ func RegisterResourceIndexServer(s grpc.ServiceRegistrar, srv ResourceIndexServe s.RegisterService(&ResourceIndex_ServiceDesc, srv) } +func _ResourceIndex_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SearchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceIndexServer).Search(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceIndex_Search_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceIndexServer).Search(ctx, req.(*SearchRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ResourceIndex_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(HistoryRequest) if err := dec(in); err != nil { @@ -471,6 +505,10 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{ ServiceName: "resource.ResourceIndex", HandlerType: (*ResourceIndexServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "Search", + Handler: _ResourceIndex_Search_Handler, + }, { MethodName: "History", Handler: _ResourceIndex_History_Handler, diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 3bf1db0529a..c44c1ac1ebc 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -100,6 +100,25 @@ type ResourceServerOptions struct { Now func() int64 } +type indexServer struct{} + +func (s indexServer) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) { + res := &SearchResponse{} + return res, nil +} + +func (s indexServer) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) { + return nil, nil +} + +func (s indexServer) Origin(ctx context.Context, req *OriginRequest) (*OriginResponse, error) { + return nil, nil +} + +func NewResourceIndexServer() ResourceIndexServer { + return indexServer{} +} + func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { if opts.Tracer == nil { opts.Tracer = noop.NewTracerProvider().Tracer("resource-server") @@ -108,9 +127,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { if opts.Backend == nil { return nil, fmt.Errorf("missing Backend implementation") } - if opts.Index == nil { - opts.Index = &noopService{} - } + if opts.Diagnostics == nil { opts.Diagnostics = &noopService{} } @@ -667,6 +684,13 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { } } +func (s *server) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) { + if err := s.Init(ctx); err != nil { + return nil, err + } + return s.index.Search(ctx, req) +} + // History implements ResourceServer. func (s *server) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) { if err := s.Init(ctx); err != nil { diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index a5caf56ec37..58770875bc3 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -27,5 +27,9 @@ func NewResourceServer(db infraDB.DB, cfg *setting.Cfg, features featuremgmt.Fea opts.Diagnostics = store opts.Lifecycle = store + if features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) { + opts.Index = resource.NewResourceIndexServer() + } + return resource.NewResourceServer(opts) } From 393faa87329c6aecfa5d87f66a168bba7bae2cab Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Mon, 30 Sep 2024 16:52:49 -0500 Subject: [PATCH 095/174] Alerting: Move rule evaluation status logic out of prometheus API and into scheduler (#89141) * Add health fields to rules and an aggregator method to the scheduler * Move health, last error, and last eval time in together to minimize state processing * Wire up a readonly scheduler to prom api * Extract to exported function * Use health in api_prometheus and fix up tests * Rename health struct to status * Fix tests one more time * Several new tests * Handle inactive rules * Push state mapping into state manager * rename to StatusReader * Rectify cyclo complexity rebase * Convert existing package local status implementation to models one * fix tests * undo RuleDefs rename --- pkg/services/ngalert/api/api.go | 3 +- pkg/services/ngalert/api/api_prometheus.go | 49 +++++++++------ .../ngalert/api/api_prometheus_test.go | 6 ++ pkg/services/ngalert/api/testing.go | 26 ++++++++ pkg/services/ngalert/models/alert_rule.go | 8 +++ pkg/services/ngalert/ngalert.go | 1 + pkg/services/ngalert/schedule/alert_rule.go | 6 ++ .../ngalert/schedule/alert_rule_test.go | 20 ++++++ .../ngalert/schedule/recording_rule.go | 4 +- pkg/services/ngalert/schedule/registry.go | 8 +++ pkg/services/ngalert/schedule/schedule.go | 8 +++ .../ngalert/schedule/schedule_unit_test.go | 61 +++++++++++++++++++ pkg/services/ngalert/state/manager.go | 36 +++++++++++ 13 files changed, 213 insertions(+), 23 deletions(-) diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 0ef3a17f0f0..5057b831b08 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -63,6 +63,7 @@ type API struct { DataProxy *datasourceproxy.DataSourceProxyService MultiOrgAlertmanager *notifier.MultiOrgAlertmanager StateManager *state.Manager + Scheduler StatusReader AccessControl ac.AccessControl Policies *provisioning.NotificationPolicyService ReceiverService *notifier.ReceiverService @@ -115,7 +116,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { api.RegisterPrometheusApiEndpoints(NewForkingProm( api.DatasourceCache, NewLotexProm(proxy, logger), - &PrometheusSrv{log: logger, manager: api.StateManager, store: api.RuleStore, authz: ruleAuthzService}, + &PrometheusSrv{log: logger, manager: api.StateManager, status: api.Scheduler, store: api.RuleStore, authz: ruleAuthzService}, ), m) // Register endpoints for proxying to Cortex Ruler-compatible backends. api.RegisterRulerApiEndpoints(NewForkingRuler( diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 7953772e5b1..dfe2f01a996 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -9,7 +9,6 @@ import ( "sort" "strconv" "strings" - "time" "github.com/prometheus/alertmanager/pkg/labels" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" @@ -24,9 +23,14 @@ import ( "github.com/grafana/grafana/pkg/util" ) +type StatusReader interface { + Status(key ngmodels.AlertRuleKey) (ngmodels.RuleStatus, bool) +} + type PrometheusSrv struct { log log.Logger manager state.AlertInstanceManager + status StatusReader store RuleStore authz RuleAccessControlService } @@ -222,7 +226,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon namespaces[namespaceUID] = folder.Fullpath } - ruleResponse = PrepareRuleGroupStatuses(srv.log, srv.manager, srv.store, RuleGroupStatusesOptions{ + ruleResponse = PrepareRuleGroupStatuses(srv.log, srv.manager, srv.status, srv.store, RuleGroupStatusesOptions{ Ctx: c.Req.Context(), OrgID: c.OrgID, Query: c.Req.Form, @@ -235,7 +239,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) } -func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse { +func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse { ruleResponse := apimodels.RuleResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -346,7 +350,7 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager continue } - ruleGroup, totals := toRuleGroup(log, manager, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) + ruleGroup, totals := toRuleGroup(log, manager, status, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) ruleGroup.Totals = totals for k, v := range totals { rulesTotals[k] += v @@ -432,7 +436,7 @@ func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool { return true } -func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) { +func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, sr StatusReader, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) { newGroup := &apimodels.RuleGroup{ Name: groupKey.RuleGroup, // file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana. @@ -443,6 +447,15 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng ngmodels.RulesGroup(rules).SortByGroupIndex() for _, rule := range rules { + status, ok := sr.Status(rule.GetKey()) + // Grafana by design return "ok" health and default other fields for unscheduled rules. + // This differs from Prometheus. + if !ok { + status = ngmodels.RuleStatus{ + Health: "ok", + } + } + alertingRule := apimodels.AlertingRule{ State: "inactive", Name: rule.Title, @@ -454,9 +467,11 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng newRule := apimodels.Rule{ Name: rule.Title, Labels: apimodels.LabelsFromMap(rule.GetLabels(labelOptions...)), - Health: "ok", + Health: status.Health, + LastError: errorOrEmpty(status.LastError), Type: rule.Type().String(), - LastEvaluation: time.Time{}, + LastEvaluation: status.EvaluationTimestamp, + EvaluationTime: status.EvaluationDuration.Seconds(), } states := manager.GetStatesForRuleUID(rule.OrgID, rule.UID) @@ -485,12 +500,6 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng Value: valString, } - if alertState.LastEvaluationTime.After(newRule.LastEvaluation) { - newRule.LastEvaluation = alertState.LastEvaluationTime - } - - newRule.EvaluationTime = alertState.EvaluationDuration.Seconds() - switch alertState.State { case eval.Normal: case eval.Pending: @@ -503,14 +512,7 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng } alertingRule.State = "firing" case eval.Error: - newRule.Health = "error" case eval.NoData: - newRule.Health = "nodata" - } - - if alertState.Error != nil { - newRule.LastError = alertState.Error.Error() - newRule.Health = "error" } if len(withStates) > 0 { @@ -604,3 +606,10 @@ func encodedQueriesOrError(rules []ngmodels.AlertQuery) string { return err.Error() } + +func errorOrEmpty(err error) string { + if err != nil { + return err.Error() + } + return "" +} diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 478253e5646..497c3b4993f 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -489,6 +489,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { t.Run("should return sorted", func(t *testing.T) { ruleStore := fakes.NewRuleStore(t) fakeAIM := NewFakeAlertInstanceManager(t) + fakeSch := newFakeSchedulerReader(t).setupStates(fakeAIM) groupKey := ngmodels.GenerateGroupKey(orgID) gen := ngmodels.RuleGen rules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex()).GenerateManyRef(5, 10) @@ -497,6 +498,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, + status: fakeSch, store: ruleStore, authz: &fakeRuleAccessControlService{}, } @@ -558,6 +560,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, + status: newFakeSchedulerReader(t).setupStates(fakeAIM), store: ruleStore, authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())), } @@ -673,6 +676,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, + status: newFakeSchedulerReader(t).setupStates(fakeAIM), store: ruleStore, authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())), } @@ -1389,11 +1393,13 @@ func TestRouteGetRuleStatuses(t *testing.T) { func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) { fakeStore := fakes.NewRuleStore(t) fakeAIM := NewFakeAlertInstanceManager(t) + fakeSch := newFakeSchedulerReader(t).setupStates(fakeAIM) fakeAuthz := &fakeRuleAccessControlService{} api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, + status: fakeSch, store: fakeStore, authz: fakeAuthz, } diff --git a/pkg/services/ngalert/api/testing.go b/pkg/services/ngalert/api/testing.go index cb9dba0a02d..c219a96610e 100644 --- a/pkg/services/ngalert/api/testing.go +++ b/pkg/services/ngalert/api/testing.go @@ -166,3 +166,29 @@ func (f fakeRuleAccessControlService) AuthorizeDatasourceAccessForRule(ctx conte func (f fakeRuleAccessControlService) AuthorizeDatasourceAccessForRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error { return nil } + +type statesReader interface { + GetStatesForRuleUID(orgID int64, alertRuleUID string) []*state.State +} + +type fakeSchedulerReader struct { + states statesReader +} + +func newFakeSchedulerReader(t *testing.T) *fakeSchedulerReader { + return &fakeSchedulerReader{} +} + +// setupStates allows the fake scheduler to return data consistent with states defined elsewhere. +// This can be combined with fakeAlertInstanceManager, for instance. +func (f *fakeSchedulerReader) setupStates(reader statesReader) *fakeSchedulerReader { + f.states = reader + return f +} + +func (f *fakeSchedulerReader) Status(key models.AlertRuleKey) (models.RuleStatus, bool) { + if f.states == nil { + return models.RuleStatus{}, false + } + return state.StatesToRuleStatus(f.states.GetStatesForRuleUID(key.OrgID, key.UID)), true +} diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index bec7137c4b0..84cf3f4d4b3 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -886,3 +886,11 @@ func (r *Record) Fingerprint() data.Fingerprint { func hasAnyCondition(rule *AlertRuleWithOptionals) bool { return rule.Condition != "" || (rule.Record != nil && rule.Record.From != "") } + +// RuleStatus contains info about a rule's current evaluation state. +type RuleStatus struct { + Health string + LastError error + EvaluationTimestamp time.Time + EvaluationDuration time.Duration +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index b2628d6ae08..ab2c1f4d9b7 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -478,6 +478,7 @@ func (ng *AlertNG) init() error { ProvenanceStore: ng.store, MultiOrgAlertmanager: ng.MultiOrgAlertmanager, StateManager: ng.stateManager, + Scheduler: scheduler, AccessControl: ng.accesscontrol, Policies: policyService, ReceiverService: receiverService, diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index aa3218e7ad8..25ceaf47783 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -40,6 +40,8 @@ type Rule interface { Update(lastVersion RuleVersionAndPauseStatus) bool // Type gives the type of the rule. Type() ngmodels.RuleType + // Status indicates the status of the evaluating rule. + Status() ngmodels.RuleStatus } type ruleFactoryFunc func(context.Context, *ngmodels.AlertRule) Rule @@ -180,6 +182,10 @@ func (a *alertRule) Type() ngmodels.RuleType { return ngmodels.RuleTypeAlerting } +func (a *alertRule) Status() ngmodels.RuleStatus { + return a.stateManager.GetStatusForRuleUID(a.key.OrgID, a.key.UID) +} + // eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped. // Before sending a message into the channel, it does non-blocking read to make sure that there is no concurrent send operation. // Returns a tuple where first element is diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index d88637ddd65..0621204481d 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -369,6 +369,17 @@ func TestRuleRoutine(t *testing.T) { require.Equal(t, s.Labels, data.Labels(cmd.Labels)) }) + t.Run("status should accurately reflect latest evaluation", func(t *testing.T) { + states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.NotEmpty(t, states) + + status := ruleInfo.Status() + require.Equal(t, "ok", status.Health) + require.Nil(t, status.LastError) + require.Equal(t, states[0].LastEvaluationTime, status.EvaluationTimestamp) + require.Equal(t, states[0].EvaluationDuration, status.EvaluationDuration) + }) + t.Run("it reports metrics", func(t *testing.T) { // duration metric has 0 values because of mocked clock that do not advance expectedMetric := fmt.Sprintf( @@ -700,6 +711,15 @@ func TestRuleRoutine(t *testing.T) { assert.Len(t, args.PostableAlerts, 1) assert.Equal(t, state.ErrorAlertName, args.PostableAlerts[0].Labels[prometheusModel.AlertNameLabel]) }) + + t.Run("status should reflect unhealthy rule", func(t *testing.T) { + status := ruleInfo.Status() + require.Equal(t, "error", status.Health) + require.NotNil(t, status.LastError, "expected status to carry the latest evaluation error") + require.Contains(t, status.LastError.Error(), "cannot reference itself") + require.Equal(t, int64(0), status.EvaluationTimestamp.UTC().Unix()) + require.Equal(t, time.Duration(0), status.EvaluationDuration) + }) }) t.Run("when there are alerts that should be firing", func(t *testing.T) { diff --git a/pkg/services/ngalert/schedule/recording_rule.go b/pkg/services/ngalert/schedule/recording_rule.go index 4bcc5868f38..3abf44b6457 100644 --- a/pkg/services/ngalert/schedule/recording_rule.go +++ b/pkg/services/ngalert/schedule/recording_rule.go @@ -84,8 +84,8 @@ func (r *recordingRule) Type() ngmodels.RuleType { return ngmodels.RuleTypeRecording } -func (r *recordingRule) Status() RuleStatus { - return RuleStatus{ +func (r *recordingRule) Status() ngmodels.RuleStatus { + return ngmodels.RuleStatus{ Health: r.health.Load(), LastError: r.lastError.Load(), EvaluationTimestamp: r.evaluationTimestamp.Load(), diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index 157124373f5..2b162ca87be 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -56,6 +56,14 @@ func (r *ruleRegistry) exists(key models.AlertRuleKey) bool { return ok } +// get fetches a rule from the registry by key. It returns (rule, ok) where ok is false if the rule did not exist. +func (r *ruleRegistry) get(key models.AlertRuleKey) (Rule, bool) { + r.mu.Lock() + defer r.mu.Unlock() + ru, ok := r.rules[key] + return ru, ok +} + // del removes pair that has specific key from the registry. // Returns 2-tuple where the first element is value of the removed pair // and the second element indicates whether element with the specified key existed. diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index bd33a45ff1d..d05de8a74b0 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -171,6 +171,14 @@ func (sch *schedule) Rules() ([]*ngmodels.AlertRule, map[ngmodels.FolderKey]stri return sch.schedulableAlertRules.all() } +// Status fetches the health of a given scheduled rule, by key. +func (sch *schedule) Status(key ngmodels.AlertRuleKey) (ngmodels.RuleStatus, bool) { + if rule, ok := sch.registry.get(key); ok { + return rule.Status(), true + } + return ngmodels.RuleStatus{}, false +} + // deleteAlertRule stops evaluation of the rule, deletes it from active rules, and cleans up state cache. func (sch *schedule) deleteAlertRule(keys ...ngmodels.AlertRuleKey) { for _, key := range keys { diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 54b83cfb277..c75900a4d63 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -113,6 +113,11 @@ func TestProcessTicks(t *testing.T) { folderWithRuleGroup1 := fmt.Sprintf("%s;%s", ruleStore.getNamespaceTitle(alertRule1.NamespaceUID), alertRule1.RuleGroup) + t.Run("before 1st tick status should not be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.False(t, ok, "status for a rule should not be present before the scheduler has created it") + }) + t.Run("on 1st tick alert rule should be evaluated", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) @@ -137,12 +142,25 @@ func TestProcessTicks(t *testing.T) { require.NoError(t, err) }) + t.Run("after 1st tick status for rule should be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + // Interestingly, the rules in this test are randomised, and are sometimes invalid. + // Therefore, we can't reliably assert anything about the actual health. It might be error, it might not, depending on randomness. + // We are only testing that things were scheduled, not that the rule routine worked internally. + }) + // add alert rule under main org with three base intervals alertRule2 := gen.With(gen.WithOrgID(mainOrgID), gen.WithInterval(3*cfg.BaseInterval), gen.WithTitle("rule-2")).GenerateRef() ruleStore.PutRule(ctx, alertRule2) folderWithRuleGroup2 := fmt.Sprintf("%s;%s", ruleStore.getNamespaceTitle(alertRule2.NamespaceUID), alertRule2.RuleGroup) + t.Run("before 2nd tick status for rule should not be available", func(t *testing.T) { + _, ok := sched.Status(alertRule2.GetKey()) + require.False(t, ok, "status for a rule should not be present before the scheduler has created it") + }) + t.Run("on 2nd tick first alert rule should be evaluated", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick) @@ -184,6 +202,16 @@ func TestProcessTicks(t *testing.T) { assertEvalRun(t, evalAppliedCh, tick, keys...) }) + t.Run("after 3rd tick status for both rules should be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + _, ok = sched.Status(alertRule2.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + // Interestingly, the rules in this test are randomised, and are sometimes invalid. + // Therefore, we can't reliably assert anything about the actual health. It might be error, it might not, depending on randomness. + // We are only testing that things were scheduled, not that the rule routine worked internally. + }) + t.Run("on 4th tick only one alert rule should be evaluated", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick) @@ -223,6 +251,16 @@ func TestProcessTicks(t *testing.T) { require.NoError(t, err) }) + t.Run("after 5th tick status for both rules should be available regardless of pause state", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + _, ok = sched.Status(alertRule2.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + // Interestingly, the rules in this test are randomised, and are sometimes invalid. + // Therefore, we can't reliably assert anything about the actual health. It might be error, it might not, depending on randomness. + // We are only testing that things were scheduled, not that the rule routine worked internally. + }) + t.Run("on 6th tick all alert rule are paused (it still enters evaluation but it is early skipped)", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) @@ -309,6 +347,13 @@ func TestProcessTicks(t *testing.T) { require.NoError(t, err) }) + t.Run("after 8th tick status for deleted rule should not be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.False(t, ok, "status for a rule that was deleted should not be available") + _, ok = sched.Status(alertRule2.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + }) + t.Run("on 9th tick one alert rule should be evaluated", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) @@ -338,6 +383,14 @@ func TestProcessTicks(t *testing.T) { require.Emptyf(t, updated, "None rules are expected to be updated") assertEvalRun(t, evalAppliedCh, tick, alertRule3.GetKey()) }) + t.Run("after 10th tick status for remaining rules should be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.False(t, ok, "status for a rule that was deleted should not be available") + _, ok = sched.Status(alertRule2.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + _, ok = sched.Status(alertRule3.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + }) t.Run("on 11th tick rule2 should be updated", func(t *testing.T) { newRule2 := models.CopyRule(alertRule2) newRule2.Version++ @@ -465,6 +518,14 @@ func TestProcessTicks(t *testing.T) { require.Emptyf(t, updated, "No rules should be updated") }) + t.Run("after 12th tick no status should be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.False(t, ok, "status for a rule that was deleted should not be available") + _, ok = sched.Status(alertRule2.GetKey()) + require.False(t, ok, "status for a rule that just evaluated was not available") + _, ok = sched.Status(alertRule3.GetKey()) + require.False(t, ok, "status for a rule that just evaluated was not available") + }) t.Run("scheduled rules should be sorted", func(t *testing.T) { rules := gen.With(gen.WithOrgID(mainOrgID), gen.WithInterval(cfg.BaseInterval)).GenerateManyRef(10, 20) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index cbea45a5db3..fc23cd85998 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -556,6 +556,11 @@ func (st *Manager) GetStatesForRuleUID(orgID int64, alertRuleUID string) []*Stat return st.cache.getStatesForRuleUID(orgID, alertRuleUID, st.doNotSaveNormalState) } +func (st *Manager) GetStatusForRuleUID(orgID int64, alertRuleUID string) ngModels.RuleStatus { + states := st.GetStatesForRuleUID(orgID, alertRuleUID) + return StatesToRuleStatus(states) +} + func (st *Manager) Put(states []*State) { for _, s := range states { st.cache.set(s) @@ -623,3 +628,34 @@ func (st *Manager) deleteStaleStatesFromCache(ctx context.Context, logger log.Lo func stateIsStale(evaluatedAt time.Time, lastEval time.Time, intervalSeconds int64) bool { return !lastEval.Add(2 * time.Duration(intervalSeconds) * time.Second).After(evaluatedAt) } + +func StatesToRuleStatus(states []*State) ngModels.RuleStatus { + status := ngModels.RuleStatus{ + Health: "ok", + LastError: nil, + EvaluationTimestamp: time.Time{}, + } + for _, state := range states { + if state.LastEvaluationTime.After(status.EvaluationTimestamp) { + status.EvaluationTimestamp = state.LastEvaluationTime + } + + status.EvaluationDuration = state.EvaluationDuration + + switch state.State { + case eval.Normal: + case eval.Pending: + case eval.Alerting: + case eval.Error: + status.Health = "error" + case eval.NoData: + status.Health = "nodata" + } + + if state.Error != nil { + status.LastError = state.Error + status.Health = "error" + } + } + return status +} From 0c1aafd6437ce0b9f3cc3d429847099d1f4209c6 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Mon, 30 Sep 2024 18:50:55 -0400 Subject: [PATCH 096/174] Alerting: skip flaky test TestBroadcastAndHandleMessages (#94039) --- pkg/services/ngalert/notifier/redis_channel_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/services/ngalert/notifier/redis_channel_test.go b/pkg/services/ngalert/notifier/redis_channel_test.go index 62353e39b09..5c53039c5e8 100644 --- a/pkg/services/ngalert/notifier/redis_channel_test.go +++ b/pkg/services/ngalert/notifier/redis_channel_test.go @@ -29,6 +29,8 @@ func TestNewRedisChannel(t *testing.T) { } func TestBroadcastAndHandleMessages(t *testing.T) { + t.Skip() // TODO fix the flaky test https://github.com/grafana/grafana/issues/94037 + const channelName = "testChannel" mr, err := miniredis.Run() From 1c648fd010b5f886a2d7c1fd9dcd70e68bad528a Mon Sep 17 00:00:00 2001 From: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:24:47 -0400 Subject: [PATCH 097/174] Chore: Fix flaky cloud migration test (#94035) * attempt to fix flaky test * remove skip from test --- .../cloudmigration/cloudmigrationimpl/cloudmigration_test.go | 2 +- .../cloudmigration/cloudmigrationimpl/xorm_store_test.go | 4 +--- .../secrets/kvstore/migrations/to_plugin_mig_test.go | 5 +++-- pkg/services/secrets/kvstore/test_helpers.go | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index ea4d905e97f..4af1501181f 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -617,7 +617,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi featuremgmt.FlagDashboardRestore), sqlStore, dsService, - secretskv.NewFakeSQLSecretsKVStore(t), + secretskv.NewFakeSQLSecretsKVStore(t, sqlStore), secretsService, rr, prometheus.DefaultRegisterer, diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index 2de8a5025ee..51c7529495e 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -228,8 +228,6 @@ func Test_SnapshotResources(t *testing.T) { } func TestGetSnapshotList(t *testing.T) { - t.Skip("FLAKY test: disabled until fixed") - _, s := setUpTest(t) // Taken from setUpTest sessionUID := "qwerty" @@ -322,7 +320,7 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { s := &sqlStore{ db: testDB, secretsService: fakeSecrets.FakeSecretsService{}, - secretsStore: secretskv.NewFakeSQLSecretsKVStore(t), + secretsStore: secretskv.NewFakeSQLSecretsKVStore(t, testDB), } ctx := context.Background() diff --git a/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go b/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go index 1293ff6313e..b2ad6dbf956 100644 --- a/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go +++ b/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go @@ -100,12 +100,13 @@ func setupTestMigrateToPluginService(t *testing.T) (*MigrateToPluginService, sec raw, err := ini.Load([]byte(rawCfg)) require.NoError(t, err) cfg := &setting.Cfg{Raw: raw} + sqlStore := db.InitTestDB(t) + // this would be the plugin - mocked at the moment - fallbackStore := secretskvs.WithCache(secretskvs.NewFakeSQLSecretsKVStore(t), time.Minute*5, time.Minute*5) + fallbackStore := secretskvs.WithCache(secretskvs.NewFakeSQLSecretsKVStore(t, sqlStore), time.Minute*5, time.Minute*5) secretsStoreForPlugin := secretskvs.WithCache(secretskvs.NewFakePluginSecretsKVStore(t, featuremgmt.WithFeatures(), fallbackStore), time.Minute*5, time.Minute*5) // this is to init the sql secret store inside the migration - sqlStore := db.InitTestDB(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) manager := secretskvs.NewFakeSecretsPluginManager(t, false) migratorService := ProvideMigrateToPluginService( diff --git a/pkg/services/secrets/kvstore/test_helpers.go b/pkg/services/secrets/kvstore/test_helpers.go index 768a94d7085..7c19def969c 100644 --- a/pkg/services/secrets/kvstore/test_helpers.go +++ b/pkg/services/secrets/kvstore/test_helpers.go @@ -20,12 +20,12 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) -func NewFakeSQLSecretsKVStore(t *testing.T) *SecretsKVStoreSQL { +func NewFakeSQLSecretsKVStore(t *testing.T, sqlStore *sqlstore.SQLStore) *SecretsKVStoreSQL { t.Helper() - sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) return NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) } From 8b6cbae96b2ad2212a6c562a0ccc7bff894db8e4 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 06:22:27 +0100 Subject: [PATCH 098/174] I18n: Download translations from Crowdin (#94032) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 7 +++++++ public/locales/es-ES/grafana.json | 7 +++++++ public/locales/fr-FR/grafana.json | 7 +++++++ public/locales/pt-BR/grafana.json | 7 +++++++ public/locales/zh-Hans/grafana.json | 7 +++++++ 5 files changed, 35 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index cd5e88fc6cc..2aafc803723 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -216,6 +216,13 @@ "badge-tooltip-provenance": "", "badge-tooltip-standard": "" }, + "queryAndExpressionsStep": { + "disableAdvancedOptions": { + "text": "" + }, + "preview": "", + "previewCondition": "" + }, "rule-groups": { "delete": { "success": "" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 325dfe0ccc6..a3bf61617b9 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -216,6 +216,13 @@ "badge-tooltip-provenance": "", "badge-tooltip-standard": "" }, + "queryAndExpressionsStep": { + "disableAdvancedOptions": { + "text": "" + }, + "preview": "", + "previewCondition": "" + }, "rule-groups": { "delete": { "success": "" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index cb2c93d379e..803ca8b53b5 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -216,6 +216,13 @@ "badge-tooltip-provenance": "", "badge-tooltip-standard": "" }, + "queryAndExpressionsStep": { + "disableAdvancedOptions": { + "text": "" + }, + "preview": "", + "previewCondition": "" + }, "rule-groups": { "delete": { "success": "" diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index cc230ebd186..c77a4328b1b 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -216,6 +216,13 @@ "badge-tooltip-provenance": "", "badge-tooltip-standard": "" }, + "queryAndExpressionsStep": { + "disableAdvancedOptions": { + "text": "" + }, + "preview": "", + "previewCondition": "" + }, "rule-groups": { "delete": { "success": "" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 120535a2b8d..a599cb1e951 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -212,6 +212,13 @@ "badge-tooltip-provenance": "", "badge-tooltip-standard": "" }, + "queryAndExpressionsStep": { + "disableAdvancedOptions": { + "text": "" + }, + "preview": "", + "previewCondition": "" + }, "rule-groups": { "delete": { "success": "" From 1a31abe254b27f2b531586ff499b998d31eb3aba Mon Sep 17 00:00:00 2001 From: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Date: Tue, 1 Oct 2024 04:28:25 -0400 Subject: [PATCH 099/174] CloudMigrations: Limit frontend query to get latest snapshots (#93639) * latest param to endpoint and adapt frontend query * change to sort param * api * remove description --- pkg/services/cloudmigration/api/api.go | 1 + pkg/services/cloudmigration/api/api_test.go | 18 +++++++- .../fake/cloudmigration_fake.go | 18 +++++++- .../cloudmigrationimpl/xorm_store.go | 11 +++-- .../cloudmigrationimpl/xorm_store_test.go | 42 +++++++++++++++++-- pkg/services/cloudmigration/model.go | 1 + public/api-enterprise-spec.json | 3 ++ .../migrate-to-cloud/api/endpoints.gen.ts | 4 +- .../features/migrate-to-cloud/onprem/Page.tsx | 4 +- 9 files changed, 90 insertions(+), 12 deletions(-) diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index b647433e114..1cd851a9717 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -392,6 +392,7 @@ func (cma *CloudMigrationAPI) GetSnapshotList(c *contextmodel.ReqContext) respon SessionUID: uid, Limit: c.QueryInt("limit"), Page: c.QueryInt("page"), + Sort: c.Query("sort"), } if q.Limit == 0 { q.Limit = 100 diff --git a/pkg/services/cloudmigration/api/api_test.go b/pkg/services/cloudmigration/api/api_test.go index 5926276a5d0..1b56c8a4c20 100644 --- a/pkg/services/cloudmigration/api/api_test.go +++ b/pkg/services/cloudmigration/api/api_test.go @@ -395,7 +395,23 @@ func TestCloudMigrationAPI_GetSnapshotList(t *testing.T) { requestUrl: "/api/cloudmigration/migration/1234/snapshots", basicRole: org.RoleAdmin, expectedHttpResult: http.StatusOK, - expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z"},{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z"}]}`, + expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T17:30:40Z","finished":"0001-01-01T00:00:00Z"},{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T18:30:40Z","finished":"0001-01-01T00:00:00Z"}]}`, + }, + { + desc: "with limit query param should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234/snapshots?limit=1", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T17:30:40Z","finished":"0001-01-01T00:00:00Z"}]}`, + }, + { + desc: "with sort query param should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234/snapshots?sort=latest", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T18:30:40Z","finished":"0001-01-01T00:00:00Z"},{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T17:30:40Z","finished":"0001-01-01T00:00:00Z"}]}`, }, { desc: "should return 403 if no used is not admin", diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go index 250c837b14e..50ccfff8ef9 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go @@ -3,6 +3,7 @@ package fake import ( "context" "fmt" + "sort" "time" "github.com/grafana/grafana/pkg/services/cloudmigration" @@ -108,18 +109,31 @@ func (m FakeServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrati if m.ReturnError { return nil, fmt.Errorf("mock error") } - return []cloudmigration.CloudMigrationSnapshot{ + + cloudSnapshots := []cloudmigration.CloudMigrationSnapshot{ { UID: "fake_uid", SessionUID: query.SessionUID, Status: cloudmigration.SnapshotStatusCreating, + Created: time.Date(2024, 6, 5, 17, 30, 40, 0, time.UTC), }, { UID: "fake_uid", SessionUID: query.SessionUID, Status: cloudmigration.SnapshotStatusCreating, + Created: time.Date(2024, 6, 5, 18, 30, 40, 0, time.UTC), }, - }, nil + } + + if query.Sort == "latest" { + sort.Slice(cloudSnapshots, func(first, second int) bool { + return cloudSnapshots[first].Created.After(cloudSnapshots[second].Created) + }) + } + if query.Limit > 0 { + return cloudSnapshots[0:min(len(cloudSnapshots), query.Limit)], nil + } + return cloudSnapshots, nil } func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index 22de2474310..abccfbaf653 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -23,9 +23,10 @@ type sqlStore struct { } const ( - tableName = "cloud_migration_resource" - secretType = "cloudmigration-snapshot-encryption-key" - GetAllSnapshots = -1 + tableName = "cloud_migration_resource" + secretType = "cloudmigration-snapshot-encryption-key" + GetAllSnapshots = -1 + GetSnapshotListSortingLatest = "latest" ) func (ss *sqlStore) GetMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { @@ -278,7 +279,9 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li offset := (query.Page - 1) * query.Limit sess.Limit(query.Limit, offset) } - sess.OrderBy("cloud_migration_snapshot.created DESC") + if query.Sort == GetSnapshotListSortingLatest { + sess.OrderBy("cloud_migration_snapshot.created DESC") + } return sess.Find(&snapshots, &cloudmigration.CloudMigrationSnapshot{ SessionUID: query.SessionUID, }) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index 51c7529495e..58b98f6c1ad 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -3,7 +3,6 @@ package cloudmigrationimpl import ( "context" "encoding/base64" - "slices" "strconv" "testing" @@ -241,9 +240,46 @@ func TestGetSnapshotList(t *testing.T) { for _, snapshot := range snapshots { ids = append(ids, snapshot.UID) } - slices.Sort(ids) // There are 3 snapshots in the db but only 2 of them belong to this specific session. + assert.Equal(t, []string{"poiuy", "lkjhg"}, ids) + }) + + t.Run("returns only one snapshot that belongs to a session", func(t *testing.T) { + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 1}) + require.NoError(t, err) + assert.Len(t, snapshots, 1) + }) + + t.Run("return no snapshots if limit is set to 0", func(t *testing.T) { + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 0}) + require.NoError(t, err) + assert.Empty(t, snapshots) + }) + + t.Run("returns paginated snapshot that belongs to a session", func(t *testing.T) { + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 2, Limit: 1}) + require.NoError(t, err) + + ids := make([]string, 0) + for _, snapshot := range snapshots { + ids = append(ids, snapshot.UID) + } + + // Return paginated snapshot of the 2 belonging to this specific session + assert.Equal(t, []string{"lkjhg"}, ids) + }) + + t.Run("returns desc sorted list of snapshots that belong to a session", func(t *testing.T) { + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 100, Sort: "latest"}) + require.NoError(t, err) + + ids := make([]string, 0) + for _, snapshot := range snapshots { + ids = append(ids, snapshot.UID) + } + + // Return desc sorted snapshots belonging to this specific session assert.Equal(t, []string{"lkjhg", "poiuy"}, ids) }) @@ -345,7 +381,7 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { cloud_migration_snapshot (session_uid, uid, created, updated, finished, status) VALUES ('qwerty', 'poiuy', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), - ('qwerty', 'lkjhg', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), + ('qwerty', 'lkjhg', '2024-03-26 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), ('zxcvbn', 'mnbvvc', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"); `, ) diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 0eb8a8d04d6..786df738ca5 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -144,6 +144,7 @@ type ListSnapshotsQuery struct { SessionUID string Page int Limit int + Sort string } type UpdateSnapshotCmd struct { diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index ac04c23d278..e2d491516ff 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5416,6 +5416,9 @@ "message": { "type": "string" }, + "name": { + "type": "string" + }, "refId": { "type": "string" }, diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index 447ba395491..a3cba8abefb 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -41,7 +41,7 @@ const injectedRtkApi = api.injectEndpoints({ getShapshotList: build.query({ query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}/snapshots`, - params: { page: queryArg.page, limit: queryArg.limit }, + params: { page: queryArg.page, limit: queryArg.limit, sort: queryArg.sort }, }), }), getCloudMigrationToken: build.query({ @@ -112,6 +112,8 @@ export type GetShapshotListApiArg = { page?: number; /** Max limit for results returned. */ limit?: number; + /** Sort with value latest to return results sorted in descending order */ + sort?: string; /** Session UID of a session */ uid: string; }; diff --git a/public/app/features/migrate-to-cloud/onprem/Page.tsx b/public/app/features/migrate-to-cloud/onprem/Page.tsx index e091c4c11da..f4808b8f246 100644 --- a/public/app/features/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/migrate-to-cloud/onprem/Page.tsx @@ -70,7 +70,9 @@ const PAGE_SIZE = 50; function useGetLatestSnapshot(sessionUid?: string, page = 1) { const [shouldPoll, setShouldPoll] = useState(false); - const listResult = useGetShapshotListQuery(sessionUid ? { uid: sessionUid } : skipToken); + const listResult = useGetShapshotListQuery( + sessionUid ? { uid: sessionUid, page: 1, limit: 1, sort: 'latest' } : skipToken + ); const lastItem = listResult.currentData?.snapshots?.at(0); const getSnapshotQueryArgs = From d75fee5207c79c13dda9d03738072c18c03acf40 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:45:16 +0100 Subject: [PATCH 100/174] DataTrail: Remove newFiltersUI feature toggle usage from explore metrics (#93693) remove newFiltersUI feature toggle usage from explore metrics --- public/app/features/trails/DataTrail.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 3b4bdcce5b9..fd689bc5fb4 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -10,7 +10,7 @@ import { VariableHide, urlUtil, } from '@grafana/data'; -import { config, locationService, useChromeHeaderHeight } from '@grafana/runtime'; +import { locationService, useChromeHeaderHeight } from '@grafana/runtime'; import { AdHocFiltersVariable, ConstantVariable, @@ -650,7 +650,7 @@ function getVariableSet( addFilterButtonText: 'Add label', datasource: trailDS, hide: VariableHide.hideLabel, - layout: config.featureToggles.newFiltersUI ? 'combobox' : 'vertical', + layout: 'vertical', filters: initialFilters ?? [], baseFilters: getBaseFiltersForMetric(metric), applyMode: 'manual', From f8fd45892d14bdd94ae4739a740877fad5b0f51d Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 1 Oct 2024 10:58:08 +0200 Subject: [PATCH 101/174] Chore: Fix legend changing when using incremental querying (#93529) * rename variables * fix setting legend * yarn prettier:write * only update displayNameFromDS --- .../src/querycache/QueryCache.test.ts | 105 +++++++- .../src/querycache/QueryCache.ts | 57 ++-- .../src/querycache/QueryCacheTestData.ts | 255 ++++++++++++++++++ 3 files changed, 380 insertions(+), 37 deletions(-) diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts index 01cd25fd14f..28e10381b61 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts @@ -7,7 +7,11 @@ import { QueryEditorMode } from '../querybuilder/shared/types'; import { PromQuery } from '../types'; import { CacheRequestInfo, findDatapointStep, QueryCache } from './QueryCache'; -import { IncrementalStorageDataFrameScenarios, trimmedFirstPointInPromFrames } from './QueryCacheTestData'; +import { + differentDisplayNameFromDS, + trimmedFirstPointInPromFrames, + IncrementalStorageDataFrameScenarios, +} from './QueryCacheTestData'; // Will not interpolate vars! const interpolateStringTest = (query: PromQuery) => { @@ -124,7 +128,7 @@ describe('QueryCache: Generic', function () { }), { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }, firstFrames @@ -148,7 +152,7 @@ describe('QueryCache: Generic', function () { secondRequest, { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }, secondFrames @@ -241,7 +245,7 @@ describe('QueryCache: Prometheus', function () { request, { requests: [], // unused - targSigs: targetSignatures, + targetSignatures: targetSignatures, shouldCache: true, }, firstFrames @@ -265,7 +269,7 @@ describe('QueryCache: Prometheus', function () { secondRequest, { requests: [], // unused - targSigs: targetSignatures, + targetSignatures: targetSignatures, shouldCache: true, }, secondFrames @@ -387,9 +391,9 @@ describe('QueryCache: Prometheus', function () { panelId: panelId, }); - const requestInfo = { + const requestInfo: CacheRequestInfo = { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }; const targetSignature = `1=1|${interval}|${JSON.stringify(request.rangeRaw ?? '')}`; @@ -407,7 +411,7 @@ describe('QueryCache: Prometheus', function () { }), { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }, secondFrames @@ -430,7 +434,7 @@ describe('QueryCache: Prometheus', function () { }), { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }, thirdFrames @@ -537,7 +541,7 @@ describe('QueryCache: Prometheus', function () { request.targets[0].interval = '1m'; const requestInfo: CacheRequestInfo = { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }; const targetSignature = `1=1|${interval}|${JSON.stringify(request.rangeRaw ?? '')}`; @@ -558,6 +562,87 @@ describe('QueryCache: Prometheus', function () { expect(cacheRequest.requests[0]).toBe(request); expect(cacheRequest.shouldCache).toBe(true); }); + + it('should always use the newest config information', () => { + const storage = new QueryCache({ + getTargetSignature: getPrometheusTargetSignature, + overlapString: '10m', + }); + + // Initial request with custom legend info {{org}}-customLegend + const firstFrames = differentDisplayNameFromDS.first.dataFrames as unknown as DataFrame[]; + + // Second request with legend __auto which results having no displayNameFromDS + const secondFrames = differentDisplayNameFromDS.second.dataFrames as unknown as DataFrame[]; + + const cache = new Map(); + const interval = 15000; + + // start time of scenario + const firstFrom = dateTime(new Date(1726829205000)); + const firstTo = dateTime(new Date(1726829832515)); + const firstRange: TimeRange = { + from: firstFrom, + to: firstTo, + raw: { + from: 'now-1h', + to: 'now', + }, + }; + + const secondFrom = dateTime(new Date(1726829220000)); + const secondTo = dateTime(new Date(1726829903931)); + const secondRange: TimeRange = { + from: secondFrom, + to: secondTo, + raw: { + from: 'now-1h', + to: 'now', + }, + }; + + // Signifier definition + const dashboardId = `dashid`; + const panelId = 200; + const targetIdentity = `${dashboardId}|${panelId}|A`; + + const request = mockPromRequest({ + range: firstRange, + dashboardUID: dashboardId, + panelId: panelId, + app: 'first_app', + }); + + const requestInfo: CacheRequestInfo = { + requests: [], // unused + targetSignatures: cache, + shouldCache: true, + }; + const targetSignature = `1=1|${interval}|${JSON.stringify(request.rangeRaw ?? '')}`; + cache.set(targetIdentity, targetSignature); + + const firstQueryResult = storage.procFrames(request, requestInfo, firstFrames); + + expect(firstQueryResult[0].fields[1].config.displayNameFromDS).toBeDefined(); + expect(firstQueryResult[0].fields[1].config.displayNameFromDS).toEqual('rutgerkerkhoffdevuseast-customLegend'); + + const secondQueryResult = storage.procFrames( + mockPromRequest({ + range: secondRange, + dashboardUID: dashboardId, + panelId: panelId, + app: 'second_app', + }), + { + requests: [], // unused + targetSignatures: cache, + shouldCache: true, + }, + secondFrames + ); + + expect(secondQueryResult[0].fields[1].config.displayNameFromDS).not.toBeDefined(); + }); }); describe('findDataPointStep', () => { diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.ts b/packages/grafana-prometheus/src/querycache/QueryCache.ts index 9b06746b7cb..3afc3344f20 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.ts @@ -23,7 +23,7 @@ type TargetIdent = string; // query + template variables + interval + raw time range // used for full target cache busting -> full range re-query -type TargetSig = string; +type TargetSignature = string; type TimestampMs = number; type SupportedQueryTypes = PromQuery; type ApplyInterpolation = (str: string, scopedVars?: ScopedVars) => string; @@ -32,14 +32,14 @@ type ApplyInterpolation = (str: string, scopedVars?: ScopedVars) => string; export const defaultPrometheusQueryOverlapWindow = '10m'; interface TargetCache { - sig: TargetSig; + signature: TargetSignature; prevTo: TimestampMs; frames: DataFrame[]; } export interface CacheRequestInfo { requests: Array>; - targSigs: Map; + targetSignatures: Map; shouldCache: boolean; } @@ -48,13 +48,13 @@ export interface CacheRequestInfo { * This is the string used to uniquely identify a field within a "target" * @param field */ -export const getFieldIdent = (field: Field) => `${field.type}|${field.name}|${JSON.stringify(field.labels ?? '')}`; +export const getFieldIdentity = (field: Field) => `${field.type}|${field.name}|${JSON.stringify(field.labels ?? '')}`; /** * NOMENCLATURE * Target: The request target (DataQueryRequest), i.e. a specific query reference within a panel - * Ident: Identity: the string that is not expected to change - * Sig: Signature: the string that is expected to change, upon which we wipe the cache fields + * Identity: the string that is not expected to change + * Signature: the string that is expected to change, upon which we wipe the cache fields */ export class QueryCache { private overlapWindowMs: number; @@ -97,20 +97,20 @@ export class QueryCache { let doPartialQuery = shouldCache; let prevTo: TimestampMs | undefined = undefined; - // pre-compute reqTargSigs - const reqTargSigs = new Map(); - request.targets.forEach((targ) => { - let targIdent = `${request.dashboardUID}|${request.panelId}|${targ.refId}`; - let targSig = this.getTargetSignature(request, targ); // ${request.maxDataPoints} ? - reqTargSigs.set(targIdent, targSig); + // pre-compute reqTargetSignatures + const reqTargetSignatures = new Map(); + request.targets.forEach((target) => { + let targetIdentity = `${request.dashboardUID}|${request.panelId}|${target.refId}`; + let targetSignature = this.getTargetSignature(request, target); // ${request.maxDataPoints} ? + reqTargetSignatures.set(targetIdentity, targetSignature); }); // figure out if new query range or new target props trigger full cache invalidation & re-query - for (const [targIdent, targSig] of reqTargSigs) { - let cached = this.cache.get(targIdent); - let cachedSig = cached?.sig; + for (const [targetIdentity, targetSignature] of reqTargetSignatures) { + let cached = this.cache.get(targetIdentity); + let cachedSig = cached?.signature; - if (cachedSig !== targSig) { + if (cachedSig !== targetSignature) { doPartialQuery = false; } else { // only do partial queries when new request range follows prior request range (possibly with overlap) @@ -142,14 +142,14 @@ export class QueryCache { }, }; } else { - reqTargSigs.forEach((targSig, targIdent) => { + reqTargetSignatures.forEach((targSig, targIdent) => { this.cache.delete(targIdent); }); } return { requests: [request], - targSigs: reqTargSigs, + targetSignatures: reqTargetSignatures, shouldCache, }; } @@ -168,13 +168,13 @@ export class QueryCache { const respByTarget = new Map(); respFrames.forEach((frame: DataFrame) => { - let targIdent = `${request.dashboardUID}|${request.panelId}|${frame.refId}`; + let targetIdent = `${request.dashboardUID}|${request.panelId}|${frame.refId}`; - let frames = respByTarget.get(targIdent); + let frames = respByTarget.get(targetIdent); if (!frames) { frames = []; - respByTarget.set(targIdent, frames); + respByTarget.set(targetIdent, frames); } frames.push(frame); @@ -182,8 +182,8 @@ export class QueryCache { let outFrames: DataFrame[] = []; - respByTarget.forEach((respFrames, targIdent) => { - let cachedFrames = (targIdent ? this.cache.get(targIdent)?.frames : null) ?? []; + respByTarget.forEach((respFrames, targetIdentity) => { + let cachedFrames = (targetIdentity ? this.cache.get(targetIdentity)?.frames : null) ?? []; respFrames.forEach((respFrame: DataFrame) => { // skip empty frames @@ -193,9 +193,9 @@ export class QueryCache { // frames are identified by their second (non-time) field's name + labels // TODO: maybe also frame.meta.type? - let respFrameIdent = getFieldIdent(respFrame.fields[1]); + let respFrameIdentity = getFieldIdentity(respFrame.fields[1]); - let cachedFrame = cachedFrames.find((cached) => getFieldIdent(cached.fields[1]) === respFrameIdent); + let cachedFrame = cachedFrames.find((cached) => getFieldIdentity(cached.fields[1]) === respFrameIdentity); if (!cachedFrame) { // append new unknown frames @@ -213,6 +213,9 @@ export class QueryCache { if (amendedTable) { for (let i = 0; i < amendedTable.length; i++) { cachedFrame.fields[i].values = amendedTable[i]; + if (cachedFrame.fields[i].config.displayNameFromDS !== respFrame.fields[i].config.displayNameFromDS) { + cachedFrame.fields[i].config.displayNameFromDS = respFrame.fields[i].config.displayNameFromDS; + } } cachedFrame.length = cachedFrame.fields[0].values.length; } @@ -239,8 +242,8 @@ export class QueryCache { } }); - this.cache.set(targIdent, { - sig: requestInfo.targSigs.get(targIdent)!, + this.cache.set(targetIdentity, { + signature: requestInfo.targetSignatures.get(targetIdentity)!, frames: nonEmptyCachedFrames, prevTo: newTo, }); diff --git a/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts b/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts index 91aca344f2f..ae283225f1b 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts @@ -1006,3 +1006,258 @@ export const IncrementalStorageDataFrameScenarios = { }, }, }; + +export const differentDisplayNameFromDS = { + first: { + dataFrames: [ + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + executedQueryString: + 'Expr: sum by (org, stackId)(count_over_time(hosted_grafana:grafana_datasource_loki_orgs_stacks_queries:count2m{org=~".*", stackId=~".*"}[5m]))\nStep: 5m0s', + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'rutgerkerkhoffdevuseast', + stackId: '2791', + }, + config: { + displayNameFromDS: 'rutgerkerkhoffdevuseast-customLegend', + }, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'securityops', + stackId: '1533', + }, + config: { + displayNameFromDS: 'securityops-customLegend', + }, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'stephaniehingtgen', + stackId: '3740', + }, + config: { + displayNameFromDS: 'stephaniehingtgen-customLegend', + }, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + ], + }, + second: { + dataFrames: [ + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + executedQueryString: + 'Expr: sum by (org, stackId)(count_over_time(hosted_grafana:grafana_datasource_loki_orgs_stacks_queries:count2m{org=~".*", stackId=~".*"}[5m]))\nStep: 5m0s', + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'rutgerkerkhoffdevuseast', + stackId: '2791', + }, + config: {}, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'securityops', + stackId: '1533', + }, + config: {}, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'stephaniehingtgen', + stackId: '3740', + }, + config: {}, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + ], + }, +}; From 299fe3e5b1b229aaf38fb5a5ae3c734bf3abe7ea Mon Sep 17 00:00:00 2001 From: Robert Goltz Date: Tue, 1 Oct 2024 10:59:23 +0200 Subject: [PATCH 102/174] Chore: bump module github.com/rs/cors from v1.10.1 to v1.11.1 (#93363) * Chore: Update module github.com/rs/cors to v1.11.0 * Fix: rs/cors to v1.11.1 to benefit from fix regarding support for multiple Access-Control-Request-Headers field, e.g. API Gateway * update go.sum --------- Co-authored-by: Jo --- go.mod | 2 +- go.sum | 3 ++- go.work.sum | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 8c1da0d6802..af4a8d08e8f 100644 --- a/go.mod +++ b/go.mod @@ -393,7 +393,7 @@ require ( github.com/redis/rueidis v1.0.45 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/cors v1.10.1 // @grafana/identity-access-team + github.com/rs/cors v1.11.1 // @grafana/identity-access-team github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/go.sum b/go.sum index f7d7f5ef080..41e1a216dca 100644 --- a/go.sum +++ b/go.sum @@ -3056,8 +3056,9 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= diff --git a/go.work.sum b/go.work.sum index 0b97fda9088..8e7044ed205 100644 --- a/go.work.sum +++ b/go.work.sum @@ -828,6 +828,7 @@ github.com/relvacode/iso8601 v1.4.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= From 9144e3b44a2327b9ce30c75c2ec258264f510585 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 1 Oct 2024 11:28:33 +0200 Subject: [PATCH 103/174] Navigation: Fix empty admin menu (#94024) --- pkg/services/navtree/navtreeimpl/navtree.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 0fe4e27e31c..47095e0c1ef 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -167,6 +167,10 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere if sec := treeRoot.FindById(navtree.NavIDCfgAccess); sec != nil && len(sec.Children) == 0 { treeRoot.RemoveSectionByID(navtree.NavIDCfgAccess) } + // double-check and remove admin menu if empty + if sec := treeRoot.FindById(navtree.NavIDCfg); sec != nil && len(sec.Children) == 0 { + treeRoot.RemoveSectionByID(navtree.NavIDCfg) + } if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPinNavItems) { treeRoot.AddSection(&navtree.NavLink{ From 8338e92a70daaea41e7b824cbd7672926f467d69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:30:32 +0100 Subject: [PATCH 104/174] Update dependency sass-loader to v16 (#94006) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 1d94cc0a482..12500d11759 100644 --- a/package.json +++ b/package.json @@ -223,7 +223,7 @@ "rimraf": "6.0.1", "rudder-sdk-js": "2.48.18", "sass": "1.79.3", - "sass-loader": "14.2.1", + "sass-loader": "16.0.2", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", "stylelint": "16.9.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 5833a424a3d..08c7f213e9f 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -132,7 +132,7 @@ "rollup-plugin-esbuild": "6.1.1", "rollup-plugin-node-externals": "^7.1.3", "sass": "1.79.3", - "sass-loader": "14.2.1", + "sass-loader": "16.0.2", "style-loader": "4.0.0", "testing-library-selector": "0.3.1", "ts-node": "10.9.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 3f81f708616..b44d5cd61a9 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -181,7 +181,7 @@ "rollup-plugin-esbuild": "6.1.1", "rollup-plugin-node-externals": "^7.1.3", "rollup-plugin-svg-import": "3.0.0", - "sass-loader": "14.2.1", + "sass-loader": "16.0.2", "storybook": "^8.1.6", "storybook-dark-mode": "^4.0.1", "style-loader": "4.0.0", diff --git a/yarn.lock b/yarn.lock index 42b0af03219..d407735adc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4056,7 +4056,7 @@ __metadata: rollup-plugin-node-externals: "npm:^7.1.3" rxjs: "npm:7.8.1" sass: "npm:1.79.3" - sass-loader: "npm:14.2.1" + sass-loader: "npm:16.0.2" semver: "npm:7.6.3" style-loader: "npm:4.0.0" testing-library-selector: "npm:0.3.1" @@ -4380,7 +4380,7 @@ __metadata: rollup-plugin-node-externals: "npm:^7.1.3" rollup-plugin-svg-import: "npm:3.0.0" rxjs: "npm:7.8.1" - sass-loader: "npm:14.2.1" + sass-loader: "npm:16.0.2" slate: "npm:0.47.9" slate-plain-serializer: "npm:0.7.13" slate-react: "npm:0.22.10" @@ -19214,7 +19214,7 @@ __metadata: rudder-sdk-js: "npm:2.48.18" rxjs: "npm:7.8.1" sass: "npm:1.79.3" - sass-loader: "npm:14.2.1" + sass-loader: "npm:16.0.2" selecto: "npm:1.26.3" semver: "npm:7.6.3" slate: "npm:0.47.9" @@ -29186,9 +29186,9 @@ __metadata: languageName: node linkType: hard -"sass-loader@npm:14.2.1": - version: 14.2.1 - resolution: "sass-loader@npm:14.2.1" +"sass-loader@npm:16.0.2": + version: 16.0.2 + resolution: "sass-loader@npm:16.0.2" dependencies: neo-async: "npm:^2.6.2" peerDependencies: @@ -29208,7 +29208,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10/9cb864fd8d4c4f73d05f6cedae9ff4500f15fa742385e1f1cffcc0f994270810288fe99009f233ac6516fdc497570ce21f53c63f079c70e841c1e5bf994bc27d + checksum: 10/a24ae7f6b7ef2159274b47c0a7e35cb3bfb56367a192d362ff7674f2b85b2c7bfd021e83e9351d3af36b66524efd85ebf70e242971c0b807af6622ba4ee8e298 languageName: node linkType: hard From 137da12c992917a6cdb421acba2d17c0ccd2b88b Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Tue, 1 Oct 2024 12:45:31 +0300 Subject: [PATCH 105/174] Fix panel search (#94043) --- .../features/dashboard-scene/scene/PanelSearchLayout.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx index 343ccef5d8d..4334ef92114 100644 --- a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx +++ b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { SceneGridLayout, VizPanel, sceneGraph } from '@grafana/scenes'; +import { VizPanel, sceneGraph } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; @@ -11,6 +11,7 @@ import { activateInActiveParents } from '../utils/utils'; import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; export interface Props { dashboard: DashboardScene; @@ -25,11 +26,13 @@ export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: const panels: VizPanel[] = []; const styles = useStyles2(getStyles); - if (!(body instanceof SceneGridLayout)) { + const bodyGrid = body instanceof DefaultGridLayoutManager ? body.state.grid : null; + + if (!bodyGrid) { return Unsupported layout; } - for (const gridItem of body.state.children) { + for (const gridItem of bodyGrid.state.children) { if (gridItem instanceof DashboardGridItem) { const panel = gridItem.state.body; const interpolatedTitle = sceneGraph.interpolate(dashboard, panel.state.title).toLowerCase(); From a8b94fe203867c2f6b12a1cde9a3389d4fff28ae Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 1 Oct 2024 10:55:48 +0100 Subject: [PATCH 106/174] SingleTopNav: Move org switcher into Megamenu header (#94053) move org switcher into megamenu header --- .../AppChrome/MegaMenu/MegaMenuHeader.tsx | 5 +++-- .../OrganizationSwitcher.tsx | 18 +++++++++++++----- .../AppChrome/TopBar/SingleTopBar.tsx | 2 -- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx index 6f0d1feeb62..cfce7392d31 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx @@ -1,11 +1,12 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { IconButton, Stack, Text, ToolbarButton, useTheme2 } from '@grafana/ui'; +import { IconButton, Stack, ToolbarButton, useTheme2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { t } from 'app/core/internationalization'; import { Branding } from '../../Branding/Branding'; +import { OrganizationSwitcher } from '../OrganizationSwitcher/OrganizationSwitcher'; import { TOP_BAR_LEVEL_HEIGHT } from '../types'; export interface Props { @@ -26,7 +27,7 @@ export function MegaMenuHeader({ handleMegaMenu, handleDockedMenu, onClose }: Pr - {Branding.AppTitle} + { - setIsSmallScreen(!e.matches); + setIsSmallScreen(!config.featureToggles.singleTopNav && !e.matches); }, }); if (orgs?.length <= 1) { - return null; + if (config.featureToggles.singleTopNav) { + return {Branding.AppTitle}; + } else { + return null; + } } const Switcher = isSmallScreen ? OrganizationPicker : OrganizationSelect; diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index 2052979f2c8..40db95e4000 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -16,7 +16,6 @@ import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs'; import { buildBreadcrumbs } from '../../Breadcrumbs/utils'; import { enrichHelpItem } from '../MegaMenu/utils'; import { NewsContainer } from '../News/NewsContainer'; -import { OrganizationSwitcher } from '../OrganizationSwitcher/OrganizationSwitcher'; import { QuickAdd } from '../QuickAdd/QuickAdd'; import { TOP_BAR_LEVEL_HEIGHT } from '../types'; @@ -59,7 +58,6 @@ export const SingleTopBar = memo(function SingleTopBar({ )} - From 38ad0d3ebfaebf2946c9eb64f5eec9ab7292a6e6 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:08:47 +0200 Subject: [PATCH 107/174] Alerting docs: relocate `Intro>Notifications>Templates` (#94057) * Alerting docs: relocate `Intro>Notifications>Templates` * Rename to `Templates` --- .../fundamentals/{notifications => }/templates.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename docs/sources/alerting/fundamentals/{notifications => }/templates.md (92%) diff --git a/docs/sources/alerting/fundamentals/notifications/templates.md b/docs/sources/alerting/fundamentals/templates.md similarity index 92% rename from docs/sources/alerting/fundamentals/notifications/templates.md rename to docs/sources/alerting/fundamentals/templates.md index 2154b82ceef..42b0900ff4c 100644 --- a/docs/sources/alerting/fundamentals/notifications/templates.md +++ b/docs/sources/alerting/fundamentals/templates.md @@ -1,10 +1,11 @@ --- aliases: - - ../../contact-points/message-templating/ # /docs/grafana//alerting/contact-points/message-templating/ - - ../../alert-rules/message-templating/ # /docs/grafana//alerting/alert-rules/message-templating/ - - ../../unified-alerting/message-templating/ # /docs/grafana//alerting/unified-alerting/message-templating/ + - ../fundamentals/notifications/templates/ # /docs/grafana//alerting/fundamentals/notifications/templates/ + - ../contact-points/message-templating/ # /docs/grafana//alerting/contact-points/message-templating/ + - ../alert-rules/message-templating/ # /docs/grafana//alerting/alert-rules/message-templating/ + - ../unified-alerting/message-templating/ # /docs/grafana//alerting/unified-alerting/message-templating/ canonical: https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/templates/ -description: Learn about templates +description: Use templating to customize, format, and reuse alert notification messages. Create more flexible and informative alert notification messages by incorporating dynamic content, such as metric values, labels, and other contextual information. keywords: - grafana - alerting From 95d379368a87dbb24bd37f404226193f85bb7833 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 1 Oct 2024 13:23:21 +0300 Subject: [PATCH 108/174] Announcement banner: Enable feature toggle by default (#94041) * Announcement banner: Enable by default * Update feature stage --- .../feature-toggles/index.md | 2 +- pkg/services/featuremgmt/registry.go | 3 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.json | 42 ++++++++++--------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 2b3b25d5a08..7b536cf07d2 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -72,6 +72,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | | `newDashboardSharingComponent` | Enables the new sharing drawer design | | +| `notificationBanner` | Enables the notification banner UI and API | Yes | | `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | | `openSearchBackendFlowEnabled` | Enables the backend query flow for Open Search datasource plugin | Yes | | `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes | @@ -186,7 +187,6 @@ Experimental features might be changed or removed without prior notice. | `queryLibrary` | Enables Query Library feature in Explore | | `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | | `alertingListViewV2` | Enables the new alert list view design | -| `notificationBanner` | Enables the notification banner UI and API | | `dashboardRestore` | Enables deleted dashboard restore feature (backend only) | | `alertingCentralAlertHistory` | Enables the new central alert history. | | `pinNavItems` | Enables pinning of nav items | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b2eefd0622b..84c26aba608 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1237,9 +1237,10 @@ var ( { Name: "notificationBanner", Description: "Enables the notification banner UI and API", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, Owner: grafanaFrontendPlatformSquad, FrontendOnly: false, + Expression: "true", }, { Name: "dashboardRestore", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ffdf26a0e3c..b11709ae1b1 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -161,7 +161,7 @@ queryLibrary,experimental,@grafana/explore-squad,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true -notificationBanner,experimental,@grafana/grafana-frontend-platform,false,false,false +notificationBanner,GA,@grafana/grafana-frontend-platform,false,false,false dashboardRestore,experimental,@grafana/search-and-storage,false,false,false datasourceProxyDisableRBAC,GA,@grafana/identity-access-team,false,false,false alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0868229e1c7..1430f24481a 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2108,13 +2108,17 @@ { "metadata": { "name": "notificationBanner", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-05-13T09:32:34Z" + "resourceVersion": "1727777007488", + "creationTimestamp": "2024-05-13T09:32:34Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-10-01 10:03:27.48823 +0000 UTC" + } }, "spec": { "description": "Enables the notification banner UI and API", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform" + "stage": "GA", + "codeowner": "@grafana/grafana-frontend-platform", + "expression": "true" } }, { @@ -2999,6 +3003,20 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "unifiedStorageSearch", + "resourceVersion": "1726771421439", + "creationTimestamp": "2024-09-19T18:43:41Z" + }, + "spec": { + "description": "Enable unified storage search", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "useSeessionStorageForRedirection", @@ -3024,20 +3042,6 @@ "codeowner": "@grafana/identity-access-team" } }, - { - "metadata": { - "name": "unifiedStorageSearch", - "resourceVersion": "1726771421439", - "creationTimestamp": "2024-09-19T18:43:41Z" - }, - "spec": { - "description": "Enable unified storage search", - "stage": "experimental", - "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, - "hideFromDocs": true - } - }, { "metadata": { "name": "vizActions", @@ -3092,4 +3096,4 @@ } } ] -} +} \ No newline at end of file From 8de1047f65482ad35be9692dd3a4903e6513b559 Mon Sep 17 00:00:00 2001 From: Tim Levett Date: Tue, 1 Oct 2024 05:31:31 -0500 Subject: [PATCH 109/174] Change from Apps to "More Apps" (#93454) * Change the label used for additional app links from apps to more apps so it doesn't conflict with applications, which is application observability, not the other bucket * update to more apps * more in german is mehr * fix case, update translations correctly * revert changes to de * fix be tests --------- Co-authored-by: joshhunt Co-authored-by: Ashley Harrison --- pkg/services/navtree/navtreeimpl/applinks.go | 2 +- pkg/services/navtree/navtreeimpl/applinks_test.go | 8 ++++---- public/app/core/utils/navBarItem-translations.ts | 2 +- public/locales/en-US/grafana.json | 2 +- public/locales/pseudo-LOCALE/grafana.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index e9fb15d950d..ec7f9ee65bf 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -195,7 +195,7 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n switch sectionID { case navtree.NavIDApps: treeRoot.AddSection(&navtree.NavLink{ - Text: "Apps", + Text: "More apps", Icon: "layer-group", SubTitle: "App plugins that extend the Grafana experience", Id: navtree.NavIDApps, diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index fdac09b8450..40978045b44 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -121,14 +121,14 @@ func TestAddAppLinks(t *testing.T) { }, } - t.Run("Should move apps to Apps category", func(t *testing.T) { + t.Run("Should move apps to 'More apps' category", func(t *testing.T) { treeRoot := navtree.NavTreeRoot{} err := service.addAppLinks(&treeRoot, reqCtx) require.NoError(t, err) appsNode := treeRoot.FindById(navtree.NavIDApps) require.NotNil(t, appsNode) - require.Equal(t, "Apps", appsNode.Text) + require.Equal(t, "More apps", appsNode.Text) require.Len(t, appsNode.Children, 3) require.Equal(t, testApp1.Name, appsNode.Children[0].Text) }) @@ -169,7 +169,7 @@ func TestAddAppLinks(t *testing.T) { require.Len(t, treeRoot.Children, 2) require.Equal(t, "plugin-page-test-app1", treeRoot.Children[0].Id) - // Check if it is not under the "Apps" section anymore + // Check if it is not under the "More apps" section anymore appsNode := treeRoot.FindById(navtree.NavIDApps) require.NotNil(t, appsNode) require.Len(t, appsNode.Children, 2) @@ -197,7 +197,7 @@ func TestAddAppLinks(t *testing.T) { require.Len(t, adminNode.Children, 1) require.Equal(t, "plugin-page-test-app1", adminNode.Children[0].Id) - // Check if it is not under the "Apps" section anymore + // Check if it is not under the "More apps" section anymore appsNode := treeRoot.FindById(navtree.NavIDApps) require.NotNil(t, appsNode) require.Len(t, appsNode.Children, 2) diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 57be9f87a5f..d5a664526af 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -140,7 +140,7 @@ export function getNavTitle(navId: string | undefined) { case 'frontend': return t('nav.frontend.title', 'Frontend'); case 'apps': - return t('nav.apps.title', 'Apps'); + return t('nav.apps.title', 'More apps'); case 'alerts-and-incidents': return t('nav.alerts-and-incidents.title', 'Alerts & IRM'); case 'testing-and-synthetics': diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 310d59feacb..0c9b09b6853 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1574,7 +1574,7 @@ }, "apps": { "subtitle": "App plugins that extend the Grafana experience", - "title": "Apps" + "title": "More apps" }, "authentication": { "title": "Authentication" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index b8107773274..8e39227ae2f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1574,7 +1574,7 @@ }, "apps": { "subtitle": "Åpp pľūģįʼnş ŧĥäŧ ęχŧęʼnđ ŧĥę Ğřäƒäʼnä ęχpęřįęʼnčę", - "title": "Åppş" + "title": "Mőřę äppş" }, "authentication": { "title": "Åūŧĥęʼnŧįčäŧįőʼn" From a20ebbc8f8937a2d4a69eeaa40eb0408b95e581c Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 1 Oct 2024 14:11:58 +0300 Subject: [PATCH 110/174] Routing: Replace useHistory hook (#94061) * Update ConfigureIRM * Update Browse --- .../configuration-tracker/components/ConfigureIRM.tsx | 9 +++++---- public/app/features/plugins/admin/pages/Browse.test.tsx | 9 ++------- public/app/features/plugins/admin/pages/Browse.tsx | 5 ++--- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/public/app/features/gops/configuration-tracker/components/ConfigureIRM.tsx b/public/app/features/gops/configuration-tracker/components/ConfigureIRM.tsx index 802e7a7091c..a2822e767c9 100644 --- a/public/app/features/gops/configuration-tracker/components/ConfigureIRM.tsx +++ b/public/app/features/gops/configuration-tracker/components/ConfigureIRM.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { GrafanaTheme2 } from '@grafana/data'; import { IconName, Text, useStyles2 } from '@grafana/ui'; @@ -13,6 +13,7 @@ import { useGetConfigurationForUI, useGetEssentialsConfiguration } from '../irmH import { ConfigCard } from './ConfigCard'; import { Essentials } from './Essentials'; + export interface IrmCardConfiguration { id: number; title: string; @@ -40,7 +41,7 @@ function useGetDataSourceConfiguration(): DataSourceConfigurationData { export function ConfigureIRM() { const styles = useStyles2(getStyles); - const history = useHistory(); + const navigate = useNavigate(); // get all the configuration data const dataSourceConfigurationData = useGetDataSourceConfiguration(); @@ -72,9 +73,9 @@ export function ConfigureIRM() { switch (configID) { case ConfigurationStepsEnum.CONNECT_DATASOURCE: if (isDone) { - history.push(DATASOURCES_ROUTES.List); + navigate(DATASOURCES_ROUTES.List); } else { - history.push(DATASOURCES_ROUTES.New); + navigate(DATASOURCES_ROUTES.New); } break; case ConfigurationStepsEnum.ESSENTIALS: diff --git a/public/app/features/plugins/admin/pages/Browse.test.tsx b/public/app/features/plugins/admin/pages/Browse.test.tsx index 48ac0897243..f270e4d0352 100644 --- a/public/app/features/plugins/admin/pages/Browse.test.tsx +++ b/public/app/features/plugins/admin/pages/Browse.test.tsx @@ -3,13 +3,11 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { PluginType, escapeStringForRegex } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; -import { RouteDescriptor } from 'app/core/navigation/types'; import { configureStore } from 'app/store/configureStore'; import { getCatalogPluginMock, getPluginsStateMock } from '../__mocks__'; import { fetchRemotePlugins } from '../state/actions'; -import { PluginAdminRoutes, CatalogPlugin, ReducerState, RequestStatus } from '../types'; +import { CatalogPlugin, ReducerState, RequestStatus } from '../types'; import BrowsePage from './Browse'; @@ -30,13 +28,10 @@ const renderBrowse = ( ): RenderResult => { const store = configureStore({ plugins: pluginsStateOverride || getPluginsStateMock(plugins) }); locationService.push(path); - const props = getRouteComponentProps({ - route: { routeName: PluginAdminRoutes.Home } as RouteDescriptor, - }); return render( - + ); }; diff --git a/public/app/features/plugins/admin/pages/Browse.tsx b/public/app/features/plugins/admin/pages/Browse.tsx index 5153498edb7..58857662c54 100644 --- a/public/app/features/plugins/admin/pages/Browse.tsx +++ b/public/app/features/plugins/admin/pages/Browse.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { ReactElement, useState } from 'react'; +import { useState } from 'react'; import { useLocation } from 'react-router-dom-v5-compat'; import { SelectableValue, GrafanaTheme2, PluginType } from '@grafana/data'; @@ -7,7 +7,6 @@ import { locationSearchToObject } from '@grafana/runtime'; import { Select, RadioButtonGroup, useStyles2, Tooltip, Field, Button } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { Trans } from 'app/core/internationalization'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; import { useSelector } from 'app/types'; @@ -21,7 +20,7 @@ import { Sorters } from '../helpers'; import { useHistory } from '../hooks/useHistory'; import { useGetAll, useGetUpdatable, useIsRemotePluginsAvailable } from '../state/hooks'; -export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null { +export default function Browse() { const location = useLocation(); const locationSearch = locationSearchToObject(location.search); const navModel = useSelector((state) => getNavModel(state.navIndex, 'plugins')); From 2a73b89374a43ca37f42e5527535bdf8fa4c9368 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Tue, 1 Oct 2024 07:17:59 -0400 Subject: [PATCH 111/174] Prometheus: Add resource for suggestions that include scopes/adhoc filters (#94001) Co-authored-by: Dominik Prokop Co-authored-by: Bogdan Matei --- .../as-admin-user/variableEditPage.spec.ts | 1 + packages/grafana-prometheus/src/datasource.ts | 42 ++++++- .../src/language_provider.ts | 62 ++++++++++ pkg/promlib/library.go | 10 +- pkg/promlib/library_test.go | 52 ++++++++ pkg/promlib/models/scope.go | 4 +- pkg/promlib/resource/resource.go | 112 ++++++++++++++++++ 7 files changed, 277 insertions(+), 6 deletions(-) diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts index 75e7b943536..b93d9a8e5dd 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts @@ -5,6 +5,7 @@ import { prometheusLabels } from '../mocks/resources'; test('variable query with mocked response', async ({ variableEditPage, page }) => { variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); + variableEditPage.mockResourceResponse('suggestions*', prometheusLabels); await variableEditPage.datasource.set('gdev-prometheus'); await variableEditPage.getByGrafanaSelector('Query type').fill('Label names'); await page.keyboard.press('Tab'); diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index cb416fa8f0c..e69327b718e 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -74,7 +74,13 @@ import { import { PrometheusVariableSupport } from './variables'; const ANNOTATION_QUERY_STEP_DEFAULT = '60s'; -const GET_AND_POST_METADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels']; +const GET_AND_POST_METADATA_ENDPOINTS = [ + 'api/v1/query', + 'api/v1/query_range', + 'api/v1/series', + 'api/v1/labels', + 'suggestions', +]; export const InstantQueryRefIdIndex = '-Instant'; @@ -263,7 +269,9 @@ export class PrometheusDatasource .join('&'); } } else { - options.headers!['Content-Type'] = 'application/x-www-form-urlencoded'; + if (!options.headers!['Content-Type']) { + options.headers!['Content-Type'] = 'application/x-www-form-urlencoded'; + } options.data = data; } @@ -599,6 +607,20 @@ export class PrometheusDatasource // it is used in metric_find_query.ts // and in Tempo here grafana/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx async getTagKeys(options: DataSourceGetTagKeysOptions): Promise { + if (config.featureToggles.promQLScope && !!options) { + const suggestions = await this.languageProvider.fetchSuggestions( + options.timeRange, + options.queries, + options.scopes, + options.filters + ); + + // filter out already used labels and empty labels + return suggestions + .filter((labelName) => !!labelName && !options.filters.find((filter) => filter.key === labelName)) + .map((k) => ({ value: k, text: k })); + } + if (!options || options.filters.length === 0) { await this.languageProvider.fetchLabels(options.timeRange, options.queries); return this.languageProvider.getLabelKeys().map((k) => ({ value: k, text: k })); @@ -621,6 +643,21 @@ export class PrometheusDatasource // By implementing getTagKeys and getTagValues we add ad-hoc filters functionality async getTagValues(options: DataSourceGetTagValuesOptions) { + const requestId = `[${this.uid}][${options.key}]`; + if (config.featureToggles.promQLScope) { + return ( + await this.languageProvider.fetchSuggestions( + options.timeRange, + options.queries, + options.scopes, + options.filters, + options.key, + undefined, + requestId + ) + ).map((v) => ({ value: v, text: v })); + } + const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({ label: f.key, value: f.value, @@ -630,7 +667,6 @@ export class PrometheusDatasource const expr = promQueryModeller.renderLabels(labelFilters); if (this.hasLabelsMatchAPISupport()) { - const requestId = `[${this.uid}][${options.key}]`; return ( await this.languageProvider.fetchSeriesValuesWithMatch(options.key, expr, requestId, options.timeRange) ).map((v) => ({ diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 102ec109a90..d645c3ba74b 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -6,8 +6,12 @@ import { AbstractLabelMatcher, AbstractLabelOperator, AbstractQuery, + AdHocVariableFilter, getDefaultTimeRange, LanguageProvider, + Scope, + scopeFilterOperatorMap, + ScopeSpecFilter, TimeRange, } from '@grafana/data'; import { BackendSrvRequest } from '@grafana/runtime'; @@ -407,6 +411,64 @@ export default class PromQlLanguageProvider extends LanguageProvider { const values = await Promise.all(DEFAULT_KEYS.map((key) => this.fetchLabelValues(key))); return DEFAULT_KEYS.reduce((acc, key, i) => ({ ...acc, [key]: values[i] }), {}); }); + + /** + * Fetch labels or values for a label based on the queries, scopes, filters and time range + * @param timeRange + * @param queries + * @param scopes + * @param adhocFilters + * @param labelName + * @param limit + * @param requestId + */ + fetchSuggestions = async ( + timeRange?: TimeRange, + queries?: PromQuery[], + scopes?: Scope[], + adhocFilters?: AdHocVariableFilter[], + labelName?: string, + limit?: number, + requestId?: string + ): Promise => { + if (timeRange) { + this.timeRange = timeRange; + } + + const url = '/suggestions'; + const timeParams = this.datasource.getAdjustedInterval(this.timeRange); + const value = await this.request( + url, + [], + { + labelName, + queries: queries?.map((q) => q.expr), + scopes: scopes?.reduce((acc, scope) => { + acc.push(...scope.spec.filters); + + return acc; + }, []), + adhocFilters: adhocFilters?.map((filter) => ({ + key: filter.key, + operator: scopeFilterOperatorMap[filter.operator], + value: filter.value, + values: filter.values, + })), + limit, + ...timeParams, + }, + { + ...(requestId && { requestId }), + headers: { + ...this.getDefaultCacheHeaders()?.headers, + 'Content-Type': 'application/json', + }, + method: 'POST', + } + ); + + return value ?? []; + }; } function getNameLabelValue(promQuery: string, tokens: Array): string { diff --git a/pkg/promlib/library.go b/pkg/promlib/library.go index bd7d48b2fc1..1f35360be54 100644 --- a/pkg/promlib/library.go +++ b/pkg/promlib/library.go @@ -109,7 +109,8 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq return err } - if strings.EqualFold(req.Path, "version-detect") { + switch { + case strings.EqualFold(req.Path, "version-detect"): versionObj, found := i.versionCache.Get("version") if found { return sender.Send(versionObj.(*backend.CallResourceResponse)) @@ -121,6 +122,13 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq } i.versionCache.Set("version", vResp, cache.DefaultExpiration) return sender.Send(vResp) + + case strings.EqualFold(req.Path, "suggestions"): + resp, err := i.resource.GetSuggestions(ctx, req) + if err != nil { + return err + } + return sender.Send(resp) } resp, err := i.resource.Execute(ctx, req) diff --git a/pkg/promlib/library_test.go b/pkg/promlib/library_test.go index 9bdf5d504bd..b5d5d6235bb 100644 --- a/pkg/promlib/library_test.go +++ b/pkg/promlib/library_test.go @@ -117,6 +117,19 @@ func TestService(t *testing.T) { err := service.CallResource(context.Background(), req, sender) require.NoError(t, err) }) + + t.Run("suggest resource", func(t *testing.T) { + f := &fakeHTTPClientProvider{} + httpProvider := getMockPromTestSDKProvider(f) + l := backend.NewLoggerWith("logger", "test") + service := NewService(httpProvider, l, mockExtendTransportOptions) + + req := mockSuggestResource() + sender := &fakeSender{} + err := service.CallResource(context.Background(), req, sender) + require.NoError(t, err) + require.Equal(t, `http://localhost:9090/api/v1/labels?end=2022-06-01T12%3A00%3A00Z&limit=10&match%5B%5D=go_cgo_go_to_c_calls_calls_total%7Bjob%3D~%22.%2B%22%7D&match%5B%5D=up%7Bjob%3D~%22.%2B%22%7D&start=2022-06-01T00%3A00%3A00Z`, f.Roundtripper.Req.URL.String()) + }) } func mockRequest() *backend.CallResourceRequest { @@ -146,3 +159,42 @@ func mockRequest() *backend.CallResourceRequest { Body: []byte("match%5B%5D: ALERTS\nstart: 1655271408\nend: 1655293008"), } } + +func mockSuggestResource() *backend.CallResourceRequest { + return &backend.CallResourceRequest{ + PluginContext: backend.PluginContext{ + OrgID: 0, + PluginID: "prometheus", + User: nil, + AppInstanceSettings: nil, + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + ID: 0, + UID: "", + Type: "prometheus", + Name: "test-prom", + URL: "http://localhost:9090", + User: "", + Database: "", + BasicAuthEnabled: true, + BasicAuthUser: "admin", + Updated: time.Time{}, + JSONData: []byte("{}"), + }, + }, + Path: "suggestions", + URL: "suggestions", + Method: http.MethodPost, + Body: []byte(` + { + "queries": ["up + 1", "go_cgo_go_to_c_calls_calls_total + 2"], + "scopes": [{ + "key": "job", + "value": ".+", + "operator": "regex-match" + }], + "start": "2022-06-01T00:00:00Z", + "end": "2022-06-01T12:00:00Z", + "limit": 10 + }`), + } +} diff --git a/pkg/promlib/models/scope.go b/pkg/promlib/models/scope.go index ec6c8f95b28..3fd7a62b8eb 100644 --- a/pkg/promlib/models/scope.go +++ b/pkg/promlib/models/scope.go @@ -15,7 +15,7 @@ func ApplyFiltersAndGroupBy(rawExpr string, scopeFilters, adHocFilters []ScopeFi return "", err } - matchers, err := filtersToMatchers(scopeFilters, adHocFilters) + matchers, err := FiltersToMatchers(scopeFilters, adHocFilters) if err != nil { return "", err } @@ -70,7 +70,7 @@ func ApplyFiltersAndGroupBy(rawExpr string, scopeFilters, adHocFilters []ScopeFi return expr.String(), nil } -func filtersToMatchers(scopeFilters, adhocFilters []ScopeFilter) ([]*labels.Matcher, error) { +func FiltersToMatchers(scopeFilters, adhocFilters []ScopeFilter) ([]*labels.Matcher, error) { filterMap := make(map[string]*labels.Matcher) for _, filter := range append(scopeFilters, adhocFilters...) { diff --git a/pkg/promlib/resource/resource.go b/pkg/promlib/resource/resource.go index 888267d4138..bf880d5564a 100644 --- a/pkg/promlib/resource/resource.go +++ b/pkg/promlib/resource/resource.go @@ -3,14 +3,19 @@ package resource import ( "bytes" "context" + "encoding/json" "fmt" "net/http" + "net/url" + "slices" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data/utils/maputil" + "github.com/prometheus/prometheus/promql/parser" "github.com/grafana/grafana/pkg/promlib/client" + "github.com/grafana/grafana/pkg/promlib/models" "github.com/grafana/grafana/pkg/promlib/utils" ) @@ -84,3 +89,110 @@ func (r *Resource) DetectVersion(ctx context.Context, req *backend.CallResourceR return r.Execute(ctx, newReq) } + +func getSelectors(expr string) ([]string, error) { + parsed, err := parser.ParseExpr(expr) + if err != nil { + return nil, err + } + + selectors := make([]string, 0) + + parser.Inspect(parsed, func(node parser.Node, nodes []parser.Node) error { + switch v := node.(type) { + case *parser.VectorSelector: + for _, matcher := range v.LabelMatchers { + if matcher == nil { + continue + } + if matcher.Name == "__name__" { + selectors = append(selectors, matcher.Value) + } + } + } + return nil + }) + + return selectors, nil +} + +// SuggestionRequest is the request body for the GetSuggestions resource. +type SuggestionRequest struct { + // LabelName, if provided, will result in label values being returned for the given label name. + LabelName string `json:"labelName"` + + Queries []string `json:"queries"` + + Scopes []models.ScopeFilter `json:"scopes"` + AdhocFilters []models.ScopeFilter `json:"adhocFilters"` + + // Start and End are proxied directly to the prometheus endpoint (which is rfc3339 | unix_timestamp) + Start string `json:"start"` + End string `json:"end"` + + // Limit is the maximum number of suggestions to return and is proxied directly to the prometheus endpoint. + Limit int64 `json:"limit"` +} + +// GetSuggestions takes a Suggestion Request in the body of the resource request. +// It builds a to call prometheus' labels endpoint (or label values endpoint if labelName is provided) +// The match parameters for the endpoints are built from metrics extracted from the queries +// combined with the scopes and adhoc filters provided in the request. +// Queries must be valid raw promql. +func (r *Resource) GetSuggestions(ctx context.Context, req *backend.CallResourceRequest) (*backend.CallResourceResponse, error) { + sugReq := SuggestionRequest{} + err := json.Unmarshal(req.Body, &sugReq) + if err != nil { + return nil, fmt.Errorf("error unmarshalling suggestion request: %v", err) + } + + selectorList := []string{} + for _, query := range sugReq.Queries { + s, err := getSelectors(query) + if err != nil { + return nil, fmt.Errorf("error parsing selectors: %v", err) + } + selectorList = append(selectorList, s...) + } + + slices.Sort(selectorList) + selectorList = slices.Compact(selectorList) + + matchers, err := models.FiltersToMatchers(sugReq.Scopes, sugReq.AdhocFilters) + if err != nil { + return nil, fmt.Errorf("error converting filters to matchers: %v", err) + } + + values := url.Values{} + + for _, s := range selectorList { + vs := parser.VectorSelector{Name: s, LabelMatchers: matchers} + values.Add("match[]", vs.String()) + } + + if sugReq.Start != "" { + values.Add("start", sugReq.Start) + } + if sugReq.End != "" { + values.Add("end", sugReq.End) + } + if sugReq.Limit > 0 { + values.Add("limit", fmt.Sprintf("%d", sugReq.Limit)) + } + + newReq := &backend.CallResourceRequest{ + PluginContext: req.PluginContext, + } + + if sugReq.LabelName != "" { + // Get label values for the given name (key) + newReq.Path = "/api/v1/label/" + sugReq.LabelName + "/values" + newReq.URL = "/api/v1/label/" + sugReq.LabelName + "/values?" + values.Encode() + } else { + // Get Label names (keys) + newReq.Path = "/api/v1/labels" + newReq.URL = "/api/v1/labels?" + values.Encode() + } + + return r.Execute(ctx, newReq) +} From e399fe6d092bcafa0c0177ba024124bac2feaa78 Mon Sep 17 00:00:00 2001 From: "Arati R." <33031346+suntala@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:03:02 +0200 Subject: [PATCH 112/174] Folders: Set folder creation permission as part of legacy create (#94040) * Add folder store to dashboard permissions * Include folder store in annotation scope resolver * Add folder store when initialising library elements * Include folder store in search v2 service initialisation * Include folder store in GetInheritedScopes * Add folder store to folder permissions provider * Include cfg, folder permissions in folder service * Move setting of folder permissions for folder service create method --- pkg/api/annotations.go | 4 +- pkg/api/annotations_test.go | 7 +- pkg/api/dashboard_test.go | 2 +- pkg/api/folder.go | 38 --------- pkg/api/folder_bench_test.go | 9 +- pkg/api/http_server.go | 3 +- .../ossaccesscontrol/dashboard.go | 4 +- .../accesscontrol/ossaccesscontrol/folder.go | 4 +- .../ossaccesscontrol/testutil/testutil.go | 83 +++++++++++++++++++ .../annotationsimpl/annotations_test.go | 6 +- pkg/services/dashboards/accesscontrol.go | 41 +++++---- pkg/services/dashboards/accesscontrol_test.go | 34 ++++---- .../database/database_folder_test.go | 4 +- .../dashboards/database/database_test.go | 10 ++- .../dashboards/service/dashboard_service.go | 4 +- pkg/services/folder/folderimpl/folder.go | 49 ++++++++++- pkg/services/folder/folderimpl/folder_test.go | 22 +++-- .../guardian/accesscontrol_guardian_test.go | 9 +- pkg/services/libraryelements/accesscontrol.go | 4 +- .../libraryelements/libraryelements.go | 4 +- .../libraryelements/libraryelements_test.go | 5 +- .../librarypanels/librarypanels_test.go | 10 ++- .../ngalert/api/api_provisioning_test.go | 4 +- .../ngalert/provisioning/alert_rules_test.go | 4 +- pkg/services/ngalert/testutil/testutil.go | 3 +- pkg/services/searchV2/auth.go | 12 +-- pkg/services/searchV2/index_test.go | 4 +- pkg/services/searchV2/service.go | 10 +-- pkg/services/searchV2/service_bench_test.go | 4 +- .../sqlstore/permissions/dashboard_test.go | 6 +- .../permissions/dashboards_bench_test.go | 3 +- 31 files changed, 269 insertions(+), 137 deletions(-) create mode 100644 pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 5612fd4a639..df6afa33be1 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -587,7 +587,7 @@ func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Res // where is the type of annotation with id . // If annotationPermissionUpdate feature toggle is enabled, dashboard annotation scope will be resolved to the corresponding // dashboard and folder scopes (eg, "dashboards:uid:", "folders:uid:" etc). -func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, folderSvc folder.Service) (string, accesscontrol.ScopeAttributeResolver) { +func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, folderStore folder.Store) (string, accesscontrol.ScopeAttributeResolver) { prefix := accesscontrol.ScopeAnnotationsProvider.GetResourceScope("") return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) { scopeParts := strings.Split(initialScope, ":") @@ -649,7 +649,7 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature // Append dashboard parent scopes if dashboard is in a folder or the general scope if dashboard is not in a folder if dashboard.FolderUID != "" { scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID)) - inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc) + inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderStore) if err != nil { return nil, err } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 12eea783e5d..5fb09aa7baf 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -397,14 +397,15 @@ func TestAPI_Annotations(t *testing.T) { dashService := &dashboards.FakeDashboardService{} dashService.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{UID: dashUID, FolderUID: folderUID, FolderID: 1}, nil) folderService := &foldertest.FakeService{} + fStore := folder.NewFakeStore() folderService.ExpectedFolder = &folder.Folder{UID: folderUID, ID: 1} folderDB := &foldertest.FakeFolderStore{} folderDB.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(&folder.Folder{UID: folderUID, ID: 1}, nil) hs.DashboardService = dashService hs.folderService = folderService hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) - hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, folderService)) - hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderDB, dashService, folderService)) + hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, fStore)) + hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderDB, dashService, fStore)) }) var body io.Reader if tt.body != "" { @@ -504,7 +505,7 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { features := featuremgmt.WithFeatures(tc.featureToggles...) - prefix, resolver := AnnotationTypeScopeResolver(fakeAnnoRepo, features, dashSvc, &foldertest.FakeService{}) + prefix, resolver := AnnotationTypeScopeResolver(fakeAnnoRepo, features, dashSvc, folder.NewFakeStore()) require.Equal(t, "annotations:id:", prefix) resolved, err := resolver.Resolve(context.Background(), 1, tc.given) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 42bd4a84956..2a561916821 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -836,7 +836,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr db := db.InitTestDB(t) fStore := folderimpl.ProvideStore(db) folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), - dashboardStore, folderStore, db, features, + dashboardStore, folderStore, db, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) if dashboardService == nil { dashboardService, err = service.ProvideDashboardServiceImpl( diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 5456100cd0f..c8d7949b48f 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -1,7 +1,6 @@ package api import ( - "context" "errors" "net/http" "strconv" @@ -12,12 +11,10 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" - "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/apimachinery/identity" folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/metrics" internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" @@ -31,7 +28,6 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements/model" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errhttp" @@ -219,10 +215,6 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response return apierrors.ToFolderErrorResponse(err) } - if err := hs.setDefaultFolderPermissions(c.Req.Context(), cmd.OrgID, cmd.SignedInUser, folder); err != nil { - hs.log.Error("Could not set the default folder permissions", "folder", folder.Title, "user", cmd.SignedInUser, "error", err) - } - // Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call // Required for cases when caller wants to immediately interact with the newly created object hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser) @@ -236,36 +228,6 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response return response.JSON(http.StatusOK, folderDTO) } -func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, folder *folder.Folder) error { - if !hs.Cfg.RBAC.PermissionsOnCreation("folder") { - return nil - } - - var permissions []accesscontrol.SetResourcePermissionCommand - - if user.IsIdentityType(claims.TypeUser) { - userID, err := user.GetInternalID() - if err != nil { - return err - } - - permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ - UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(), - }) - } - - isNested := folder.ParentUID != "" - if !isNested || !hs.Features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { - permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()}, - {BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()}, - }...) - } - - _, err := hs.folderPermissionsService.SetPermissions(ctx, orgID, folder.UID, permissions...) - return err -} - // swagger:route POST /folders/{folder_uid}/move folders moveFolder // // Move folder. diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 3d5113c715c..590a4ea0b66 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -464,14 +464,15 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), ) fStore := folderimpl.ProvideStore(sc.db) - folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, - folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( - cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets) + cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, fStore, acSvc, sc.teamSvc, sc.userSvc, actionSets) require.NoError(b, err) + folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, + folderStore, sc.db, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( - cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets) + cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, fStore, acSvc, sc.teamSvc, sc.userSvc, actionSets) require.NoError(b, err) dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 04fed03bcca..f9908deaadf 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -253,6 +253,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi authInfoService login.AuthInfoService, storageService store.StorageService, notificationService notifications.Service, dashboardService dashboards.DashboardService, dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service, + folderStore folder.Store, dsGuardian guardian.DatasourceGuardianProvider, dashboardsnapshotsService dashboardsnapshots.Service, pluginSettings pluginSettings.Service, avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, @@ -379,7 +380,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi hs.registerRoutes() // Register access control scope resolver for annotations - hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, features, dashboardService, folderService)) + hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, features, dashboardService, folderStore)) if err := hs.declareFixedRoles(); err != nil { return nil, err diff --git a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go index 85e5d5a4960..8010ef19c27 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go @@ -93,7 +93,7 @@ func registerDashboardRoles(cfg *setting.Cfg, features featuremgmt.FeatureToggle func ProvideDashboardPermissions( cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, folderStore folder.Store, service accesscontrol.Service, teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService, ) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { @@ -145,7 +145,7 @@ func ProvideDashboardPermissions( } parentScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID) - nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderService) + nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderStore) if err != nil { return nil, err } diff --git a/pkg/services/accesscontrol/ossaccesscontrol/folder.go b/pkg/services/accesscontrol/ossaccesscontrol/folder.go index 8904567d935..1af283a81c2 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/folder.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/folder.go @@ -84,7 +84,7 @@ func registerFolderRoles(cfg *setting.Cfg, features featuremgmt.FeatureToggles, func ProvideFolderPermissions( cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, folderStore folder.Store, service accesscontrol.Service, teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService, ) (*FolderPermissionsService, error) { if err := registerFolderRoles(cfg, features, service); err != nil { @@ -111,7 +111,7 @@ func ProvideFolderPermissions( return nil }, InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { - return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderService) + return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderStore) }, Assignments: resourcepermissions.Assignments{ Users: true, diff --git a/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go new file mode 100644 index 00000000000..9dbed99aa67 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go @@ -0,0 +1,83 @@ +package testutil + +import ( + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/licensing/licensingtest" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry" + "github.com/grafana/grafana/pkg/services/team/teamimpl" + "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/setting" +) + +func ProvideFolderPermissions( + features featuremgmt.FeatureToggles, + cfg *setting.Cfg, + sqlStore *sqlstore.SQLStore, +) (*ossaccesscontrol.FolderPermissionsService, error) { + actionSets := resourcepermissions.NewActionSetService(features) + acSvc := acimpl.ProvideOSSService( + cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(), + features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sqlStore, permreg.ProvidePermissionRegistry(), + ) + + license := licensingtest.NewFakeLicensing() + license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() + + ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) + + fStore := folderimpl.ProvideStore(sqlStore) + + quotaService := quotatest.New(false, nil) + orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) + if err != nil { + return nil, err + } + teamSvc, err := teamimpl.ProvideService(sqlStore, cfg, tracing.InitializeTracerForTest()) + if err != nil { + return nil, err + } + cache := localcache.ProvideService() + + userSvc, err := userimpl.ProvideService( + sqlStore, + orgService, + cfg, + teamSvc, + cache, + tracing.InitializeTracerForTest(), + quotaService, + bundleregistry.ProvideService(), + ) + if err != nil { + return nil, err + } + + return ossaccesscontrol.ProvideFolderPermissions( + cfg, + features, + routing.NewRouteRegister(), + sqlStore, + ac, + license, + &dashboards.FakeDashboardStore{}, + fStore, + acSvc, + teamSvc, + userSvc, + actionSets, + ) +} diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index 4263acc340c..6a1f9e56a2f 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + ftestutil "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/testutil" "github.com/grafana/grafana/pkg/services/authz/zanzana" @@ -227,10 +228,11 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { }) ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()) + folderPermissions, err := ftestutil.ProvideFolderPermissions(features, cfg, sql) + require.NoError(t, err) fStore := folderimpl.ProvideStore(sql) folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, - folderimpl.ProvideDashboardFolderStore(sql), sql, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - + folderimpl.ProvideDashboardFolderStore(sql), sql, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) cfg.AnnotationMaximumTagsLength = 60 store := NewXormStore(cfg, log.New("annotation.test"), sql, tagService) diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index c4b53df4c87..57dafdd454c 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -43,7 +43,7 @@ var ( ) // NewFolderNameScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope. -func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeFoldersProvider.GetResourceScopeName("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewFolderNameScopeResolver") @@ -63,7 +63,7 @@ func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Se return nil, err } - result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderStore) if err != nil { return nil, err } @@ -74,7 +74,7 @@ func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Se } // NewFolderIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:id:" into an uid based scope. -func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeFoldersProvider.GetResourceScope("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewFolderIDScopeResolver") @@ -98,7 +98,7 @@ func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Serv return nil, err } - result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderStore) if err != nil { return nil, err } @@ -110,7 +110,7 @@ func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Serv // NewFolderUIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:uid:" // into uid based scopes for folder and its parents -func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewFolderUIDScopeResolver(folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeFoldersProvider.GetResourceScopeUID("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewFolderUIDScopeResolver") @@ -125,7 +125,7 @@ func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttrib return nil, err } - inheritedScopes, err := GetInheritedScopes(ctx, orgID, uid, folderSvc) + inheritedScopes, err := GetInheritedScopes(ctx, orgID, uid, folderStore) if err != nil { return nil, err } @@ -135,7 +135,7 @@ func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttrib // NewDashboardIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:id:" // into uid based scopes for both dashboard and folder -func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeDashboardsProvider.GetResourceScope("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewDashboardIDScopeResolver") @@ -155,13 +155,13 @@ func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardServic return nil, err } - return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderSvc) + return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderStore) }) } // NewDashboardUIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:uid:" // into uid based scopes for both dashboard and folder -func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeDashboardsProvider.GetResourceScopeUID("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewDashboardUIDScopeResolver") @@ -181,11 +181,11 @@ func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardServi return nil, err } - return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderSvc) + return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderStore) }) } -func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) { +func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, orgID int64, dashboard *Dashboard, folderStore folder.Store) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.resolveDashboardScope") span.End() @@ -208,7 +208,7 @@ func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, org folderUID = folder.UID } - result, err := GetInheritedScopes(ctx, orgID, folderUID, folderSvc) + result, err := GetInheritedScopes(ctx, orgID, folderUID, folderStore) if err != nil { return nil, err } @@ -223,17 +223,24 @@ func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, org return result, nil } -func GetInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderSvc folder.Service) ([]string, error) { +func GetInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderStore folder.Store) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.GetInheritedScopes") span.End() if folderUID == ac.GeneralFolderUID { return nil, nil } - ancestors, err := folderSvc.GetParents(ctx, folder.GetParentsQuery{ - UID: folderUID, - OrgID: orgID, - }) + + var ancestors []*folder.Folder + var err error + if folderUID == folder.SharedWithMeFolderUID { + ancestors = []*folder.Folder{&folder.SharedWithMeFolder} + } else { + ancestors, err = folderStore.GetParents(ctx, folder.GetParentsQuery{ + UID: folderUID, + OrgID: orgID, + }) + } if err != nil { if errors.Is(err, folder.ErrFolderNotFound) { diff --git a/pkg/services/dashboards/accesscontrol_test.go b/pkg/services/dashboards/accesscontrol_test.go index 57e1d8e80e0..ead4055ac3f 100644 --- a/pkg/services/dashboards/accesscontrol_test.go +++ b/pkg/services/dashboards/accesscontrol_test.go @@ -18,7 +18,7 @@ import ( func TestNewFolderNameScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + prefix, _ := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) require.Equal(t, "folders:name:", prefix) }) @@ -31,7 +31,7 @@ func TestNewFolderNameScopeResolver(t *testing.T) { scope := "folders:name:" + title - _, resolver := NewFolderNameScopeResolver(folderStore, foldertest.NewFakeService()) + _, resolver := NewFolderNameScopeResolver(folderStore, folder.NewFakeStore()) resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope) require.NoError(t, err) require.Len(t, resolvedScopes, 1) @@ -49,8 +49,8 @@ func TestNewFolderNameScopeResolver(t *testing.T) { scope := "folders:name:" + title - folderSvc := foldertest.NewFakeService() - folderSvc.ExpectedFolders = []*folder.Folder{ + fStore := folder.NewFakeStore() + fStore.ExpectedParentFolders = []*folder.Folder{ { UID: "parent", }, @@ -58,7 +58,7 @@ func TestNewFolderNameScopeResolver(t *testing.T) { UID: "grandparent", }, } - _, resolver := NewFolderNameScopeResolver(folderStore, folderSvc) + _, resolver := NewFolderNameScopeResolver(folderStore, fStore) resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope) require.NoError(t, err) @@ -75,20 +75,20 @@ func TestNewFolderNameScopeResolver(t *testing.T) { folderStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title, mock.Anything) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) { - _, resolver := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:name:") require.ErrorIs(t, err, ac.ErrInvalidScope) }) t.Run("returns 'not found' if folder does not exist", func(t *testing.T) { folderStore := foldertest.NewFakeFolderStore(t) - _, resolver := NewFolderNameScopeResolver(folderStore, foldertest.NewFakeService()) + _, resolver := NewFolderNameScopeResolver(folderStore, folder.NewFakeStore()) orgId := rand.Int63() folderStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrDashboardNotFound).Once() @@ -103,12 +103,12 @@ func TestNewFolderNameScopeResolver(t *testing.T) { func TestNewFolderIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + prefix, _ := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) require.Equal(t, "folders:id:", prefix) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:uid:123") require.ErrorIs(t, err, ac.ErrInvalidScope) @@ -118,7 +118,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { var ( orgId = rand.Int63() scope = "folders:id:0" - _, resolver = NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver = NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) ) resolved, err := resolver.Resolve(context.Background(), orgId, scope) @@ -129,7 +129,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { }) t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) { - _, resolver := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:") require.ErrorIs(t, err, ac.ErrInvalidScope) @@ -137,7 +137,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { t.Run("returns 'not found' if folder does not exist", func(t *testing.T) { folderStore := foldertest.NewFakeFolderStore(t) folderStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrDashboardNotFound).Once() - _, resolver := NewFolderIDScopeResolver(folderStore, foldertest.NewFakeService()) + _, resolver := NewFolderIDScopeResolver(folderStore, folder.NewFakeStore()) orgId := rand.Int63() scope := "folders:id:10" @@ -149,12 +149,12 @@ func TestNewFolderIDScopeResolver(t *testing.T) { func TestNewDashboardIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + prefix, _ := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, folder.NewFakeStore()) require.Equal(t, "dashboards:id:", prefix) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + _, resolver := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:uid:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) @@ -162,12 +162,12 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { func TestNewDashboardUIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + prefix, _ := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, folder.NewFakeStore()) require.Equal(t, "dashboards:uid:", prefix) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + _, resolver := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:id:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index 177f0ba3e52..b792facc2c4 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -303,8 +303,10 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { guardian.New = origNewGuardian }) + folderPermissions := mock.NewMockedPermissionsService() folderStore := folderimpl.ProvideStore(sqlStore) - folderSvc := folderimpl.ProvideService(folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + folderSvc := folderimpl.ProvideService(folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), + sqlStore, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) parentUID := "" for i := 0; ; i++ { diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 73d85983569..83259785a69 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -16,6 +16,8 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -829,10 +831,11 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false) ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()) + folderPermissions := mock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) fStore := folderimpl.ProvideStore(sqlStore) folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, - folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + folderStore, sqlStore, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) user := &user.SignedInUser{ OrgID: 1, @@ -951,8 +954,11 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) fStore := folderimpl.ProvideStore(sqlStore) + folderPermissions, err := testutil.ProvideFolderPermissions(features, cfg, sqlStore) + require.NoError(t, err) + folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, - folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + folderStore, sqlStore, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) user := &user.SignedInUser{ OrgID: 1, diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 02a1934103a..90653059b10 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -81,8 +81,8 @@ func ProvideDashboardServiceImpl( metrics: newDashboardsMetrics(r), } - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, folderSvc)) - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, dashSvc, folderSvc)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, fStore)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, dashSvc, fStore)) if err := folderSvc.RegisterService(dashSvc); err != nil { return nil, err diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index c87c7505c73..36f21e37170 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/grafana/authlib/claims" "github.com/grafana/dskit/concurrency" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" @@ -29,11 +30,13 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -46,6 +49,8 @@ type Service struct { dashboardStore dashboards.Store dashboardFolderStore folder.FolderStore features featuremgmt.FeatureToggles + cfg *setting.Cfg + folderPermissions accesscontrol.FolderPermissionsService accessControl accesscontrol.AccessControl // bus is currently used to publish event in case of folder full path change. // For example when a folder is moved to another folder or when a folder is renamed. @@ -65,6 +70,8 @@ func ProvideService( folderStore folder.FolderStore, db db.DB, // DB for the (new) nested folder store features featuremgmt.FeatureToggles, + cfg *setting.Cfg, + folderPermissions accesscontrol.FolderPermissionsService, supportBundles supportbundles.Service, r prometheus.Registerer, tracer tracing.Tracer, @@ -75,6 +82,8 @@ func ProvideService( dashboardFolderStore: folderStore, store: store, features: features, + cfg: cfg, + folderPermissions: folderPermissions, accessControl: ac, bus: bus, db: db, @@ -86,9 +95,9 @@ func ProvideService( supportBundles.RegisterSupportItemCollector(srv.supportBundleCollector()) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(folderStore, srv)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(folderStore, store)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, store)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(store)) return srv } @@ -658,9 +667,43 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( if nestedFolder != nil && nestedFolder.ParentUID != "" { f.ParentUID = nestedFolder.ParentUID } + if err = s.setDefaultFolderPermissions(ctx, cmd.OrgID, user, f); err != nil { + return nil, err + } + return f, nil } +func (s *Service) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, folder *folder.Folder) error { + if !s.cfg.RBAC.PermissionsOnCreation("folder") { + return nil + } + + var permissions []accesscontrol.SetResourcePermissionCommand + + if user.IsIdentityType(claims.TypeUser) { + userID, err := user.GetInternalID() + if err != nil { + return err + } + + permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ + UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(), + }) + } + + isNested := folder.ParentUID != "" + if !isNested || !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { + permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ + {BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()}, + {BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()}, + }...) + } + + _, err := s.folderPermissions.SetPermissions(ctx, orgID, folder.UID, permissions...) + return err +} + func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) { ctx, span := s.tracer.Start(ctx, "folder.Update") defer span.End() diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 2e5260fc594..283dcb668c1 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -62,10 +62,11 @@ func TestIntegrationProvideFolderService(t *testing.T) { } t.Run("should register scope resolvers", func(t *testing.T) { ac := acmock.New() - db, _ := db.InitTestDBWithCfg(t) + db, cfg := db.InitTestDBWithCfg(t) + folderPermissions := acmock.NewMockedPermissionsService() store := ProvideStore(db) ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db, - featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + featuremgmt.WithFeatures(), cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3) }) @@ -97,6 +98,7 @@ func TestIntegrationFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, + cfg: cfg, bus: bus.ProvideBus(tracing.InitializeTracerForTest()), db: db, accessControl: acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), @@ -439,6 +441,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOn, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -494,7 +497,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac) require.NoError(t, err) - elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, serviceWithFlagOn.store, featuresFlagOn, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn) require.NoError(t, err) @@ -554,6 +557,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOff, + cfg: cfg, bus: b, db: db, registry: make(map[string]folder.RegistryService), @@ -576,7 +580,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac) require.NoError(t, err) - elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, serviceWithFlagOff.store, featuresFlagOff, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff) require.NoError(t, err) @@ -632,6 +636,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardFolderStore: folderStore, features: featuresFlagOff, + cfg: cfg, bus: b, db: db, registry: make(map[string]folder.RegistryService), @@ -705,7 +710,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { CanEditValue: true, }) - elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag, ac) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.service.store, tc.featuresFlag, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, tc.service) require.NoError(t, err) @@ -810,6 +815,7 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) { dashboardStore: &dashStore, dashboardFolderStore: dashboardFolderStore, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + cfg: setting.NewCfg(), accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), @@ -847,6 +853,7 @@ func TestFolderServiceDualWrite(t *testing.T) { dashboardStore: dashStore, dashboardFolderStore: dashboardFolderStore, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + cfg: cfg, accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), @@ -1479,6 +1486,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOn, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -1901,6 +1909,7 @@ func TestFolderServiceGetFolder(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -1983,6 +1992,7 @@ func TestFolderServiceGetFolders(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOff, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -2070,6 +2080,7 @@ func TestGetChildrenFilterByPermission(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -2533,6 +2544,7 @@ func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder dashboardFolderStore: dashboardFolderStore, store: nestedFolderStore, features: features, + cfg: setting.NewCfg(), accessControl: ac, db: db, metrics: newFoldersMetrics(nil), diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index ae6aaf279b7..ba8b19cd72f 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/user" @@ -958,13 +959,13 @@ func setupAccessControlGuardianTest( fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Maybe().Return(d, nil) ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) - folderSvc := foldertest.NewFakeService() + fStore := folder.NewFakeStore() folderStore := foldertest.NewFakeFolderStore(t) - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, fakeDashboardService, folderSvc)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(folderSvc)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, folderSvc)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, fakeDashboardService, fStore)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(fStore)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, fStore)) license := licensingtest.NewFakeLicensing() license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() diff --git a/pkg/services/libraryelements/accesscontrol.go b/pkg/services/libraryelements/accesscontrol.go index 68d789cad06..9b52cdd2357 100644 --- a/pkg/services/libraryelements/accesscontrol.go +++ b/pkg/services/libraryelements/accesscontrol.go @@ -35,7 +35,7 @@ var ( // LibraryPanelUIDScopeResolver provides a ScopeAttributeResolver that is able to convert a scope prefixed with "library.panels:uid:" // into uid based scopes for a library panel and its associated folder hierarchy -func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeLibraryPanelsProvider.GetResourceScopeUID("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { if !strings.HasPrefix(scope, prefix) { @@ -60,7 +60,7 @@ func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderSvc folder.Ser return nil, err } - inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, libElDTO.FolderUID, folderSvc) + inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, libElDTO.FolderUID, folderStore) if err != nil { return nil, err } diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index 02fb5f0ad73..3fbbdca72d9 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -15,7 +15,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) *LibraryElementService { +func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, folderStore folder.Store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) *LibraryElementService { l := &LibraryElementService{ Cfg: cfg, SQLStore: sqlStore, @@ -27,7 +27,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout } l.registerAPIEndpoints() - ac.RegisterScopeAttributeResolver(LibraryPanelUIDScopeResolver(l, l.folderService)) + ac.RegisterScopeAttributeResolver(LibraryPanelUIDScopeResolver(l, folderStore)) return l } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 36bdf451541..94e19f86380 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -324,6 +324,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder features := featuremgmt.WithFeatures() cfg := setting.NewCfg() ac := actest.FakeAccessControl{ExpectedEvaluate: true} + folderPermissions := acmock.NewMockedPermissionsService() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore), quotaService) require.NoError(t, err) @@ -331,7 +332,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) store := folderimpl.ProvideStore(sc.sqlStore) s := folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, - features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) t.Logf("Creating folder with title and UID %q", title) ctx := identity.WithRequester(context.Background(), &sc.user) folder, err := s.Create(ctx, &folder.CreateFolderCommand{ @@ -463,7 +464,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo guardian.InitAccessControlGuardian(cfg, ac, dashService) fStore := folderimpl.ProvideStore(sqlStore) folderSrv := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore, - features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) service := LibraryElementService{ Cfg: cfg, features: featuremgmt.WithFeatures(), diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 8e571be20e2..f61304f9837 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -747,6 +748,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder features := featuremgmt.WithFeatures() ac := actest.FakeAccessControl{ExpectedEvaluate: true} + folderPermissions := acmock.NewMockedPermissionsService() cfg := setting.NewCfg() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore), quotaService) @@ -754,7 +756,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) fStore := folderimpl.ProvideStore(sc.sqlStore) s := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, - features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) t.Logf("Creating folder with title and UID %q", title) ctx := identity.WithRequester(context.Background(), sc.user) @@ -838,10 +840,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo require.NoError(t, err) fStore := folderimpl.ProvideStore(sqlStore) + folderPermissions, err := testutil.ProvideFolderPermissions(features, cfg, sqlStore) + require.NoError(t, err) folderService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, - features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac) + elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, fStore, features, ac) service := LibraryPanelService{ Cfg: cfg, SQLStore: sqlStore, diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index ba50444f8c8..f4e0a2e7eb5 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -1817,13 +1818,14 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { }}, nil).Maybe() ac := &recordingAccessControlFake{} + folderPermissions := acmock.NewMockedPermissionsService() dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) fStore := folderimpl.ProvideStore(sqlStore) folderService := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, - featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + featuremgmt.WithFeatures(), cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) store := store.DBstore{ Logger: log, SQLStore: sqlStore, diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index d6d49bca373..432dc0e1b31 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -1546,11 +1546,11 @@ func TestProvisiongWithFullpath(t *testing.T) { folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) _, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg) ac := acmock.New() + folderPermissions := acmock.NewMockedPermissionsService() features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) fStore := folderimpl.ProvideStore(sqlStore) folderService := folderimpl.ProvideService(fStore, ac, inProcBus, dashboardStore, folderStore, sqlStore, - features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) ruleService := createAlertRuleService(t, folderService) var orgID int64 = 1 diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index b9885c7bd53..417680c39c2 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -27,9 +27,10 @@ import ( func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStore dashboards.Store, folderStore *folderimpl.DashboardFolderStoreImpl, bus *bus.InProcBus, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) folder.Service { tb.Helper() + folderPermissions := acmock.NewMockedPermissionsService() fStore := folderimpl.ProvideStore(db) return folderimpl.ProvideService(fStore, ac, bus, dashboardStore, folderStore, db, - features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) } func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) { diff --git a/pkg/services/searchV2/auth.go b/pkg/services/searchV2/auth.go index 5c23e6598ab..5d75fbfd24c 100644 --- a/pkg/services/searchV2/auth.go +++ b/pkg/services/searchV2/auth.go @@ -22,24 +22,24 @@ type FutureAuthService interface { var _ FutureAuthService = (*simpleAuthService)(nil) type simpleAuthService struct { - sql db.DB - ac accesscontrol.Service - folderService folder.Service - logger log.Logger + sql db.DB + ac accesscontrol.Service + folderStore folder.Store + logger log.Logger } func (a *simpleAuthService) GetDashboardReadFilter(ctx context.Context, orgID int64, user *user.SignedInUser) (ResourceFilter, error) { canReadDashboard, canReadFolder := accesscontrol.Checker(user, dashboards.ActionDashboardsRead), accesscontrol.Checker(user, dashboards.ActionFoldersRead) return func(kind entityKind, uid, parent string) bool { if kind == entityKindFolder { - scopes, err := dashboards.GetInheritedScopes(ctx, orgID, uid, a.folderService) + scopes, err := dashboards.GetInheritedScopes(ctx, orgID, uid, a.folderStore) if err != nil { a.logger.Debug("Could not retrieve inherited folder scopes:", "err", err) } scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)) return canReadFolder(scopes...) } else if kind == entityKindDashboard { - scopes, err := dashboards.GetInheritedScopes(ctx, orgID, parent, a.folderService) + scopes, err := dashboards.GetInheritedScopes(ctx, orgID, parent, a.folderStore) if err != nil { a.logger.Debug("Could not retrieve inherited folder scopes:", "err", err) } diff --git a/pkg/services/searchV2/index_test.go b/pkg/services/searchV2/index_test.go index e6a569fb57c..976233f7a07 100644 --- a/pkg/services/searchV2/index_test.go +++ b/pkg/services/searchV2/index_test.go @@ -18,7 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -754,7 +754,7 @@ func setupIntegrationEnv(t *testing.T, folderCount, dashboardsPerFolder int, sql ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, } searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{}, - tracing.InitializeTracerForTest(), features, orgSvc, nil, foldertest.NewFakeService()).(*StandardSearchService) + tracing.InitializeTracerForTest(), features, orgSvc, nil, folder.NewFakeStore()).(*StandardSearchService) require.True(t, ok) err = runSearchService(searchService) diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index d886ec649a0..b0fbcfe72ca 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -85,7 +85,7 @@ func (s *StandardSearchService) IsReady(ctx context.Context, orgId int64) IsSear func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEventsService, ac accesscontrol.Service, tracer tracing.Tracer, features featuremgmt.FeatureToggles, orgService org.Service, - userService user.Service, folderService folder.Service) SearchService { + userService user.Service, folderStore folder.Store) SearchService { extender := &NoopExtender{} logger := log.New("searchV2") s := &StandardSearchService{ @@ -93,10 +93,10 @@ func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEv sql: sql, ac: ac, auth: &simpleAuthService{ - sql: sql, - ac: ac, - folderService: folderService, - logger: logger, + sql: sql, + ac: ac, + folderStore: folderStore, + logger: logger, }, dashboardIndex: newSearchIndex( newSQLDashboardLoader(sql, tracer, cfg.Search), diff --git a/pkg/services/searchV2/service_bench_test.go b/pkg/services/searchV2/service_bench_test.go index 778e7bd2bf8..42a9849fd31 100644 --- a/pkg/services/searchV2/service_bench_test.go +++ b/pkg/services/searchV2/service_bench_test.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/store" @@ -42,7 +42,7 @@ func setupBenchEnv(b *testing.B, folderCount, dashboardsPerFolder int) (*Standar ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, } searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{}, - tracing.InitializeTracerForTest(), features, orgSvc, nil, foldertest.NewFakeService()).(*StandardSearchService) + tracing.InitializeTracerForTest(), features, orgSvc, nil, folder.NewFakeStore()).(*StandardSearchService) require.True(b, ok) err = runSearchService(searchService) diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 57040ee9f1d..338b4fcd05a 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -822,10 +823,11 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil)) require.NoError(t, err) + folderPermissions, err := testutil.ProvideFolderPermissions(features, cfg, db) + require.NoError(t, err) fStore := folderimpl.ProvideStore(db) folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, - folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - + folderimpl.ProvideDashboardFolderStore(db), db, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) // create parent folder parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ UID: "parent", diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go index 594b41dcfb2..7998e1a1adf 100644 --- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go +++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go @@ -78,12 +78,13 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe quotaService := quotatest.New(false, nil) + folderPermissions := mock.NewMockedPermissionsService() dashboardWriteStore, err := database.ProvideDashboardStore(store, cfg, features, tagimpl.ProvideService(store), quotaService) require.NoError(b, err) fStore := folderimpl.ProvideStore(store) folderSvc := folderimpl.ProvideService(fStore, mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), - store, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + store, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true}) From 821bb235b36c98bbfcabfec7cc1bf4e09b27ea94 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Tue, 1 Oct 2024 14:09:42 +0200 Subject: [PATCH 113/174] CloudMigrations: document and re-generate api for syncing (#94063) * CloudMigrations: document frontend open-api generator steps * CloudMigrations: re-run api generation --- pkg/services/cloudmigration/api/dtos.go | 5 +++++ public/api-merged.json | 6 ++++++ public/app/features/migrate-to-cloud/api/README.md | 6 ++++++ public/app/features/migrate-to-cloud/api/endpoints.gen.ts | 5 +++-- public/openapi3.json | 8 ++++++++ 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 public/app/features/migrate-to-cloud/api/README.md diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go index f301dea4de7..d16f616739e 100644 --- a/pkg/services/cloudmigration/api/dtos.go +++ b/pkg/services/cloudmigration/api/dtos.go @@ -313,6 +313,11 @@ type GetSnapshotListParams struct { // Session UID of a session // in: path UID string `json:"uid"` + + // Sort with value latest to return results sorted in descending order. + // in:query + // required:false + Sort string `json:"sort"` } // swagger:response snapshotListResponse diff --git a/public/api-merged.json b/public/api-merged.json index 87416169e3a..65ede3f4d53 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2600,6 +2600,12 @@ "name": "uid", "in": "path", "required": true + }, + { + "type": "string", + "description": "Sort with value latest to return results sorted in descending order.", + "name": "sort", + "in": "query" } ], "responses": { diff --git a/public/app/features/migrate-to-cloud/api/README.md b/public/app/features/migrate-to-cloud/api/README.md new file mode 100644 index 00000000000..a3b146d1df6 --- /dev/null +++ b/public/app/features/migrate-to-cloud/api/README.md @@ -0,0 +1,6 @@ +## Migrate to Cloud API + +The [`endpoints.gen.ts`](./endpoints.gen.ts) file is machine generated. In order to update it, follow these steps: + +- Run: `make swagger-clean && make openapi3-gen` +- Run: `yarn generate-apis` diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index a3cba8abefb..9a16e0b511c 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -112,10 +112,10 @@ export type GetShapshotListApiArg = { page?: number; /** Max limit for results returned. */ limit?: number; - /** Sort with value latest to return results sorted in descending order */ - sort?: string; /** Session UID of a session */ uid: string; + /** Sort with value latest to return results sorted in descending order. */ + sort?: string; }; export type GetCloudMigrationTokenApiResponse = /** status 200 (empty) */ GetAccessTokenResponseDto; export type GetCloudMigrationTokenApiArg = void; @@ -157,6 +157,7 @@ export type CreateSnapshotResponseDto = { }; export type MigrateDataResponseItemDto = { message?: string; + name?: string; refId: string; status: 'OK' | 'WARNING' | 'ERROR' | 'PENDING' | 'UNKNOWN'; type: 'DASHBOARD' | 'DATASOURCE' | 'FOLDER' | 'LIBRARY_ELEMENT'; diff --git a/public/openapi3.json b/public/openapi3.json index 2b1c269d61e..1b7bb227b26 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -15802,6 +15802,14 @@ "schema": { "type": "string" } + }, + { + "description": "Sort with value latest to return results sorted in descending order.", + "in": "query", + "name": "sort", + "schema": { + "type": "string" + } } ], "responses": { From dc31bbb55529cc3e1a29b128bc71a5ebf71d0443 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:24:52 +0200 Subject: [PATCH 114/174] I18n: Download translations from Crowdin (#94065) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 2 +- public/locales/es-ES/grafana.json | 2 +- public/locales/fr-FR/grafana.json | 2 +- public/locales/pt-BR/grafana.json | 2 +- public/locales/zh-Hans/grafana.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 2aafc803723..8bd5d6d6741 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1574,7 +1574,7 @@ }, "apps": { "subtitle": "App-Plug-ins, die das Grafana-Erlebnis erweitern", - "title": "Apps" + "title": "" }, "authentication": { "title": "Authentifizierung" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index a3bf61617b9..14416cf1e63 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1574,7 +1574,7 @@ }, "apps": { "subtitle": "Complementos de aplicaciones que amplían la experiencia de Grafana", - "title": "Aplicaciones" + "title": "" }, "authentication": { "title": "Autenticación" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 803ca8b53b5..1936e186ebc 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1574,7 +1574,7 @@ }, "apps": { "subtitle": "Plugins d'application qui prolongent l'expérience Grafana", - "title": "Applications" + "title": "" }, "authentication": { "title": "Authentification" diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index c77a4328b1b..edc9bbecf65 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1574,7 +1574,7 @@ }, "apps": { "subtitle": "Plug-ins de aplicativos que ampliam a experiência do Grafana", - "title": "Aplicativos" + "title": "" }, "authentication": { "title": "Autenticação" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index a599cb1e951..d546c2cbf8d 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1564,7 +1564,7 @@ }, "apps": { "subtitle": "扩展 Grafana 体验的应用插件", - "title": "应用" + "title": "" }, "authentication": { "title": "身份验证" From 6c91b65aca29aaaf587e89824e4697572d708785 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Tue, 1 Oct 2024 09:46:32 -0300 Subject: [PATCH 115/174] ShareButton: Split copy link button and dropdown (#94020) --- .../sharing/ShareButton/ShareButton.tsx | 19 +++++++++++++------ .../dashboard-scene/utils/interactions.ts | 3 --- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx index f5574ea89f9..a7f6fd46890 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx @@ -1,9 +1,11 @@ +import { css } from '@emotion/css'; import { useCallback, useState } from 'react'; import { useAsyncFn } from 'react-use'; +import { GrafanaTheme2 } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { VizPanel } from '@grafana/scenes'; -import { Button, ButtonGroup, Dropdown } from '@grafana/ui'; +import { Button, ButtonGroup, Dropdown, useStyles2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { DashboardScene } from '../../scene/DashboardScene'; @@ -15,6 +17,7 @@ import { buildShareUrl } from './utils'; const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButton; export default function ShareButton({ dashboard, panel }: { dashboard: DashboardScene; panel?: VizPanel }) { + const styles = useStyles2(getStyles); const [isOpen, setIsOpen] = useState(false); const [_, buildUrl] = useAsyncFn(async () => { @@ -23,17 +26,13 @@ export default function ShareButton({ dashboard, panel }: { dashboard: Dashboard }, [dashboard]); const onMenuClick = useCallback((isOpen: boolean) => { - if (isOpen) { - DashboardInteractions.toolbarShareDropdownClick(); - } - setIsOpen(isOpen); }, []); const MenuActions = () => ; return ( - +
+ {isAngularPanel && ( +
+ + + Panel options + + + + Angular panels options can only be edited using the JSON editor. + + + + + + +
+ )}
@@ -146,6 +192,13 @@ function getStyles(theme: GrafanaTheme2) { rotateIcon: css({ rotate: '180deg', }), + angularDeprecationContainer: css({ + label: 'angular-deprecation-container', + padding: theme.spacing(1), + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', + }), }; } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 741bc4bb5a7..f7f49b5b5f8 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -59,6 +59,17 @@ jest.mock('@grafana/runtime', () => ({ getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }), }; }, + config: { + ...jest.requireActual('@grafana/runtime').config, + angularSupportEnabled: true, + panels: { + 'briangann-datatable-panel': { + id: 'briangann-datatable-panel', + state: 'deprecated', + angular: { detected: true, hideDeprecation: false }, + }, + }, + }, })); jest.mock('app/features/playlist/PlaylistSrv', () => ({ @@ -826,6 +837,75 @@ describe('DashboardScene', () => { expect(restoredGrid.state.grid.state.children.length).toBe(1); }); }); + + describe('When a dashboard contain angular panels', () => { + it('should return true if the dashboard contains angular panels', () => { + // create a scene with angular panels inside + const scene = buildTestScene({ + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'briangann-datatable-panel', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new DashboardGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + ], + }), + }), + }); + + scene.activate(); + + expect(scene.hasDashboardAngularPlugins()).toBe(true); + }); + it('should return true if the dashboard contains explicitControllerMigration panels', () => { + // create a scene with angular panels inside + const scene = buildTestScene({ + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'graph', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new DashboardGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + ], + }), + }), + }); + + scene.activate(); + + expect(scene.hasDashboardAngularPlugins()).toBe(true); + }); + }); }); function buildTestScene(overrides?: Partial) { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index cad6daa473a..0d037f71285 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -67,6 +67,7 @@ import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { RowRepeaterBehavior } from './RowRepeaterBehavior'; import { ViewPanelScene } from './ViewPanelScene'; +import { isUsingAngularDatasourcePlugin, isUsingAngularPanelPlugin } from './angular/AngularDeprecation'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { DashboardLayoutManager } from './types'; @@ -662,6 +663,27 @@ export class DashboardScene extends SceneObjectBase { locationService.replace('/'); } + public getDashboardPanels() { + return dashboardSceneGraph.getVizPanels(this); + } + + public hasDashboardAngularPlugins() { + const sceneGridLayout = this.state.body; + if (!(sceneGridLayout instanceof DefaultGridLayoutManager)) { + return false; + } + const gridItems = sceneGridLayout.state.grid.state.children; + const dashboardWasAngular = gridItems.some((gridItem) => { + if (!(gridItem instanceof DashboardGridItem)) { + return false; + } + const isAngularPanel = isUsingAngularPanelPlugin(gridItem.state.body); + const isAngularDs = isUsingAngularDatasourcePlugin(gridItem.state.body); + return isAngularPanel || isAngularDs; + }); + return dashboardWasAngular; + } + public onSetScrollRef = (scrollElement: ScrollRefElement): void => { this._scrollRef = scrollElement; }; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx index 881883dd441..d4ec59ed2cf 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx @@ -1,13 +1,36 @@ import { screen } from '@testing-library/react'; import { render } from 'test/test-utils'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { selectors } from '@grafana/e2e-selectors'; +import { config, setPluginImportUtils } from '@grafana/runtime'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (id: string) => undefined, +}); + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), useChromeHeaderHeight: jest.fn(), + getDataSourceSrv: () => { + return { + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }), + }; + }, + config: { + ...jest.requireActual('@grafana/runtime').config, + angularSupportEnabled: true, + panels: { + 'briangann-datatable-panel': { + id: 'briangann-datatable-panel', + state: 'deprecated', + angular: { detected: true, hideDeprecation: false }, + }, + }, + }, })); describe('DashboardSceneRenderer', () => { @@ -50,4 +73,54 @@ describe('DashboardSceneRenderer', () => { expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument(); }); + + it('should render angular deprecation notice when dashboard contains angular components', async () => { + const noticeText = /This dashboard depends on Angular/i; + //enable feature flag angularDeprecationUI + config.featureToggles.angularDeprecationUI = true; + const scene = transformSaveModelToScene({ + meta: {}, + dashboard: { + title: 'Angular dashboard', + uid: 'uid', + schemaVersion: 0, + // Disabling build in annotations to avoid mocking Grafana data source + annotations: { + list: [ + { + builtIn: 1, + datasource: { + type: 'grafana', + uid: '-- Grafana --', + }, + enable: false, + hide: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + type: 'dashboard', + }, + ], + }, + + panels: [ + { + id: 1, + type: 'briangann-datatable-panel', + gridPos: { x: 0, y: 0, w: 12, h: 6 }, + title: 'Angular component', + options: { + showHeader: true, + }, + fieldConfig: { defaults: {}, overrides: [] }, + datasource: { uid: 'abcdef' }, + targets: [{ refId: 'A' }], + }, + ], + }, + }); + + render(); + + expect(await screen.findByText(noticeText)).toBeInTheDocument(); + }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index ae71d1e7d16..32780129248 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -16,6 +16,7 @@ import { useSelector } from 'app/types'; import { DashboardScene } from './DashboardScene'; import { NavToolbarActions } from './NavToolbarActions'; import { PanelSearchLayout } from './PanelSearchLayout'; +import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } = @@ -65,7 +66,9 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps; - let body: React.ReactNode = [withPanels]; + const angularBanner = ; + + let body: React.ReactNode = [angularBanner, withPanels]; if (notFound) { body = [notFound]; diff --git a/public/app/features/dashboard-scene/scene/angular/AngularDeprecation.tsx b/public/app/features/dashboard-scene/scene/angular/AngularDeprecation.tsx new file mode 100644 index 00000000000..4f39f8f4530 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/angular/AngularDeprecation.tsx @@ -0,0 +1,103 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { SceneComponentProps, SceneObjectBase, VizPanel } from '@grafana/scenes'; +import { Icon, PanelChrome, Tooltip, useStyles2 } from '@grafana/ui'; +import { explicitlyControlledMigrationPanels } from 'app/features/dashboard/state/PanelModel'; +import { isAngularDatasourcePluginAndNotHidden } from 'app/features/plugins/angularDeprecation/utils'; + +import { getQueryRunnerFor } from '../../utils/utils'; + +export class AngularDeprecation extends SceneObjectBase { + static Component = AngularDeprecationRenderer; + + constructor() { + super({}); + this.addActivationHandler(this.onActivate); + } + + private onActivate = () => { + const panel = this.parent; + if (!panel || !(panel instanceof VizPanel)) { + throw new Error('PanelNotices can be used only as title items for VizPanel'); + } + }; + + public getPanel() { + const panel = this.parent; + + if (panel && panel instanceof VizPanel) { + return panel; + } + + return null; + } +} + +function AngularDeprecationRenderer({ model }: SceneComponentProps) { + const panel = model.getPanel(); + + const styles = useStyles2(getStyles); + if (!panel) { + return null; + } + + const showAngularNotice = shouldShowAngularNotice(panel); + if (showAngularNotice) { + const pluginTypeNotice = getPluginTypeNotice( + isUsingAngularDatasourcePlugin(panel), + isUsingAngularPanelPlugin(panel) + ); + const message = `This ${pluginTypeNotice} requires Angular (deprecated).`; + const angularNoticeTooltip = ( + + + + + + ); + return angularNoticeTooltip; + } + return null; +} + +export function getPluginTypeNotice(isAngularDatasource: boolean, isAngularPanel: boolean) { + if (isAngularPanel) { + return 'panel'; + } + if (isAngularDatasource) { + return 'data source'; + } + return 'panel or data source'; +} + +export function isUsingAngularPanelPlugin(panel: VizPanel) { + return ( + (config.panels[panel.state.pluginId]?.angular?.detected || + explicitlyControlledMigrationPanels.includes(panel.state.pluginId)) && + !config.panels[panel.state.pluginId]?.angular?.hideDeprecation + ); +} + +export function isUsingAngularDatasourcePlugin(panel: VizPanel) { + const queryRunner = getQueryRunnerFor(panel); + const datasource = queryRunner?.state.datasource; + + return datasource?.uid ? isAngularDatasourcePluginAndNotHidden(datasource?.uid) : false; +} + +export function shouldShowAngularNotice(panel: VizPanel) { + return ( + (config.featureToggles.angularDeprecationUI ?? false) && + (isUsingAngularDatasourcePlugin(panel) || isUsingAngularPanelPlugin(panel)) + ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + angularNotice: css({ + color: theme.colors.warning.text, + }), + }; +} diff --git a/public/app/features/dashboard-scene/scene/angular/DashboardAngularDeprecationBanner.tsx b/public/app/features/dashboard-scene/scene/angular/DashboardAngularDeprecationBanner.tsx new file mode 100644 index 00000000000..b5c7fda4007 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/angular/DashboardAngularDeprecationBanner.tsx @@ -0,0 +1,31 @@ +import { config } from '@grafana/runtime'; +import { VizPanel } from '@grafana/scenes'; +import { explicitlyControlledMigrationPanels } from 'app/features/dashboard/state/PanelModel'; +import { AngularDeprecationNotice } from 'app/features/plugins/angularDeprecation/AngularDeprecationNotice'; + +import { DashboardScene } from '../DashboardScene'; + +interface Props { + dashboard: DashboardScene; +} + +export const DashboardAngularDeprecationBanner = ({ dashboard }: Props) => { + const panels = dashboard.getDashboardPanels(); + const shouldShowAutoMigrateLink = panels.some((panel) => { + if (panel instanceof VizPanel) { + return explicitlyControlledMigrationPanels.includes(panel.state.pluginId); + } + return false; + }); + + const isContainingAngularPanels = + config.featureToggles.angularDeprecationUI && dashboard.hasDashboardAngularPlugins(); + + return isContainingAngularPanels && dashboard.state.uid ? ( + + ) : null; +}; diff --git a/public/app/features/dashboard-scene/serialization/angularMigration.ts b/public/app/features/dashboard-scene/serialization/angularMigration.ts index 82115f1365f..d92f13cb1ed 100644 --- a/public/app/features/dashboard-scene/serialization/angularMigration.ts +++ b/public/app/features/dashboard-scene/serialization/angularMigration.ts @@ -18,7 +18,6 @@ export function getAngularPanelMigrationHandler(oldModel: PanelModel) { const wasAngular = autoMigrateAngular[oldModel.autoMigrateFrom] != null; const oldOptions = oldModel.getOptionsToRemember(); const prevPluginId = oldModel.autoMigrateFrom; - if (plugin.onPanelTypeChanged) { const prevOptions = wasAngular ? { angular: oldOptions } : oldOptions.options; Object.assign(panel.options, plugin.onPanelTypeChanged(panel, prevPluginId, prevOptions, panel.fieldConfig)); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index b5237a83683..e23afc410d6 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -37,6 +37,7 @@ import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavio import { PanelNotices } from '../scene/PanelNotices'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { AngularDeprecation } from '../scene/angular/AngularDeprecation'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowActions } from '../scene/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; @@ -278,6 +279,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem { const titleItems: SceneObject[] = []; + if (config.featureToggles.angularDeprecationUI) { + titleItems.push(new AngularDeprecation()); + } titleItems.push( new VizPanelLinks({ rawLinks: panel.links, diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 61ee5d75291..ff4eb38cc49 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -13,12 +13,10 @@ function getRefreshPicker(scene: DashboardScene) { } function getPanelLinks(panel: VizPanel) { - if ( - panel.state.titleItems && - Array.isArray(panel.state.titleItems) && - panel.state.titleItems[0] instanceof VizPanelLinks - ) { - return panel.state.titleItems[0]; + if (panel.state.titleItems && Array.isArray(panel.state.titleItems)) { + // search panel.state.titleItems for VizPanelLinks + const panelLink = panel.state.titleItems.find((item) => item instanceof VizPanelLinks); + return panelLink ?? null; } return null; diff --git a/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.tsx b/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.tsx index 8e42e1dcef1..61d8e1d9abd 100644 --- a/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.tsx +++ b/public/app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice.tsx @@ -7,14 +7,12 @@ import { Alert } from '@grafana/ui'; type Props = { className?: string; - pluginId?: string; pluginType?: PluginType; - angularSupportEnabled?: boolean; showPluginDetailsLink?: boolean; - interactionElementId?: string; + children?: React.ReactNode; }; function deprecationMessage(pluginType?: string, angularSupportEnabled?: boolean): string { @@ -45,7 +43,15 @@ function deprecationMessage(pluginType?: string, angularSupportEnabled?: boolean // An Alert showing information about Angular deprecation notice. // If the plugin does not use Angular (!plugin.angularDetected), it returns null. export function AngularDeprecationPluginNotice(props: Props): React.ReactElement | null { - const { className, angularSupportEnabled, pluginId, pluginType, showPluginDetailsLink, interactionElementId } = props; + const { + className, + angularSupportEnabled, + pluginId, + pluginType, + showPluginDetailsLink, + interactionElementId, + children, + } = props; const [dismissed, setDismissed] = useState(false); const interactionAttributes: Record = {}; @@ -88,6 +94,12 @@ export function AngularDeprecationPluginNotice(props: Props): React.ReactElement ) : null}
+ {children && ( + <> +
+ {children} + + )} ); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 0c9b09b6853..3d5fff7e31a 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -751,6 +751,11 @@ } }, "dashboards": { + "panel-edit": { + "angular-deprecation-button-open-panel-json": "Open JSON editor", + "angular-deprecation-description": "Angular panels options can only be edited using the JSON editor.", + "angular-deprecation-heading": "Panel options" + }, "settings": { "variables": { "dependencies": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 8e39227ae2f..60eb86a1b52 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -751,6 +751,11 @@ } }, "dashboards": { + "panel-edit": { + "angular-deprecation-button-open-panel-json": "Øpęʼn ĴŜØŃ ęđįŧőř", + "angular-deprecation-description": "Åʼnģūľäř päʼnęľş őpŧįőʼnş čäʼn őʼnľy þę ęđįŧęđ ūşįʼnģ ŧĥę ĴŜØŃ ęđįŧőř.", + "angular-deprecation-heading": "Päʼnęľ őpŧįőʼnş" + }, "settings": { "variables": { "dependencies": { From 6a30240f58a2b8c48f694ea6beb1d2b3af4650c9 Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:25:04 +0200 Subject: [PATCH 117/174] RestoreDashboards: Fix 'Dashboards' typo in folder picker (#94046) fix: typo --- pkg/services/folder/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 309c4cc15d4..b0869713f30 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -54,7 +54,7 @@ type Folder struct { } var GeneralFolder = Folder{ID: 0, Title: "General"} -var RootFolder = &Folder{ID: 0, Title: "Dashbboards", UID: GeneralFolderUID, ParentUID: ""} +var RootFolder = &Folder{ID: 0, Title: "Dashboards", UID: GeneralFolderUID, ParentUID: ""} var SharedWithMeFolder = Folder{ Title: "Shared with me", Description: "Dashboards and folders shared with me", From bc3e1df5e306e52cecc057884c400ff437376388 Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:27:25 +0200 Subject: [PATCH 118/174] RestoreDashboards: Improve tracking (#93934) feat: add tracking in DeleteModal --- .../components/BrowseActions/DeleteModal.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx index 7cff90fd755..370355eb658 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { config } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Alert, ConfirmModal, Text, Space } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; @@ -21,6 +21,14 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P const deleteIsInvalid = Boolean(data && (data.alertRule || data.libraryPanel)); const [isDeleting, setIsDeleting] = useState(false); const onDelete = async () => { + reportInteraction('grafana_manage_dashboards_delete_clicked', { + item_counts: { + dashboard: Object.keys(selectedItems.dashboard).length, + folder: Object.keys(selectedItems.folder).length, + }, + source: 'browse_dashboards', + restore_enabled: config.featureToggles.dashboardRestoreUI, + }); setIsDeleting(true); try { await onConfirm(); From 586c95654dd1567be008d083938776a7739163d5 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 1 Oct 2024 16:29:11 +0300 Subject: [PATCH 119/174] Routing: Update more components using props.match to use hooks (#93918) * RuleViewed: Get params from hook * ProviderConfigPage: Use hooks for redux logic * Update NewDashboardWithDS * Update StorageFolderPage * Update StoragePage * Cleanup * Update PublicDashboardPage * Update RuleEditor * Update BrowseFolderAlertingPage * Update BrowseFolderLibraryPanelsPage * Update SoloPanelPage * Fix test * Add useParams mocks * Update ServiceAccountPage * Simplify mocks * Update SignupInvited * Update Playlist pages * Update AdminEditOrgPage * Update UserAdminPage * Update Silences * Update BrowseDashboardsPage * Update GrafanaModifyExport * Update AppRootPage * Remove useParams mock * Update PublicDashboardsPages * Cleanup * Update PublicDashboardPage.test * Cleanup * Update PublicDashboardScenePage.test.tsx * Update imports * Revert AppRootPage changes * Add back AppRootPage changes --- .../app/features/admin/AdminEditOrgPage.tsx | 9 +- public/app/features/admin/UserAdminPage.tsx | 228 +++++++++--------- .../alerting/unified/Silences.test.tsx | 10 + .../features/alerting/unified/Silences.tsx | 10 +- .../export/GrafanaModifyExport.test.tsx | 13 +- .../components/export/GrafanaModifyExport.tsx | 11 +- .../components/silences/SilencesEditor.tsx | 6 +- .../BrowseDashboardsPage.test.tsx | 63 +++-- .../BrowseDashboardsPage.tsx | 15 +- .../pages/PublicDashboardScenePage.test.tsx | 38 +-- .../pages/PublicDashboardScenePage.tsx | 16 +- .../containers/PublicDashboardPage.test.tsx | 49 ++-- .../containers/PublicDashboardPage.tsx | 5 +- .../PublicDashboardPageProxy.test.tsx | 45 ++-- .../containers/PublicDashboardPageProxy.tsx | 9 +- .../features/invites/SignupInvited.test.tsx | 22 +- public/app/features/invites/SignupInvited.tsx | 8 +- .../playlist/PlaylistEditPage.test.tsx | 10 +- .../features/playlist/PlaylistEditPage.tsx | 9 +- .../features/playlist/PlaylistStartPage.tsx | 5 +- .../plugins/components/AppRootPage.tsx | 5 +- public/app/features/plugins/routes.tsx | 2 +- public/app/routes/routes.tsx | 5 +- 23 files changed, 275 insertions(+), 318 deletions(-) diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index f83374d68b6..c557c3a8507 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -1,12 +1,12 @@ import { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom-v5-compat'; import { useAsyncFn } from 'react-use'; import { NavModelItem } from '@grafana/data'; import { Field, Input, Button, Legend, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { OrgUser, AccessControlAction, OrgRole } from 'app/types'; import { OrgUsersTable } from './Users/OrgUsersTable'; @@ -16,10 +16,9 @@ interface OrgNameDTO { orgName: string; } -interface Props extends GrafanaRouteComponentProps<{ id: string }> {} - -const AdminEditOrgPage = ({ match }: Props) => { - const orgId = parseInt(match.params.id, 10); +const AdminEditOrgPage = () => { + const { id = '' } = useParams(); + const orgId = parseInt(id, 10); const canWriteOrg = contextSrv.hasPermission(AccessControlAction.OrgsWrite); const canReadUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index dc40a882c73..dbfa918b49e 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -1,12 +1,12 @@ -import { PureComponent } from 'react'; +import { useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; import { featureEnabled } from '@grafana/runtime'; import { Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { StoreState, UserDTO, UserOrg, UserSession, SyncInfo, UserAdminError, AccessControlAction } from 'app/types'; import { UserLdapSyncInfo } from './UserLdapSyncInfo'; @@ -30,7 +30,7 @@ import { syncLdapUser, } from './state/actions'; -interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { +interface OwnProps { user?: UserDTO; orgs: UserOrg[]; sessions: UserSession[]; @@ -39,136 +39,142 @@ interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { error?: UserAdminError; } -export class UserAdminPage extends PureComponent { - async componentDidMount() { - const { match, loadAdminUserPage } = this.props; - loadAdminUserPage(parseInt(match.params.id, 10)); - } +export const UserAdminPage = ({ + loadAdminUserPage, + user, + orgs, + sessions, + ldapSyncInfo, + isLoading, + updateUser, + setUserPassword, + deleteUser, + disableUser, + enableUser, + updateUserPermissions, + deleteOrgUser, + updateOrgUserRole, + addOrgUser, + revokeSession, + revokeAllSessions, + syncLdapUser, +}: Props) => { + const { id = '' } = useParams(); + useEffect(() => { + const userId = parseInt(id, 10); + loadAdminUserPage(userId); + }, [id, loadAdminUserPage]); - onUserUpdate = (user: UserDTO) => { - this.props.updateUser(user); + const onPasswordChange = (password: string) => { + if (user) { + setUserPassword(user.id, password); + } }; - onPasswordChange = (password: string) => { - const { user, setUserPassword } = this.props; - user && setUserPassword(user.id, password); + const onGrafanaAdminChange = (isGrafanaAdmin: boolean) => { + if (user) { + updateUserPermissions(user.id, isGrafanaAdmin); + } }; - onUserDelete = (userId: number) => { - this.props.deleteUser(userId); + const onOrgRemove = (orgId: number) => { + if (user) { + deleteOrgUser(user.id, orgId); + } }; - onUserDisable = (userId: number) => { - this.props.disableUser(userId); + const onOrgRoleChange = (orgId: number, newRole: string) => { + if (user) { + updateOrgUserRole(user.id, orgId, newRole); + } }; - onUserEnable = (userId: number) => { - this.props.enableUser(userId); + const onOrgAdd = (orgId: number, role: string) => { + if (user) { + addOrgUser(user, orgId, role); + } }; - onGrafanaAdminChange = (isGrafanaAdmin: boolean) => { - const { user, updateUserPermissions } = this.props; - user && updateUserPermissions(user.id, isGrafanaAdmin); + const onSessionRevoke = (tokenId: number) => { + if (user) { + revokeSession(tokenId, user.id); + } }; - onOrgRemove = (orgId: number) => { - const { user, deleteOrgUser } = this.props; - user && deleteOrgUser(user.id, orgId); + const onAllSessionsRevoke = () => { + if (user) { + revokeAllSessions(user.id); + } }; - onOrgRoleChange = (orgId: number, newRole: string) => { - const { user, updateOrgUserRole } = this.props; - user && updateOrgUserRole(user.id, orgId, newRole); + const onUserSync = () => { + if (user) { + syncLdapUser(user.id); + } }; - onOrgAdd = (orgId: number, role: string) => { - const { user, addOrgUser } = this.props; - user && addOrgUser(user, orgId, role); + const isLDAPUser = user?.isExternal && user?.authLabels?.includes('LDAP'); + const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList); + const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead); + const authSource = user?.authLabels?.[0]; + const lockMessage = authSource ? `Synced via ${authSource}` : ''; + const pageNav: NavModelItem = { + text: user?.login ?? '', + icon: 'shield', + subTitle: 'Manage settings for an individual user.', }; - onSessionRevoke = (tokenId: number) => { - const { user, revokeSession } = this.props; - user && revokeSession(tokenId, user.id); - }; - - onAllSessionsRevoke = () => { - const { user, revokeAllSessions } = this.props; - user && revokeAllSessions(user.id); - }; - - onUserSync = () => { - const { user, syncLdapUser } = this.props; - user && syncLdapUser(user.id); - }; - - render() { - const { user, orgs, sessions, ldapSyncInfo, isLoading } = this.props; - const isLDAPUser = user?.isExternal && user?.authLabels?.includes('LDAP'); - const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList); - const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead); - const authSource = user?.authLabels?.[0]; - const lockMessage = authSource ? `Synced via ${authSource}` : ''; - - const pageNav: NavModelItem = { - text: user?.login ?? '', - icon: 'shield', - subTitle: 'Manage settings for an individual user.', - }; - - return ( - - - - {user && ( - <> - - {isLDAPUser && - user?.isExternallySynced && - featureEnabled('ldapsync') && - ldapSyncInfo && - canReadLDAPStatus && ( - - )} - - - )} - - {orgs && ( - + + + {user && ( + <> + - )} - - {sessions && canReadSessions && ( - + )} + - )} - - - - ); - } -} + + )} + {orgs && ( + + )} + {sessions && canReadSessions && ( + + )} + + + + ); +}; const mapStateToProps = (state: StoreState) => ({ user: state.userAdmin.user, diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 68054e1744e..009f39a8b7c 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -1,3 +1,4 @@ +import { useParams } from 'react-router-dom-v5-compat'; import { render, screen, userEvent, waitFor, within } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; @@ -31,6 +32,11 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; jest.mock('app/core/services/context_srv'); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn(), +})); + const TEST_TIMEOUT = 60000; const renderSilences = (location = '/alerting/silences/') => { @@ -314,17 +320,20 @@ describe('Silence create/edit', () => { }); it('shows an error when existing silence cannot be found', async () => { + (useParams as jest.Mock).mockReturnValue({ id: 'foo-bar' }); renderSilences('/alerting/silence/foo-bar/edit'); expect(await ui.existingSilenceNotFound.find()).toBeInTheDocument(); }); it('shows an error when user cannot edit/recreate silence', async () => { + (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_LACKING_PERMISSIONS }); renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_LACKING_PERMISSIONS}/edit`); expect(await ui.noPermissionToEdit.find()).toBeInTheDocument(); }); it('populates form with existing silence information', async () => { + (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_EXISTING }); renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING}/edit`); // Await the first value to be populated, after which we can expect that all of the other @@ -335,6 +344,7 @@ describe('Silence create/edit', () => { }); it('populates form with existing silence information that has __alert_rule_uid__', async () => { + (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID }); mockAlertRuleApi(server).getAlertRule(MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID, grafanaRulerRule); renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}/edit`); expect(await screen.findByLabelText(/alert rule/i)).toHaveValue(grafanaRulerRule.grafana_alert.title); diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index c9a2cd10af7..5bfbb676278 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -1,4 +1,4 @@ -import { Route, RouteChildrenProps, Switch } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { withErrorBoundary } from '@grafana/ui'; import { @@ -51,13 +51,7 @@ const Silences = () => { }} - {({ match }: RouteChildrenProps<{ id: string }>) => { - return ( - match?.params.id && ( - - ) - ); - }} + diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx index a24860ace5d..f64211b329f 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Route } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom-v5-compat'; import { Props } from 'react-virtualized-auto-sizer'; import { render, waitFor, waitForElementToBeRemoved, userEvent } from 'test/test-utils'; import { byRole, byTestId, byText } from 'testing-library-selector'; @@ -55,9 +55,14 @@ const dataSources = { }; function renderModifyExport(ruleId: string) { - render(, { - historyOptions: { initialEntries: [`/alerting/${ruleId}/modify-export`] }, - }); + render( + + } /> + , + { + historyOptions: { initialEntries: [`/alerting/${ruleId}/modify-export`] }, + } + ); } const server = setupMswServer(); diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx index 541bf1eb0e5..108532e5e41 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { useMemo } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { locationService } from '@grafana/runtime'; import { Alert, LoadingPlaceholder } from '@grafana/ui'; -import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types'; import { RuleIdentifier } from '../../../../../types/unified-alerting'; import { useRuleWithLocation } from '../../hooks/useCombinedRule'; import { stringifyErrorLike } from '../../utils/misc'; @@ -15,12 +15,11 @@ import { createRelativeUrl } from '../../utils/url'; import { AlertingPageWrapper } from '../AlertingPageWrapper'; import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm'; -interface GrafanaModifyExportProps extends GrafanaRouteComponentProps<{ id?: string }> {} - -export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) { +export default function GrafanaModifyExport() { + const { id } = useParams(); const ruleIdentifier = useMemo(() => { - return ruleId.tryParse(match.params.id, true); - }, [match.params.id]); + return ruleId.tryParse(id, true); + }, [id]); if (!ruleIdentifier) { return ( diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index 56e1f533e76..e9ce5661d3c 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { pickBy } from 'lodash'; import { useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom-v5-compat'; import { useDebounce } from 'react-use'; import { @@ -41,7 +42,6 @@ import { SilencedInstancesPreview } from './SilencedInstancesPreview'; import { getDefaultSilenceFormValues, getFormFieldsForSilence } from './utils'; interface Props { - silenceId: string; alertManagerSourceName: string; } @@ -50,7 +50,8 @@ interface Props { * * Fetches silence details from API, based on `silenceId` */ -const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Props) => { +const ExistingSilenceEditor = ({ alertManagerSourceName }: Props) => { + const { id: silenceId = '' } = useParams(); const { data: silence, isLoading: getSilenceIsLoading, @@ -61,7 +62,6 @@ const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Props) => ruleMetadata: true, accessControl: true, }); - const ruleUid = silence?.matchers?.find((m) => m.name === MATCHER_ALERT_RULE_UID)?.value; const isGrafanaAlertManager = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME; diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index 7269e217afa..467786f7181 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -4,15 +4,15 @@ import { HttpResponse, http } from 'msw'; import { setupServer, SetupServer } from 'msw/node'; import { ComponentProps } from 'react'; import * as React from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import AutoSizer from 'react-virtualized-auto-sizer'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectors } from '@grafana/e2e-selectors'; import { contextSrv } from 'app/core/core'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { backendSrv } from 'app/core/services/backend_srv'; -import BrowseDashboardsPage, { Props } from './BrowseDashboardsPage'; +import BrowseDashboardsPage from './BrowseDashboardsPage'; import { wellFormedTree } from './fixtures/dashboardsTreeItem.fixture'; import * as permissions from './permissions'; const [mockTree, { dashbdD, folderA, folderA_folderA }] = wellFormedTree(); @@ -44,6 +44,11 @@ jest.mock('react-virtualized-auto-sizer', () => { }; }); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn().mockReturnValue({}), +})); + function render(...[ui, options]: Parameters) { const { rerender } = rtlRender( { }); describe('browse-dashboards BrowseDashboardsPage', () => { - let props: Props; let server: SetupServer; const mockPermissions = { canCreateDashboards: true, @@ -143,10 +147,6 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); beforeEach(() => { - props = { - ...getRouteComponentProps(), - }; - jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); }); @@ -158,17 +158,17 @@ describe('browse-dashboards BrowseDashboardsPage', () => { describe('at the root level', () => { it('displays "Dashboards" as the page title', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); }); it('displays a search input', async () => { - render(); + render(); expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); }); it('shows the "New" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument(); }); @@ -180,25 +180,25 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canCreateFolders: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); }); it('does not show "Folder actions"', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('does not show an "Edit title" button', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument(); }); it('does not show any tabs', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('tab', { name: 'Dashboards' })).not.toBeInTheDocument(); @@ -207,7 +207,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('displays the filters and hides the actions initially', async () => { - render(); + render(); await screen.findByPlaceholderText('Search for dashboards and folders'); expect(await screen.findByText('Sort')).toBeInTheDocument(); @@ -218,7 +218,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('selecting an item hides the filters and shows the actions instead', async () => { - render(); + render(); const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid)); await userEvent.click(checkbox); @@ -233,7 +233,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('navigating into a child item resets the selected state', async () => { - const { rerender } = render(); + const { rerender } = render(); const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(folderA.item.uid)); await userEvent.click(checkbox); @@ -242,9 +242,8 @@ describe('browse-dashboards BrowseDashboardsPage', () => { expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); - const updatedProps = { ...props }; - updatedProps.match.params = { uid: folderA.item.uid }; - rerender(); + (useParams as jest.Mock).mockReturnValue({ uid: folderA.item.uid }); + rerender(); // Check the filters are now visible again expect(await screen.findByText('Filter by tag')).toBeInTheDocument(); @@ -258,21 +257,21 @@ describe('browse-dashboards BrowseDashboardsPage', () => { describe('for a child folder', () => { beforeEach(() => { - props.match.params = { uid: folderA.item.uid }; + (useParams as jest.Mock).mockReturnValue({ uid: folderA.item.uid }); }); it('shows the folder name as the page title', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); }); it('displays a search input', async () => { - render(); + render(); expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); }); it('shows the "New" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument(); }); @@ -284,13 +283,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canCreateFolders: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); }); it('shows the "Folder actions" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); @@ -304,13 +303,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canViewPermissions: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('shows an "Edit title" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'Edit title' })).toBeInTheDocument(); }); @@ -321,13 +320,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canEditFolders: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument(); }); it('displays all the folder tabs and shows the "Dashboards" tab as selected', async () => { - render(); + render(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toBeInTheDocument(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toHaveAttribute('aria-selected', 'true'); @@ -339,7 +338,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('displays the filters and hides the actions initially', async () => { - render(); + render(); await screen.findByPlaceholderText('Search for dashboards and folders'); expect(await screen.findByText('Sort')).toBeInTheDocument(); @@ -350,7 +349,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('selecting an item hides the filters and shows the actions instead', async () => { - render(); + render(); const checkbox = await screen.findByTestId( selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid) diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 87b8239db2f..cce7bfdcdc3 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -1,13 +1,12 @@ import { css } from '@emotion/css'; import { memo, useEffect, useMemo } from 'react'; -import { useLocation } from 'react-router-dom-v5-compat'; +import { useLocation, useParams } from 'react-router-dom-v5-compat'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { FilterInput, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useDispatch } from 'app/types'; import { buildNavModel, getDashboardsTabID } from '../folders/state/navModel'; @@ -24,17 +23,9 @@ import { SearchView } from './components/SearchView'; import { getFolderPermissions } from './permissions'; import { setAllSelection, useHasSelection } from './state'; -export interface BrowseDashboardsPageRouteParams { - uid?: string; - slug?: string; -} - -export interface Props extends GrafanaRouteComponentProps {} - // New Browse/Manage/Search Dashboards views for nested folders - -const BrowseDashboardsPage = memo(({ match }: Props) => { - const { uid: folderUID } = match.params; +const BrowseDashboardsPage = memo(() => { + const { uid: folderUID } = useParams(); const dispatch = useDispatch(); const styles = useStyles2(getStyles); diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx index bd7e7b4fc64..44688f0aa2e 100644 --- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx @@ -1,19 +1,12 @@ -import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { Route, Routes } from 'react-router-dom-v5-compat'; import { of } from 'rxjs'; -import { TestProvider } from 'test/helpers/TestProvider'; -import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { render } from 'test/test-utils'; import { getDefaultTimeRange, LoadingState, PanelData, PanelProps } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { - config, - getPluginLinkExtensions, - locationService, - LocationServiceProvider, - setPluginImportUtils, - setRunRequest, -} from '@grafana/runtime'; +import { config, getPluginLinkExtensions, setPluginImportUtils, setRunRequest } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { DashboardRoutes } from 'app/types/dashboard'; @@ -37,27 +30,22 @@ jest.mock('@grafana/runtime', () => ({ const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions); -function setup(props?: Partial) { - const context = getGrafanaContextMock(); - +function setup(token = 'an-access-token') { const pubdashProps: PublicDashboardSceneProps = { ...getRouteComponentProps({ - match: { params: { accessToken: 'an-access-token' }, isExact: true, url: '', path: '' }, route: { routeName: DashboardRoutes.Public, path: '/public-dashboards/:accessToken', component: () => null, }, }), - ...props, }; return render( - - - - - + + } /> + , + { historyOptions: { initialEntries: [`/public-dashboards/${token}`] } } ); } @@ -190,9 +178,7 @@ describe('PublicDashboardScenePage', () => { dashboard: { ...simpleDashboard, timepicker: { hidden: true } }, meta: {}, }); - setup({ - match: { params: { accessToken }, isExact: true, url: '', path: '' }, - }); + setup(accessToken); await waitForDashboardGridToRender(); @@ -210,7 +196,7 @@ describe('given unavailable public dashboard', () => { dashboard: simpleDashboard, meta: { publicDashboardEnabled: false, dashboardNotFound: false }, }); - setup({ match: { params: { accessToken }, isExact: true, url: '', path: '' } }); + setup(accessToken); await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage)); @@ -226,7 +212,7 @@ describe('given unavailable public dashboard', () => { dashboard: simpleDashboard, meta: { dashboardNotFound: true }, }); - setup({ match: { params: { accessToken }, isExact: true, url: '', path: '' } }); + setup(accessToken); await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage)); diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx index ddf886eeee6..2b2895124c9 100644 --- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; @@ -20,23 +21,26 @@ import { DashboardScene } from '../scene/DashboardScene'; import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager'; -export interface Props - extends GrafanaRouteComponentProps {} - const selectors = e2eSelectors.pages.PublicDashboardScene; -export function PublicDashboardScenePage({ match, route }: Props) { +export type Props = Omit< + GrafanaRouteComponentProps, + 'match' | 'history' +>; + +export function PublicDashboardScenePage({ route }: Props) { + const { accessToken = '' } = useParams(); const stateManager = getDashboardScenePageStateManager(); const styles = useStyles2(getStyles); const { dashboard, isLoading, loadError } = stateManager.useState(); useEffect(() => { - stateManager.loadDashboard({ uid: match.params.accessToken!, route: DashboardRoutes.Public }); + stateManager.loadDashboard({ uid: accessToken, route: DashboardRoutes.Public }); return () => { stateManager.clearState(); }; - }, [stateManager, match.params.accessToken, route.routeName]); + }, [stateManager, accessToken, route.routeName]); if (!dashboard) { return ( diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index 6968530b55d..d3e509db17c 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -1,21 +1,17 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom-v5-compat'; import { useEffectOnce } from 'react-use'; import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer'; -import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { render } from 'test/test-utils'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; -import { locationService } from '@grafana/runtime'; import { Dashboard, DashboardCursorSync, FieldConfigSource, ThresholdsMode, Panel } from '@grafana/schema/src'; import config from 'app/core/config'; -import { GrafanaContext } from 'app/core/context/GrafanaContext'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import * as appTypes from 'app/types'; import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types'; -import { SafeDynamicImport } from '../../../core/components/DynamicImports/SafeDynamicImport'; import { configureStore } from '../../../store/configureStore'; import { Props as LazyLoaderProps } from '../dashgrid/LazyLoader'; import { DashboardModel } from '../state'; @@ -55,53 +51,38 @@ jest.mock('app/types', () => ({ useDispatch: () => jest.fn(), })); -jest.mock('react-router-dom-v5-compat', () => ({ - ...jest.requireActual('react-router-dom-v5-compat'), - useParams: jest.fn().mockReturnValue({ accessToken: 'an-access-token' }), -})); - const setup = (propOverrides?: Partial, initialState?: Partial) => { - const context = getGrafanaContextMock(); const store = configureStore(initialState); const props: Props = { ...getRouteComponentProps({ route: { routeName: DashboardRoutes.Public, path: '/public-dashboards/:accessToken', - component: SafeDynamicImport( - () => - import(/* webpackChunkName: "PublicDashboardPage"*/ 'app/features/dashboard/containers/PublicDashboardPage') - ), + component: () => null, }, }), }; Object.assign(props, propOverrides); - const { unmount, rerender } = render( - - - - - - - + render( + + } /> + , + { store, historyOptions: { initialEntries: [`/public-dashboards/an-access-token`] } } ); const wrappedRerender = (newProps: Partial) => { Object.assign(props, newProps); - return rerender( - - - - - - - + return render( + + } /> + , + { store, historyOptions: { initialEntries: [`/public-dashboards/an-access-token`] } } ); }; - return { rerender: wrappedRerender, unmount }; + return { rerender: wrappedRerender }; }; const selectors = e2eSelectors.components; diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 794baab1454..391863db5aa 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -27,7 +27,10 @@ import { getTimeSrv } from '../services/TimeSrv'; import { DashboardModel } from '../state'; import { initDashboard } from '../state/initDashboard'; -export type Props = GrafanaRouteComponentProps; +export type Props = Omit< + GrafanaRouteComponentProps, + 'match' | 'history' +>; const selectors = e2eSelectors.pages.PublicDashboard; diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx index dd8ea8b4826..04b3678a0a1 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx @@ -1,13 +1,10 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; -import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { screen, waitFor } from '@testing-library/react'; +import { Routes, Route } from 'react-router-dom-v5-compat'; +import { render } from 'test/test-utils'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { LocationServiceProvider, config, locationService } from '@grafana/runtime'; -import { GrafanaContext } from 'app/core/context/GrafanaContext'; +import { config, locationService } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; -import { configureStore } from 'app/store/configureStore'; import { DashboardRoutes } from '../../../types'; @@ -31,25 +28,23 @@ jest.mock('react-router-dom-v5-compat', () => ({ })); function setup(props: Partial) { - const context = getGrafanaContextMock(); - const store = configureStore({}); return render( - - - - - null, path: '/:accessToken' }} - match={{ params: { accessToken: 'an-access-token' }, isExact: true, path: '/', url: '/' }} - {...props} - /> - - - - + + null, path: '/:accessToken' }} + {...props} + /> + } + /> + , + { + historyOptions: { initialEntries: [`/public-dashboards/an-access-token`] }, + } ); } diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx index cb5020d9dda..50cffeea8ae 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx @@ -1,15 +1,10 @@ import { config } from '@grafana/runtime'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { PublicDashboardScenePage } from '../../dashboard-scene/pages/PublicDashboardScenePage'; -import PublicDashboardPage from './PublicDashboardPage'; -import { PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams } from './types'; +import PublicDashboardPage, { type Props } from './PublicDashboardPage'; -export type PublicDashboardPageProxyProps = GrafanaRouteComponentProps< - PublicDashboardPageRouteParams, - PublicDashboardPageRouteSearchParams ->; +export type PublicDashboardPageProxyProps = Props; function PublicDashboardPageProxy(props: PublicDashboardPageProxyProps) { if (config.featureToggles.publicDashboardsScene) { diff --git a/public/app/features/invites/SignupInvited.test.tsx b/public/app/features/invites/SignupInvited.test.tsx index 295b0779a95..9c7d8a684f5 100644 --- a/public/app/features/invites/SignupInvited.test.tsx +++ b/public/app/features/invites/SignupInvited.test.tsx @@ -2,11 +2,9 @@ import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from 'test/test-utils'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; - import { backendSrv } from '../../core/services/backend_srv'; -import { SignupInvitedPage, Props } from './SignupInvited'; +import { SignupInvitedPage } from './SignupInvited'; jest.mock('app/core/core', () => ({ contextSrv: { @@ -19,6 +17,11 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn().mockReturnValue({ code: 'some code' }), +})); + const defaultGet = { email: 'some.user@localhost', name: 'Some User', @@ -35,18 +38,7 @@ async function setupTestContext({ get = defaultGet }: { get?: typeof defaultGet const postSpy = jest.spyOn(backendSrv, 'post'); postSpy.mockResolvedValue([]); - const props: Props = { - ...getRouteComponentProps({ - match: { - params: { code: 'some code' }, - isExact: false, - path: '', - url: '', - }, - }), - }; - - render(); + render(); await waitFor(() => expect(getSpy).toHaveBeenCalled()); expect(getSpy).toHaveBeenCalledTimes(1); diff --git a/public/app/features/invites/SignupInvited.tsx b/public/app/features/invites/SignupInvited.tsx index 37b754f59bc..cfc9f023eed 100644 --- a/public/app/features/invites/SignupInvited.tsx +++ b/public/app/features/invites/SignupInvited.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import { useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; @@ -9,7 +10,6 @@ import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { w3cStandardEmailValidator } from '../admin/utils'; @@ -32,10 +32,8 @@ const navModel = { }, }; -export interface Props extends GrafanaRouteComponentProps<{ code: string }> {} - -export const SignupInvitedPage = ({ match }: Props) => { - const code = match.params.code; +export const SignupInvitedPage = () => { + const { code } = useParams(); const [initFormModel, setInitFormModel] = useState(); const [greeting, setGreeting] = useState(); const [invitedBy, setInvitedBy] = useState(); diff --git a/public/app/features/playlist/PlaylistEditPage.test.tsx b/public/app/features/playlist/PlaylistEditPage.test.tsx index 37d2f618493..aa5db979917 100644 --- a/public/app/features/playlist/PlaylistEditPage.test.tsx +++ b/public/app/features/playlist/PlaylistEditPage.test.tsx @@ -1,10 +1,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { History, Location } from 'history'; import { TestProvider } from 'test/helpers/TestProvider'; import { locationService } from '@grafana/runtime'; -import { RouteDescriptor } from 'app/core/navigation/types'; import { backendSrv } from 'app/core/services/backend_srv'; import { PlaylistEditPage } from './PlaylistEditPage'; @@ -24,11 +22,7 @@ jest.mock('app/core/components/TagFilter/TagFilter', () => ({ async function getTestContext({ name, interval, items, uid }: Partial = {}) { jest.clearAllMocks(); const playlist = { name, items, interval, uid } as unknown as Playlist; - const queryParams = {}; - const route = {} as RouteDescriptor; - const match = { isExact: false, path: '', url: '', params: { uid: 'foo' } }; - const location = {} as Location; - const history = {} as History; + const getMock = jest.spyOn(backendSrv, 'get'); const putMock = jest.spyOn(backendSrv, 'put').mockImplementation(() => Promise.resolve()); @@ -41,7 +35,7 @@ async function getTestContext({ name, interval, items, uid }: Partial const { rerender } = render( - + ); await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1)); diff --git a/public/app/features/playlist/PlaylistEditPage.tsx b/public/app/features/playlist/PlaylistEditPage.tsx index ac8327a883d..ff88b01cc84 100644 --- a/public/app/features/playlist/PlaylistEditPage.tsx +++ b/public/app/features/playlist/PlaylistEditPage.tsx @@ -1,10 +1,10 @@ +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { NavModelItem } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; import { t, Trans } from 'app/core/internationalization'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { PlaylistForm } from './PlaylistForm'; import { getPlaylistAPI } from './api'; @@ -14,11 +14,10 @@ export interface RouteParams { uid: string; } -interface Props extends GrafanaRouteComponentProps {} - -export const PlaylistEditPage = ({ match }: Props) => { +export const PlaylistEditPage = () => { + const { uid = '' } = useParams(); const api = getPlaylistAPI(); - const playlist = useAsync(() => api.getPlaylist(match.params.uid), [match.params]); + const playlist = useAsync(() => api.getPlaylist(uid), [uid]); const onSubmit = async (playlist: Playlist) => { await api.updatePlaylist(playlist); diff --git a/public/app/features/playlist/PlaylistStartPage.tsx b/public/app/features/playlist/PlaylistStartPage.tsx index 9958f919d34..4bead3289dd 100644 --- a/public/app/features/playlist/PlaylistStartPage.tsx +++ b/public/app/features/playlist/PlaylistStartPage.tsx @@ -1,3 +1,5 @@ +import { useParams } from 'react-router-dom-v5-compat'; + import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { playlistSrv } from './PlaylistSrv'; @@ -6,6 +8,7 @@ interface Props extends GrafanaRouteComponentProps<{ uid: string }> {} // This is a react page that just redirects to new URLs export default function PlaylistStartPage({ match }: Props) { - playlistSrv.start(match.params.uid); + const { uid = '' } = useParams(); + playlistSrv.start(uid); return null; } diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index 9eabe95f823..7853f723a4a 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -3,6 +3,7 @@ import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { useCallback, useEffect, useMemo, useReducer } from 'react'; import * as React from 'react'; import { useLocation, useRouteMatch } from 'react-router-dom'; +import { useParams } from 'react-router-dom-v5-compat'; import { AppEvents, @@ -38,7 +39,7 @@ import { buildPluginPageContext, PluginPageContext } from './PluginPageContext'; interface Props { // The ID of the plugin we would like to load and display - pluginId: string; + pluginId?: string; // The root navModelItem for the plugin (root = lives directly under 'home'). In case app does not need a nva model, // for example it's in some way embedded or shown in a sideview this can be undefined. pluginNavSection?: NavModelItem; @@ -55,6 +56,8 @@ interface State { const initialState: State = { loading: true, loadingError: false, pluginNav: null, plugin: null }; export function AppRootPage({ pluginId, pluginNavSection }: Props) { + const { pluginId: pluginIdParam = '' } = useParams(); + pluginId = pluginId || pluginIdParam; const addedLinksRegistry = useAddedLinksRegistry(); const addedComponentsRegistry = useAddedComponentsRegistry(); const exposedComponentsRegistry = useExposedComponentsRegistry(); diff --git a/public/app/features/plugins/routes.tsx b/public/app/features/plugins/routes.tsx index a5b232521a2..b4e81d38245 100644 --- a/public/app/features/plugins/routes.tsx +++ b/public/app/features/plugins/routes.tsx @@ -33,7 +33,7 @@ export function getAppPluginRoutes(): RouteDescriptor[] { { path: '/a/:pluginId', exact: false, // route everything under this path to the plugin, so it can define more routes under this path - component: ({ match }) => , + component: () => , }, ]; } diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index da39686354c..261b3cf5726 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -5,7 +5,6 @@ import { NavLandingPage } from 'app/core/components/NavLandingPage/NavLandingPag import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound'; import config from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; -import UserAdminPage from 'app/features/admin/UserAdminPage'; import LdapPage from 'app/features/admin/ldap/LdapPage'; import { getAlertingRoutes } from 'app/features/alerting/routes'; import { isAdmin, isLocalDevEnv, isOpenSourceEdition } from 'app/features/alerting/unified/utils/misc'; @@ -336,7 +335,9 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/admin/users/edit/:id', - component: UserAdminPage, + component: SafeDynamicImport( + () => import(/* webpackChunkName: "UserAdminPage" */ 'app/features/admin/UserAdminPage') + ), }, { path: '/admin/orgs', From f39c5ed9f79a773ae612e76fc9833a6784833082 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Tue, 1 Oct 2024 09:43:58 -0400 Subject: [PATCH 120/174] Alerting: Improve Amazon SNS documentation (#93862) * Alerting: Improve Amazon SNS documentation --- .../manage-contact-points/_index.md | 1 + .../integrations/configure-amazon-sns.md | 184 ++++++++++++++++-- 2 files changed, 167 insertions(+), 18 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index 1fa195f52fb..4631a6742b1 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -162,6 +162,7 @@ The following table lists the contact point integrations supported by Grafana. | Name | Type | | ---------------------------- | ------------------------- | | Alertmanager | `prometheus-alertmanager` | +| Amazon SNS | `sns` | | Cisco Webex Teams | `webex` | | DingDing | `dingding` | | [Discord](ref:discord) | `discord` | diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md index 6cd38615744..1763f0c25a3 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md @@ -18,34 +18,61 @@ weight: 0 # Configure Amazon SNS for Alerting -Use the Grafana Alerting - Amazon SNS integration to send notifications to Amazon SNS when your alerts are firing. +Use the Grafana Alerting - Amazon SNS integration to send notifications to Amazon SNS when your alerts are firing. You can receive notifications via the various subscriber channels supported by SNS. ## Before you begin -To configure Amazon SNS to receive alert notifications, complete the following steps. +Before you begin, ensure you have the following: -1. Create a new topic in https://console.aws.amazon.com/sns. -1. Open the topic and create a new subscription. -1. Choose the protocol HTTPS. -1. Copy the URL. +- **AWS SNS Topic**: An SNS topic to send notifications to. +- **AWS IAM Identity with necessary access**: An IAM identity (e.g. user, role) with the necessary permissions to publish messages to the SNS topic. -For more information, refer to [Amazon SNS documentation](https://docs.aws.amazon.com/sns/latest/dg/welcome.html). +For an example setup, see [Example Minimal Setup Using Assumed IAM Role]({{< relref "#example-minimal-setup-using-assumed-iam-role" >}}). -## Procedure +## Adding the SNS Contact Point in Grafana -To create your Amazon SNS integration in Grafana Alerting, complete the following steps. +With AWS resources configured, proceed to add SNS as a contact point in Grafana. -1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. -1. Click **+ Add contact point**. -1. Enter a contact point name. -1. From the Integration list, select **AWS SNS**. -1. Copy in the URL from above into the **The Amazon SNS API URL** field. -1. Click **Test** to check that your integration works. -1. Click **Save contact point**. +### 1. Add a New Contact Point -## Next steps +- Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. +- Click on **"Add contact point"**. +- **Name**: Enter a descriptive name (e.g., `AWS SNS`). +- Choose **"AWS SNS"** from the list of contact point types. -The Amazon SNS contact point is ready to receive alert notifications. +### 2. Configure SNS Settings + +#### SNS Settings + +- **The Amazon SNS API URL**: (Optional) The SNS API URL, e.g., `https://sns.us-east-2.amazonaws.com`. If not specified, the SNS API URL from the SNS SDK will be used. +- **Signature Version (sigv4)**: Configures AWS's Signature Verification 4 signing process to sign requests. + - **Region**: (Optional) The AWS region. If blank, the region from the default credentials chain is used. + - **Access Key**: The AWS API access key. + - **Secret Key**: The AWS API secret key. + - **Profile**: (Optional) Named AWS profile used to authenticate. + - **Role ARN**: (Optional) The ARN of an AWS IAM role to assume for authentication, serving as an alternative to using AWS API keys. +- **SNS topic ARN**: (Optional) If you don't specify this value, you must specify a value for the `Phone number` or `Target ARN`. If you are using a FIFO SNS topic you should set a message group interval longer than 5 minutes to prevent messages with the same group key being deduplicated by the SNS default deduplication window. +- **Phone number**: (Optional) Phone number if message is delivered via SMS in E.164 format. If you don't specify this value, you must specify a value for the `SNS topic ARN` or `Target ARN`. +- **Target ARN**: (Optional) The mobile platform endpoint ARN if message is delivered via mobile notifications. If you don't specify this value, you must specify a value for the `SNS topic ARN` or `Phone number`. +- **Subject**: (Optional) Customize the subject line or use the default template. This field is templateable. +- **Message**: (Optional) Customize the message content or use the default template. This field is templateable. +- **Attributes**: (Optional) Add any SNS message attributes. + +{{< admonition type="note" >}} +Both `Access Key` and `Secret Key` must be provided together or left blank together. If blank it defaults to a chain of credential +providers to search for credentials in environment variables, shared credential file, and EC2 Instance Roles. + +Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. +{{< /admonition >}} + +### 3. Test & Save the Contact Point + +- Click **"Test"** to verify that the SNS configuration is working correctly. +- After the test is successful, click **"Save contact point"** to add the SNS contact point. + +### 4. Next steps + +The SNS contact point is ready to receive alert notifications. To add this contact point to your alert, complete the following steps. @@ -55,3 +82,124 @@ To add this contact point to your alert, complete the following steps. 1. Under Notifications click **Select contact point**. 1. From the drop-down menu, select the previously created contact point. 1. **Click Save rule and exit**. + +## Example Minimal Setup Using Assumed IAM Role + +This section outlines a minimal setup to configure SNS with Grafana using an assumed IAM Role. + +### 1. Create an SNS Topic + +1. **Navigate to SNS in AWS Console**: + + - Go to the [Amazon SNS Console](https://console.aws.amazon.com/sns/v3/home). + +2. **Create a new topic** [[AWS Documentation](https://docs.aws.amazon.com/sns/latest/dg/sns-create-topic.html)]: + + - On the **Topics** page, choose **"Create topic"**. + - Select **"Standard"** as the type. + - Enter a **Name** for your topic, e.g., `My-Topic`. + - **Encryption**: Leave disabled for this minimal setup. + - Click **"Create topic"**. + +3. (Optional) **Add an email subscriber to help test** [[AWS Documentation](https://docs.aws.amazon.com/sns/latest/dg/sns-email-notifications.html)]: + - Within your newly created topic, click on **"Create subscription"**. + - **Protocol**: Choose `Email`. + - **Endpoint**: Enter your email address to receive test notifications. + - Click **"Create subscription"**. + - **Confirm Subscription**: Check your email and confirm the subscription by clicking the provided link. + +### 2. Create an IAM Role + +1. **Navigate to IAM in AWS Console**: + + - Go to the [IAM Console](https://console.aws.amazon.com/iam/home). + +2. **Create a new role** [[AWS Documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user.html)]: + + - On the **Roles** page, choose **"Create role"**. + - **Trusted Entity**: Select **"This account"**. + - Click **"Next"** until the end, name it (e.g., `GrafanaSNSRole`), and click **"Create role"**. + +3. **Attach Inline Policy**: + + - After creating the role, select it and navigate to the **"Permissions"** tab. + - Click on **"Add permission"** > **"Create inline policy"**. + - Switch to the **"JSON"** tab and paste the following policy, replacing `Resource` with your SNS topic ARN: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["sns:Publish", "sns:GetTopicAttributes"], + "Resource": "arn:aws:sns:::" + } + ] + } + ``` + + - Click **"Next"**, name it (e.g., `SNSPublishPolicy`), and click **"Create policy"**. + +### 3. Create an IAM Policy + +1. **Create a new policy to allow assuming the above IAM role** [[AWS Documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create-console.html)]: + + - In the IAM Console, on the **Policies** page, choose **"Create policy"**. + - Switch to the **"JSON"** tab and paste the following policy, replacing `Resource` with the ARN of the role you created earlier: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam:::role/GrafanaSNSRole" + } + ] + } + ``` + +2. **Review and Create**: + - Click **"Next"**, name it (e.g., `AssumeSNSRolePolicy`), and click **"Create policy"**. + +### 4. Create an IAM User + +1. **Create a new IAM user to assume the above role** [[AWS Documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html)]: + + - In the IAM Console, on the **Users** page, choose **"Create user"**. + - Enter a **User name**, e.g., `grafana-sns-user`. + - Click **"Next"**. + - Select **"Attach policies directly"**. + - Search for the policy you created earlier (`AssumeSNSRolePolicy`) and select it. + - Click **"Next"** , and click **"Create user"**. + +2. **Generate credentials**: + - Within your newly created user, click on **"Create access key"**. + - Select an appropriate use-case, e.g., `Application running outside AWS`. + - Click **"Next"** , and click **"Create access key"**. + - **Save Credentials**: Note the **Access key ID** and **Secret access key**. You'll need these for Grafana's configuration. + +### 5. Add the SNS Contact Point in Grafana + +After creating the IAM user and obtaining the necessary credentials, proceed to [configure the SNS contact point in Grafana]({{< relref "#adding-the-sns-contact-point-in-grafana" >}}) using the following details: + +- **The Amazon SNS API URL**: `https://sns.us-east-1.amazonaws.com` +- **Signature Version (sigv4)**: + - **Region**: `us-east-1` + - **Access Key**: ``. + - **Secret Key**: `` + - **Role ARN**: `arn:aws:iam:::role/GrafanaSNSRole` +- **SNS topic ARN**: `arn:aws:sns:::My-Topic` + +{{< admonition type="note" >}} +Replace the placeholder values (`https://sns.us-east-1.amazonaws.com`, `us-east-1`, ``, ``, `arn:aws:iam:::role/GrafanaSNSRole`, `arn:aws:sns:::My-Topic`) with your actual AWS credentials and ARNs. +{{< /admonition >}} + +## Additional Resources + +- [Amazon SNS Documentation](https://docs.aws.amazon.com/sns/index.html) +- [AWS IAM Documentation](https://docs.aws.amazon.com/iam/index.html) +- [Prometheus Alertmanager SNS Integration](https://prometheus.io/docs/alerting/configuration/#sns_config) +- [Cloudwatch AWS Authentication]({{< relref "../../../../datasources/aws-cloudwatch/aws-authentication" >}}) From a9095b1dd1f4847e5e9fef24eac1d8708fd7be30 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:27:28 +0100 Subject: [PATCH 121/174] I18n: Download translations from Crowdin (#94076) --- public/locales/de-DE/grafana.json | 5 +++++ public/locales/es-ES/grafana.json | 5 +++++ public/locales/fr-FR/grafana.json | 5 +++++ public/locales/pt-BR/grafana.json | 5 +++++ public/locales/zh-Hans/grafana.json | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 8bd5d6d6741..d9ac93238d1 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -751,6 +751,11 @@ } }, "dashboards": { + "panel-edit": { + "angular-deprecation-button-open-panel-json": "", + "angular-deprecation-description": "", + "angular-deprecation-heading": "" + }, "settings": { "variables": { "dependencies": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 14416cf1e63..dee20280dcb 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -751,6 +751,11 @@ } }, "dashboards": { + "panel-edit": { + "angular-deprecation-button-open-panel-json": "", + "angular-deprecation-description": "", + "angular-deprecation-heading": "" + }, "settings": { "variables": { "dependencies": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 1936e186ebc..c31d2765dee 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -751,6 +751,11 @@ } }, "dashboards": { + "panel-edit": { + "angular-deprecation-button-open-panel-json": "", + "angular-deprecation-description": "", + "angular-deprecation-heading": "" + }, "settings": { "variables": { "dependencies": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index edc9bbecf65..bd0f3bfc9fb 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -751,6 +751,11 @@ } }, "dashboards": { + "panel-edit": { + "angular-deprecation-button-open-panel-json": "", + "angular-deprecation-description": "", + "angular-deprecation-heading": "" + }, "settings": { "variables": { "dependencies": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index d546c2cbf8d..88ea0b9dc83 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -742,6 +742,11 @@ } }, "dashboards": { + "panel-edit": { + "angular-deprecation-button-open-panel-json": "", + "angular-deprecation-description": "", + "angular-deprecation-heading": "" + }, "settings": { "variables": { "dependencies": { From 1b82595251023ab80490d99811d5f81e4b7290e1 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:02:26 +0100 Subject: [PATCH 122/174] UI/AutoSizeInput: Fixes issue where controlledValue being null caused crash (#94078) --- packages/grafana-ui/src/components/Input/AutoSizeInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx index 6e5b4ccd837..de855ff5302 100644 --- a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx +++ b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx @@ -32,7 +32,7 @@ export const AutoSizeInput = React.forwardRef((props, r // Update internal state when controlled `value` prop changes useEffect(() => { - if (controlledValue !== undefined) { + if (controlledValue) { setValue(controlledValue); } }, [controlledValue]); From cbf8e7e679cd47b6faa875f073dd148baf960b34 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:28:26 +0200 Subject: [PATCH 123/174] Alerting docs: update that test functionality only for G Alertmanager (#94064) * Alerting docs: update that test functionality only for G Alertmanager * ran prettier * fixed alphabetical order * indentation * format * all pretty, no pity --------- Co-authored-by: tonypowa --- .../configure-notifications/manage-contact-points/_index.md | 4 ++++ .../integrations/configure-amazon-sns.md | 2 -- .../manage-contact-points/integrations/configure-discord.md | 2 ++ .../manage-contact-points/integrations/configure-email.md | 3 +++ .../integrations/configure-google-chat.md | 2 ++ .../manage-contact-points/integrations/configure-mqtt.md | 6 +++++- .../integrations/configure-opsgenie.md | 2 ++ .../manage-contact-points/integrations/configure-slack.md | 3 +++ .../manage-contact-points/integrations/configure-teams.md | 4 ++++ .../integrations/configure-telegram.md | 3 +++ .../manage-contact-points/integrations/pager-duty.md | 2 ++ .../manage-contact-points/integrations/webhook-notifier.md | 3 +++ 12 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index 4631a6742b1..7ab9ffe5385 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -87,6 +87,8 @@ refs: Use contact points to select your preferred communication channel for receiving notifications when your alert rules are firing. You can add, edit, delete, export, and test a contact point. +Testing a contact point is only available for Grafana Alertmanager. + On the **Contact Points** tab, you can: - Search for name and type of contact points and integrations. @@ -144,6 +146,8 @@ Complete the following steps to add templates to your contact point. ## Test a contact point +** For Grafana Alertmanager only.** + Complete the following steps to test a contact point. 1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md index 1763f0c25a3..e0209ac8f50 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md @@ -33,8 +33,6 @@ For an example setup, see [Example Minimal Setup Using Assumed IAM Role]({{< rel With AWS resources configured, proceed to add SNS as a contact point in Grafana. -### 1. Add a New Contact Point - - Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. - Click on **"Add contact point"**. - **Name**: Enter a descriptive name (e.g., `AWS SNS`). diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-discord.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-discord.md index 7c5baff4446..399c3b31579 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-discord.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-discord.md @@ -39,6 +39,8 @@ To create your Discord integration in Grafana Alerting, complete the following s 1. In the **Webhook URL** field, paste in your Webhook URL. 1. Click **Test** to check that your integration works. + ** For Grafana Alertmanager only.** + A test alert notification should be sent to the Discord channel that you associated with the Webhook. 1. Click **Save contact point**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-email.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-email.md index 81b6ccac4d8..3b2b1898dc5 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-email.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-email.md @@ -79,6 +79,9 @@ To set up email integration, complete the following steps. E-mail addresses are case sensitive. Ensure that the e-mail address entered is correct. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + 1. Click **Save contact point**. ## Next steps diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-google-chat.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-google-chat.md index e20a31606e9..b0440d06ff2 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-google-chat.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-google-chat.md @@ -39,6 +39,8 @@ To create your Google Chat integration in Grafana Alerting, complete the followi 1. In the **URL** field, paste in your Webhook URL. 1. Click **Test** to check that your integration works. + ** For Grafana Alertmanager only.** + A test alert notification should be sent to the Google Chat space that you associated with the Webhook. 1. Click **Save contact point**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md index 0ded51cd145..0f9567a0a21 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md @@ -14,7 +14,7 @@ labels: - oss menuTitle: MQTT notifier title: Configure the MQTT notifier for Alerting -weight: 80 +weight: 0 --- # Configure the MQTT notifier for Alerting @@ -33,7 +33,11 @@ To configure the MQTT integration for Alerting, complete the following steps. 1. Enter the MQTT topic name in the **Topic** field. 1. In **Optional MQTT settings**, specify additional settings for the MQTT integration if needed. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + A test alert notification should be sent to the MQTT broker. + 1. Click **Save** contact point. The integration sends data in JSON format by default. You can change that using **Message format** field in the **Optional MQTT settings** section. There are two supported formats: diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-opsgenie.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-opsgenie.md index 0ffa3dcf20e..e1f455af2dc 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-opsgenie.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-opsgenie.md @@ -44,6 +44,8 @@ To create your Opsgenie integration in Grafana Alerting, complete the following 1. In the **Alert API URL**, enter `https://api.opsgenie.com/v2/alerts`. 1. Click **Test** to check that your integration works. + ** For Grafana Alertmanager only.** + A test alert notification is sent to the Alerts page in Opsgenie. 1. Click **Save contact point**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md index 6777771bf9a..359cebc1bd3 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md @@ -78,6 +78,9 @@ To create your Slack integration in Grafana Alerting, complete the following ste - In the **Token** field, copy in the Bot User OAuth Token that starts with “xoxb-”. 1. If you are using a Webhook URL, in the **Webhook** field, copy in your Slack app Webhook URL. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + 1. Click **Save contact point**. ## Next steps diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-teams.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-teams.md index 1141af8db6c..4f720bb616d 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-teams.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-teams.md @@ -43,7 +43,11 @@ To create your MS Teams integration in Grafana Alerting, complete the following 1. From the Integration list, select **Microsoft Teams**. 1. In the **URL** field, copy in your Webhook URL. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + A test alert notification should be sent to the MS Team channel. + 1. Click **Save** contact point. ## Next steps diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md index dd5ca20d7bb..1c86f94d0cc 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md @@ -64,6 +64,9 @@ To create your Telegram integration in Grafana Alerting, complete the following 1. In the **BOT API Token** field, copy in the bot API token. 1. In the **Chat ID** field, copy in the chat ID. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + 1. Click **Save contact point**. ## Next steps diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md index d6202d7adc4..ebfdec1f364 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md @@ -57,6 +57,8 @@ To create your PagerDuty integration in Grafana Alerting, complete the following 1. In the **Integration Key** field, copy in your integration key. 1. Click **Test** to check that your integration works. + ** For Grafana Alertmanager only.** + An incident should display in the Service’s Activity tab in PagerDuty. 1. Click **Save contact point**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md index 556d66755ce..17468cac706 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md @@ -152,6 +152,9 @@ To create your Webhook integration in Grafana Alerting, complete the following s 1. From the Integration list, select **Webhook**. 1. In the **URL** field, copy in your Webhook URL. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + 1. Click **Save contact point**. ## Next steps From a87df0528b2199405cec27589db718b51326e465 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Tue, 1 Oct 2024 18:33:06 +0300 Subject: [PATCH 124/174] Prometheus: Interpolate vars in adhoc filters request (#94087) * Interpolate vars in adhoc filters request * interpolate variables on BE --------- Co-authored-by: Kyle Brandt --- packages/grafana-prometheus/src/datasource.ts | 10 ++++++++-- .../grafana-prometheus/src/language_provider.ts | 7 ++++++- pkg/promlib/models/query.go | 6 +++--- pkg/promlib/resource/resource.go | 13 ++++++++++++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index e69327b718e..29b53f51902 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -449,8 +449,7 @@ export class PrometheusDatasource } const scopedVars = { - __interval: { text: this.interval, value: this.interval }, - __interval_ms: { text: rangeUtil.intervalToMs(this.interval), value: rangeUtil.intervalToMs(this.interval) }, + ...this.getIntervalVars(), ...this.getRangeScopedVars(options?.range ?? getDefaultTimeRange()), }; const interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr); @@ -458,6 +457,13 @@ export class PrometheusDatasource return metricFindQuery.process(options?.range ?? getDefaultTimeRange()); } + getIntervalVars() { + return { + __interval: { text: this.interval, value: this.interval }, + __interval_ms: { text: rangeUtil.intervalToMs(this.interval), value: rangeUtil.intervalToMs(this.interval) }, + }; + } + getRangeScopedVars(range: TimeRange) { const msRange = range.to.diff(range.from); const sRange = Math.round(msRange / 1000); diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index d645c3ba74b..105c354dcae 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -442,7 +442,12 @@ export default class PromQlLanguageProvider extends LanguageProvider { [], { labelName, - queries: queries?.map((q) => q.expr), + queries: queries?.map((q) => + this.datasource.interpolateString(q.expr, { + ...this.datasource.getIntervalVars(), + ...this.datasource.getRangeScopedVars(this.timeRange), + }) + ), scopes: scopes?.reduce((acc, scope) => { acc.push(...scope.spec.filters); diff --git a/pkg/promlib/models/query.go b/pkg/promlib/models/query.go index 002102db219..c54e51fea41 100644 --- a/pkg/promlib/models/query.go +++ b/pkg/promlib/models/query.go @@ -207,7 +207,7 @@ func Parse(span trace.Span, query backend.DataQuery, dsScrapeInterval string, in // Interpolate variables in expr timeRange := query.TimeRange.To.Sub(query.TimeRange.From) - expr := interpolateVariables( + expr := InterpolateVariables( model.Expr, query.Interval, calculatedStep, @@ -365,14 +365,14 @@ func calculateRateInterval( return rateInterval } -// interpolateVariables interpolates built-in variables +// InterpolateVariables interpolates built-in variables // expr PromQL query // queryInterval Requested interval in milliseconds. This value may be overridden by MinStep in query options // calculatedStep Calculated final step value. It was calculated in calculatePrometheusInterval // requestedMinStep Requested minimum step value. QueryModel.interval // dsScrapeInterval Data source scrape interval in the config // timeRange Requested time range for query -func interpolateVariables( +func InterpolateVariables( expr string, queryInterval time.Duration, calculatedStep time.Duration, diff --git a/pkg/promlib/resource/resource.go b/pkg/promlib/resource/resource.go index bf880d5564a..66483872972 100644 --- a/pkg/promlib/resource/resource.go +++ b/pkg/promlib/resource/resource.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "slices" + "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" @@ -148,7 +149,17 @@ func (r *Resource) GetSuggestions(ctx context.Context, req *backend.CallResource selectorList := []string{} for _, query := range sugReq.Queries { - s, err := getSelectors(query) + // Since we are only extracting selectors from the metric name, we can use dummy + // time durations. + interpolatedQuery := models.InterpolateVariables( + query, + time.Minute, + time.Minute, + "1m", + "15s", + time.Minute, + ) + s, err := getSelectors(interpolatedQuery) if err != nil { return nil, fmt.Errorf("error parsing selectors: %v", err) } From e0294b0b83355c290b5cb7f5111dd904f4387b5f Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:57:59 +0100 Subject: [PATCH 125/174] Release: update changelog for 10.3.11 (#94090) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a7201ada6..2cfcfefa45f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 10.3.11 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93360](https://github.com/grafana/grafana/pull/93360), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) + +### Bug fixes + +- **Correlations:** Limit access to correlations page to users who can access Explore [#93672](https://github.com/grafana/grafana/pull/93672), [@ifrost](https://github.com/ifrost) + + # 11.2.1 (2024-09-26) From 74fa0af1c2878dbdfba70d3a66ea5ce40bd32643 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:29:32 +0100 Subject: [PATCH 126/174] Release: update changelog for 10.4.10 (#94095) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cfcfefa45f..0b368346de1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ + + +# 10.4.10 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93359](https://github.com/grafana/grafana/pull/93359), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) + +### Bug fixes + +- **AzureMonitor:** Deduplicate resource picker rows [#93702](https://github.com/grafana/grafana/pull/93702), [@aangelisc](https://github.com/aangelisc) +- **Correlations:** Limit access to correlations page to users who can access Explore [#93673](https://github.com/grafana/grafana/pull/93673), [@ifrost](https://github.com/ifrost) + + # 10.3.11 (2024-10-01) From aa225a4450593ecd71dec9ea285c42c78c997973 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:57:14 +0000 Subject: [PATCH 127/174] Release: update changelog for 11.0.6 (#94098) * Update changelog * add alerting cve fix note --------- Co-authored-by: github-actions[bot] Co-authored-by: Josh Hunt --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b368346de1..50b9951ebad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + + +# 11.0.6 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93358](https://github.com/grafana/grafana/pull/93358), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) + +### Bug fixes + +- **AzureMonitor:** Deduplicate resource picker rows [#93703](https://github.com/grafana/grafana/pull/93703), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Improve resource picker efficiency [#93438](https://github.com/grafana/grafana/pull/93438), [@aangelisc](https://github.com/aangelisc) +- **Correlations:** Limit access to correlations page to users who can access Explore [#93674](https://github.com/grafana/grafana/pull/93674), [@ifrost](https://github.com/ifrost) +- **Plugins:** Avoid returning 404 for `AutoEnabled` apps [#93486](https://github.com/grafana/grafana/pull/93486), [@wbrowne](https://github.com/wbrowne) +- **Alerting:** Fixed CVE-2024-8118. + + # 10.4.10 (2024-10-01) From 78290301f4a6299890e929799acef7f709376cf9 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 1 Oct 2024 14:17:57 -0400 Subject: [PATCH 128/174] Alerting: Update GettableRuleGroupConfig and PostableRuleGroupConfig with missing fields supported by Prometheus (#94030) --- pkg/services/ngalert/api/tooling/api.json | 67 +++++++++++++++++++ .../api/tooling/definitions/cortex-ruler.go | 27 ++++++-- pkg/services/ngalert/api/tooling/post.json | 67 +++++++++++++++++++ pkg/services/ngalert/api/tooling/spec.json | 67 +++++++++++++++++++ public/api-merged.json | 67 +++++++++++++++++++ public/openapi3.json | 67 +++++++++++++++++++ 6 files changed, 358 insertions(+), 4 deletions(-) diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 6539d39c71e..965cee8114f 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -163,6 +163,14 @@ ], "type": "object" }, + "AlertRuleEditorSettings": { + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + }, + "type": "object" + }, "AlertRuleExport": { "properties": { "annotations": { @@ -286,6 +294,14 @@ }, "type": "object" }, + "AlertRuleMetadata": { + "properties": { + "editor_settings": { + "$ref": "#/definitions/AlertRuleEditorSettings" + } + }, + "type": "object" + }, "AlertRuleNotificationSettings": { "properties": { "group_by": { @@ -1573,6 +1589,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -1660,12 +1679,25 @@ }, "GettableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/GettableExtendedRuleNode" @@ -2786,6 +2818,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "no_data_state": { "enum": [ "Alerting", @@ -2824,17 +2859,36 @@ }, "PostableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/PostableExtendedRuleNode" }, "type": "array" + }, + "source_tenants": { + "items": { + "type": "string" + }, + "type": "array" } }, "type": "object" @@ -3616,12 +3670,25 @@ }, "RuleGroupConfigResponse": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/GettableExtendedRuleNode" diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index cf4c693ad69..bca1323a41b 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -237,6 +237,14 @@ type PostableRuleGroupConfig struct { Name string `yaml:"name" json:"name"` Interval model.Duration `yaml:"interval,omitempty" json:"interval,omitempty"` Rules []PostableExtendedRuleNode `yaml:"rules" json:"rules"` + + // fields below are used by Mimir/Loki rulers + + SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"` + EvaluationDelay *model.Duration `yaml:"evaluation_delay,omitempty" json:"evaluation_delay,omitempty"` + QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"` + AlignEvaluationTimeOnInterval bool `yaml:"align_evaluation_time_on_interval,omitempty" json:"align_evaluation_time_on_interval,omitempty"` + Limit int `yaml:"limit,omitempty" json:"limit,omitempty"` } func (c *PostableRuleGroupConfig) UnmarshalJSON(b []byte) error { @@ -275,15 +283,26 @@ func (c *PostableRuleGroupConfig) validate() error { if hasGrafRules && hasLotexRules { return fmt.Errorf("cannot mix Grafana & Prometheus style rules") } + + if hasGrafRules && (len(c.SourceTenants) > 0 || c.EvaluationDelay != nil || c.QueryOffset != nil || c.AlignEvaluationTimeOnInterval || c.Limit > 0) { + return fmt.Errorf("fields source_tenants, evaluation_delay, query_offset, align_evaluation_time_on_interval and limit are not supported for Grafana rules") + } return nil } // swagger:model type GettableRuleGroupConfig struct { - Name string `yaml:"name" json:"name"` - Interval model.Duration `yaml:"interval,omitempty" json:"interval,omitempty"` - SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"` - Rules []GettableExtendedRuleNode `yaml:"rules" json:"rules"` + Name string `yaml:"name" json:"name"` + Interval model.Duration `yaml:"interval,omitempty" json:"interval,omitempty"` + Rules []GettableExtendedRuleNode `yaml:"rules" json:"rules"` + + // fields below are used by Mimir/Loki rulers + + SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"` + EvaluationDelay *model.Duration `yaml:"evaluation_delay,omitempty" json:"evaluation_delay,omitempty"` + QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"` + AlignEvaluationTimeOnInterval bool `yaml:"align_evaluation_time_on_interval,omitempty" json:"align_evaluation_time_on_interval,omitempty"` + Limit int `yaml:"limit,omitempty" json:"limit,omitempty"` } func (c *GettableRuleGroupConfig) UnmarshalJSON(b []byte) error { diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 4371f639744..eae46145ba8 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -163,6 +163,14 @@ ], "type": "object" }, + "AlertRuleEditorSettings": { + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + }, + "type": "object" + }, "AlertRuleExport": { "properties": { "annotations": { @@ -286,6 +294,14 @@ }, "type": "object" }, + "AlertRuleMetadata": { + "properties": { + "editor_settings": { + "$ref": "#/definitions/AlertRuleEditorSettings" + } + }, + "type": "object" + }, "AlertRuleNotificationSettings": { "properties": { "group_by": { @@ -1573,6 +1589,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -1660,12 +1679,25 @@ }, "GettableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/GettableExtendedRuleNode" @@ -2786,6 +2818,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "no_data_state": { "enum": [ "Alerting", @@ -2824,17 +2859,36 @@ }, "PostableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/PostableExtendedRuleNode" }, "type": "array" + }, + "source_tenants": { + "items": { + "type": "string" + }, + "type": "array" } }, "type": "object" @@ -3616,12 +3670,25 @@ }, "RuleGroupConfigResponse": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/GettableExtendedRuleNode" diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 61e5ec742aa..95716ea1899 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -3794,6 +3794,14 @@ } } }, + "AlertRuleEditorSettings": { + "type": "object", + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + } + }, "AlertRuleExport": { "type": "object", "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", @@ -3917,6 +3925,14 @@ } } }, + "AlertRuleMetadata": { + "type": "object", + "properties": { + "editor_settings": { + "$ref": "#/definitions/AlertRuleEditorSettings" + } + } + }, "AlertRuleNotificationSettings": { "type": "object", "required": [ @@ -5206,6 +5222,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -5293,12 +5312,25 @@ "GettableRuleGroupConfig": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { @@ -6420,6 +6452,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "no_data_state": { "type": "string", "enum": [ @@ -6458,17 +6493,36 @@ "PostableRuleGroupConfig": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { "$ref": "#/definitions/PostableExtendedRuleNode" } + }, + "source_tenants": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -7250,12 +7304,25 @@ "RuleGroupConfigResponse": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { diff --git a/public/api-merged.json b/public/api-merged.json index 65ede3f4d53..a149ee3ead8 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -12394,6 +12394,14 @@ } } }, + "AlertRuleEditorSettings": { + "type": "object", + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + } + }, "AlertRuleExport": { "type": "object", "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", @@ -12517,6 +12525,14 @@ } } }, + "AlertRuleMetadata": { + "type": "object", + "properties": { + "editor_settings": { + "$ref": "#/definitions/AlertRuleEditorSettings" + } + } + }, "AlertRuleNotificationSettings": { "type": "object", "required": [ @@ -15799,6 +15815,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -15886,12 +15905,25 @@ "GettableRuleGroupConfig": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { @@ -18087,6 +18119,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "no_data_state": { "type": "string", "enum": [ @@ -18125,17 +18160,36 @@ "PostableRuleGroupConfig": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { "$ref": "#/definitions/PostableExtendedRuleNode" } + }, + "source_tenants": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -19600,12 +19654,25 @@ "RuleGroupConfigResponse": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { diff --git a/public/openapi3.json b/public/openapi3.json index 1b7bb227b26..65eec59a72c 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -2621,6 +2621,14 @@ ], "type": "object" }, + "AlertRuleEditorSettings": { + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + }, + "type": "object" + }, "AlertRuleExport": { "properties": { "annotations": { @@ -2744,6 +2752,14 @@ }, "type": "object" }, + "AlertRuleMetadata": { + "properties": { + "editor_settings": { + "$ref": "#/components/schemas/AlertRuleEditorSettings" + } + }, + "type": "object" + }, "AlertRuleNotificationSettings": { "properties": { "group_by": { @@ -6025,6 +6041,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/components/schemas/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -6112,12 +6131,25 @@ }, "GettableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/components/schemas/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/components/schemas/GettableExtendedRuleNode" @@ -8313,6 +8345,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/components/schemas/AlertRuleMetadata" + }, "no_data_state": { "enum": [ "Alerting", @@ -8351,17 +8386,36 @@ }, "PostableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/components/schemas/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/components/schemas/PostableExtendedRuleNode" }, "type": "array" + }, + "source_tenants": { + "items": { + "type": "string" + }, + "type": "array" } }, "type": "object" @@ -9826,12 +9880,25 @@ }, "RuleGroupConfigResponse": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/components/schemas/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/components/schemas/GettableExtendedRuleNode" From 7c3fc2f2619a0eb2157ea27e7dfef91996a9de7c Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Tue, 1 Oct 2024 14:45:47 -0400 Subject: [PATCH 129/174] Revert "Unistore : Ensure Watch works in HA mode." (#94097) Revert "Unistore : Ensure Watch works in HA mode. (#93428)" This reverts commit 0a26c9e9aec15ba0d21a836e1cbc1b03bad4bc34. --- .../storage/testing/watcher_tests.go | 35 +- pkg/storage/unified/apistore/store.go | 164 +++++++- pkg/storage/unified/apistore/store_test.go | 13 +- pkg/storage/unified/apistore/stream.go | 57 +-- pkg/storage/unified/apistore/watcher_test.go | 245 +++--------- pkg/storage/unified/apistore/watchset.go | 376 ++++++++++++++++++ pkg/storage/unified/resource/server.go | 118 ++---- pkg/storage/unified/sql/backend.go | 46 +-- pkg/storage/unified/sql/backend_test.go | 8 +- .../sql/data/resource_history_insert.sql | 2 - .../sql/data/resource_history_poll.sql | 3 +- .../unified/sql/data/resource_insert.sql | 2 - .../sql/data/resource_version_insert.sql | 2 +- .../unified/sql/db/migrations/resource_mig.go | 20 +- pkg/storage/unified/sql/queries.go | 2 - pkg/storage/unified/sql/queries_test.go | 15 +- ...ry_insert-insert into resource_history.sql | 2 - ...sql--resource_history_poll-single path.sql | 16 - .../mysql--resource_insert-simple.sql | 2 - ...l--resource_version_insert-single path.sql | 2 +- ...ry_insert-insert into resource_history.sql | 2 - ...res--resource_history_poll-single path.sql | 16 - .../postgres--resource_insert-simple.sql | 2 - ...s--resource_version_insert-single path.sql | 2 +- ...ry_insert-insert into resource_history.sql | 2 - ...ite--resource_history_poll-single path.sql | 16 - .../sqlite--resource_insert-simple.sql | 2 - ...e--resource_version_insert-single path.sql | 2 +- 28 files changed, 699 insertions(+), 475 deletions(-) create mode 100644 pkg/storage/unified/apistore/watchset.go delete mode 100755 pkg/storage/unified/sql/testdata/mysql--resource_history_poll-single path.sql delete mode 100755 pkg/storage/unified/sql/testdata/postgres--resource_history_poll-single path.sql delete mode 100755 pkg/storage/unified/sql/testdata/sqlite--resource_history_poll-single path.sql diff --git a/pkg/apiserver/storage/testing/watcher_tests.go b/pkg/apiserver/storage/testing/watcher_tests.go index 213b7553faa..1684f15819f 100644 --- a/pkg/apiserver/storage/testing/watcher_tests.go +++ b/pkg/apiserver/storage/testing/watcher_tests.go @@ -1407,25 +1407,22 @@ func RunWatchSemantics(ctx context.Context, t *testing.T, store storage.Interfac podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, }, - // Not Supported by unistore because there is no way to differentiate between: - // - SendInitialEvents=nil && resourceVersion=0 - // - sendInitialEvents=false && resourceVersion=0 - // This is a Legacy feature in k8s.io/apiserver/pkg/storage/etcd3/watcher_test.go#196 - // { - // name: "legacy, RV=0", - // resourceVersion: "0", - // initialPods: []*example.Pod{makePod("1"), makePod("2"), makePod("3")}, - // expectedInitialEventsInRandomOrder: addEventsFromCreatedPods, - // podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, - // expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, - // }, - // { - // name: "legacy, RV=unset", - // initialPods: []*example.Pod{makePod("1"), makePod("2"), makePod("3")}, - // expectedInitialEventsInRandomOrder: addEventsFromCreatedPods, - // podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, - // expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, - // }, + + { + name: "legacy, RV=0", + resourceVersion: "0", + initialPods: []*example.Pod{makePod("1"), makePod("2"), makePod("3")}, + expectedInitialEventsInRandomOrder: addEventsFromCreatedPods, + podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, + expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, + }, + { + name: "legacy, RV=unset", + initialPods: []*example.Pod{makePod("1"), makePod("2"), makePod("3")}, + expectedInitialEventsInRandomOrder: addEventsFromCreatedPods, + podsAfterEstablishingWatch: []*example.Pod{makePod("4"), makePod("5")}, + expectedEventsAfterEstablishingWatch: addEventsFromCreatedPods, + }, } for idx, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { diff --git a/pkg/storage/unified/apistore/store.go b/pkg/storage/unified/apistore/store.go index d4911da167c..4d2947ff54b 100644 --- a/pkg/storage/unified/apistore/store.go +++ b/pkg/storage/unified/apistore/store.go @@ -26,6 +26,7 @@ import ( "k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/apiserver/pkg/storage/storagebackend/factory" "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" "github.com/grafana/grafana/pkg/apimachinery/utils" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" @@ -50,6 +51,7 @@ type Storage struct { store resource.ResourceClient getKey func(string) (*resource.ResourceKey, error) + watchSet *WatchSet versioner storage.Versioner } @@ -82,7 +84,8 @@ func NewStorage( trigger: trigger, indexers: indexers, - getKey: keyParser, + watchSet: NewWatchSet(), + getKey: keyParser, versioner: &storage.APIObjectVersioner{}, } @@ -109,7 +112,9 @@ func NewStorage( } } - return s, func() {}, nil + return s, func() { + s.watchSet.cleanupWatchers() + }, nil } func (s *Storage) Versioner() storage.Versioner { @@ -160,6 +165,11 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou }) } + s.watchSet.notifyWatchers(watch.Event{ + Object: out.DeepCopyObject(), + Type: watch.Added, + }, nil) + return nil } @@ -216,11 +226,16 @@ func (s *Storage) Delete( if err := s.versioner.UpdateObject(out, uint64(rsp.ResourceVersion)); err != nil { return err } + + s.watchSet.notifyWatchers(watch.Event{ + Object: out.DeepCopyObject(), + Type: watch.Deleted, + }, nil) return nil } // This version is not yet passing the watch tests -func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { +func (s *Storage) WatchNEXT(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { k, err := s.getKey(key) if err != nil { return watch.NewEmptyWatch(), nil @@ -240,11 +255,10 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption if opts.SendInitialEvents != nil { cmd.SendInitialEvents = *opts.SendInitialEvents } - ctx, cancelWatch := context.WithCancel(ctx) + client, err := s.store.Watch(ctx, cmd) if err != nil { // if the context was canceled, just return a new empty watch - cancelWatch() if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.EOF) { return watch.NewEmptyWatch(), nil } @@ -252,11 +266,138 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption } reporter := apierrors.NewClientErrorReporter(500, "WATCH", "") - decoder := newStreamDecoder(client, s.newFunc, predicate, s.codec, cancelWatch) + decoder := &streamDecoder{ + client: client, + newFunc: s.newFunc, + predicate: predicate, + codec: s.codec, + } return watch.NewStreamWatcher(decoder, reporter), nil } +// Watch begins watching the specified key. Events are decoded into API objects, +// and any items selected by the predicate are sent down to returned watch.Interface. +// resourceVersion may be used to specify what version to begin watching, +// which should be the current resourceVersion, and no longer rv+1 +// (e.g. reconnecting without missing any updates). +// If resource version is "0", this interface will get current object at given key +// and send it in an "ADDED" event, before watch starts. +func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { + k, err := s.getKey(key) + if err != nil { + return watch.NewEmptyWatch(), nil + } + + req, predicate, err := toListRequest(k, opts) + if err != nil { + return watch.NewEmptyWatch(), nil + } + + listObj := s.newListFunc() + + var namespace *string + if k.Namespace != "" { + namespace = &k.Namespace + } + + if ctx.Err() != nil { + return watch.NewEmptyWatch(), nil + } + + if (opts.SendInitialEvents == nil && req.ResourceVersion == 0) || (opts.SendInitialEvents != nil && *opts.SendInitialEvents) { + if err := s.GetList(ctx, key, opts, listObj); err != nil { + return nil, err + } + + listAccessor, err := meta.ListAccessor(listObj) + if err != nil { + klog.Errorf("could not determine new list accessor in watch") + return nil, err + } + // Updated if requesting RV was either "0" or "" + maybeUpdatedRV, err := s.versioner.ParseResourceVersion(listAccessor.GetResourceVersion()) + if err != nil { + klog.Errorf("could not determine new list RV in watch") + return nil, err + } + + jw := s.watchSet.newWatch(ctx, maybeUpdatedRV, predicate, s.versioner, namespace) + + initEvents := make([]watch.Event, 0) + listPtr, err := meta.GetItemsPtr(listObj) + if err != nil { + return nil, err + } + v, err := conversion.EnforcePtr(listPtr) + if err != nil || v.Kind() != reflect.Slice { + return nil, fmt.Errorf("need pointer to slice: %v", err) + } + + for i := 0; i < v.Len(); i++ { + obj, ok := v.Index(i).Addr().Interface().(runtime.Object) + if !ok { + return nil, fmt.Errorf("need item to be a runtime.Object: %v", err) + } + + initEvents = append(initEvents, watch.Event{ + Type: watch.Added, + Object: obj.DeepCopyObject(), + }) + } + + if predicate.AllowWatchBookmarks && len(initEvents) > 0 { + listRV, err := s.versioner.ParseResourceVersion(listAccessor.GetResourceVersion()) + if err != nil { + return nil, fmt.Errorf("could not get last init event's revision for bookmark: %v", err) + } + + bookmarkEvent := watch.Event{ + Type: watch.Bookmark, + Object: s.newFunc(), + } + + if err := s.versioner.UpdateObject(bookmarkEvent.Object, listRV); err != nil { + return nil, err + } + + bookmarkObject, err := meta.Accessor(bookmarkEvent.Object) + if err != nil { + return nil, fmt.Errorf("could not get bookmark object's acccesor: %v", err) + } + bookmarkObject.SetAnnotations(map[string]string{"k8s.io/initial-events-end": "true"}) + initEvents = append(initEvents, bookmarkEvent) + } + + jw.Start(initEvents...) + return jw, nil + } + + maybeUpdatedRV := uint64(req.ResourceVersion) + if maybeUpdatedRV == 0 { + rsp, err := s.store.List(ctx, &resource.ListRequest{ + Options: &resource.ListOptions{ + Key: k, + }, + Limit: 1, // we ignore the results, just look at the RV + }) + if err != nil { + return nil, err + } + if rsp.Error != nil { + return nil, resource.GetError(rsp.Error) + } + maybeUpdatedRV = uint64(rsp.ResourceVersion) + if maybeUpdatedRV < 1 { + return nil, fmt.Errorf("expecting a non-zero resource version") + } + } + jw := s.watchSet.newWatch(ctx, maybeUpdatedRV, predicate, s.versioner, namespace) + + jw.Start() + return jw, nil +} + // Get unmarshals object found at key into objPtr. On a not found error, will either // return a zero object of the requested type, or an error, depending on 'opts.ignoreNotFound'. // Treats empty responses and nil response nodes exactly like a not found error. @@ -527,6 +668,17 @@ func (s *Storage) GuaranteedUpdate( return err } + if created { + s.watchSet.notifyWatchers(watch.Event{ + Object: destination.DeepCopyObject(), + Type: watch.Added, + }, nil) + } else { + s.watchSet.notifyWatchers(watch.Event{ + Object: destination.DeepCopyObject(), + Type: watch.Modified, + }, existingObj.DeepCopyObject()) + } return nil } diff --git a/pkg/storage/unified/apistore/store_test.go b/pkg/storage/unified/apistore/store_test.go index 287aeea5c41..8977693c966 100644 --- a/pkg/storage/unified/apistore/store_test.go +++ b/pkg/storage/unified/apistore/store_test.go @@ -92,13 +92,12 @@ func TestCreate(t *testing.T) { storagetesting.RunTestCreate(ctx, t, store, checkStorageInvariants(store)) } -// No TTL support in unifed storage -// func TestCreateWithTTL(t *testing.T) { -// ctx, store, destroyFunc, err := testSetup(t) -// defer destroyFunc() -// assert.NoError(t, err) -// storagetesting.RunTestCreateWithTTL(ctx, t, store) -// } +func TestCreateWithTTL(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestCreateWithTTL(ctx, t, store) +} func TestCreateWithKeyExist(t *testing.T) { ctx, store, destroyFunc, err := testSetup(t) diff --git a/pkg/storage/unified/apistore/stream.go b/pkg/storage/unified/apistore/stream.go index 9546e3e8b64..a425279185a 100644 --- a/pkg/storage/unified/apistore/stream.go +++ b/pkg/storage/unified/apistore/stream.go @@ -1,11 +1,9 @@ package apistore import ( - "context" "errors" "fmt" "io" - "sync" grpcCodes "google.golang.org/grpc/codes" grpcStatus "google.golang.org/grpc/status" @@ -19,23 +17,12 @@ import ( ) type streamDecoder struct { - client resource.ResourceStore_WatchClient - newFunc func() runtime.Object - predicate storage.SelectionPredicate - codec runtime.Codec - cancelWatch context.CancelFunc - done sync.WaitGroup + client resource.ResourceStore_WatchClient + newFunc func() runtime.Object + predicate storage.SelectionPredicate + codec runtime.Codec } -func newStreamDecoder(client resource.ResourceStore_WatchClient, newFunc func() runtime.Object, predicate storage.SelectionPredicate, codec runtime.Codec, cancelWatch context.CancelFunc) *streamDecoder { - return &streamDecoder{ - client: client, - newFunc: newFunc, - predicate: predicate, - codec: codec, - cancelWatch: cancelWatch, - } -} func (d *streamDecoder) toObject(w *resource.WatchEvent_Resource) (runtime.Object, error) { obj, _, err := d.codec.Decode(w.Value, nil, d.newFunc()) if err == nil { @@ -48,30 +35,25 @@ func (d *streamDecoder) toObject(w *resource.WatchEvent_Resource) (runtime.Objec return obj, err } -// nolint: gocyclo // we may be able to simplify this in the future, but this is a complex function by nature func (d *streamDecoder) Decode() (action watch.EventType, object runtime.Object, err error) { - d.done.Add(1) - defer d.done.Done() decode: for { - var evt *resource.WatchEvent - var err error - select { - case <-d.client.Context().Done(): - default: - evt, err = d.client.Recv() + err := d.client.Context().Err() + if err != nil { + klog.Errorf("client: context error: %s\n", err) + return watch.Error, nil, err } - switch { - case errors.Is(d.client.Context().Err(), context.Canceled): - return watch.Error, nil, io.EOF - case d.client.Context().Err() != nil: - return watch.Error, nil, d.client.Context().Err() - case errors.Is(err, io.EOF): - return watch.Error, nil, io.EOF - case grpcStatus.Code(err) == grpcCodes.Canceled: + evt, err := d.client.Recv() + if errors.Is(err, io.EOF) { return watch.Error, nil, err - case err != nil: + } + + if grpcStatus.Code(err) == grpcCodes.Canceled { + return watch.Error, nil, err + } + + if err != nil { klog.Errorf("client: error receiving result: %s", err) return watch.Error, nil, err } @@ -212,15 +194,10 @@ decode: } func (d *streamDecoder) Close() { - // Close the send stream err := d.client.CloseSend() if err != nil { klog.Errorf("error closing watch stream: %s", err) } - // Cancel the send context - d.cancelWatch() - // Wait for all decode operations to finish - d.done.Wait() } var _ watch.Decoder = (*streamDecoder)(nil) diff --git a/pkg/storage/unified/apistore/watcher_test.go b/pkg/storage/unified/apistore/watcher_test.go index 203d14c0729..fb4deb11811 100644 --- a/pkg/storage/unified/apistore/watcher_test.go +++ b/pkg/storage/unified/apistore/watcher_test.go @@ -7,9 +7,9 @@ package apistore import ( "context" + "fmt" "os" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,20 +29,7 @@ import ( "k8s.io/apiserver/pkg/storage/storagebackend/factory" storagetesting "github.com/grafana/grafana/pkg/apiserver/storage/testing" - infraDB "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/storage/unified/sql" - "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" - "github.com/grafana/grafana/pkg/tests/testsuite" -) - -type StorageType string - -const ( - StorageTypeFile StorageType = "file" - StorageTypeUnified StorageType = "unified" ) var scheme = runtime.NewScheme() @@ -61,7 +48,6 @@ type setupOptions struct { prefix string resourcePrefix string groupResource schema.GroupResource - storageType StorageType } type setupOption func(*setupOptions, testing.TB) @@ -73,20 +59,10 @@ func withDefaults(options *setupOptions, t testing.TB) { options.prefix = t.TempDir() options.resourcePrefix = storagetesting.KeyFunc("", "") options.groupResource = schema.GroupResource{Resource: "pods"} - options.storageType = StorageTypeFile -} -func withStorageType(storageType StorageType) setupOption { - return func(options *setupOptions, t testing.TB) { - options.storageType = storageType - } } var _ setupOption = withDefaults -func TestMain(m *testing.M) { - testsuite.Run(m) -} - func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Interface, factory.DestroyFunc, error) { setupOpts := setupOptions{} opts = append([]setupOption{withDefaults}, opts...) @@ -109,56 +85,18 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte Metadata: fileblob.MetadataDontWrite, // skip }) require.NoError(t, err) + fmt.Printf("ROOT: %s\n\n", tmp) } ctx := storagetesting.NewContext() + backend, err := resource.NewCDKBackend(ctx, resource.CDKBackendOptions{ + Bucket: bucket, + }) + require.NoError(t, err) - var server resource.ResourceServer - switch setupOpts.storageType { - case StorageTypeFile: - backend, err := resource.NewCDKBackend(ctx, resource.CDKBackendOptions{ - Bucket: bucket, - }) - require.NoError(t, err) - - server, err = resource.NewResourceServer(resource.ResourceServerOptions{ - Backend: backend, - }) - require.NoError(t, err) - - // Issue a health check to ensure the server is initialized - _, err = server.IsHealthy(ctx, &resource.HealthCheckRequest{}) - require.NoError(t, err) - case StorageTypeUnified: - if testing.Short() { - t.Skip("skipping integration test") - } - dbstore := infraDB.InitTestDB(t) - cfg := setting.NewCfg() - features := featuremgmt.WithFeatures() - - eDB, err := dbimpl.ProvideResourceDB(dbstore, cfg, features, nil) - require.NoError(t, err) - require.NotNil(t, eDB) - - ret, err := sql.NewBackend(sql.BackendOptions{ - DBProvider: eDB, - PollingInterval: time.Millisecond, // Keep this fast - }) - require.NoError(t, err) - require.NotNil(t, ret) - ctx := storagetesting.NewContext() - err = ret.Init(ctx) - require.NoError(t, err) - - server, err = resource.NewResourceServer(resource.ResourceServerOptions{ - Backend: ret, - Diagnostics: ret, - Lifecycle: ret, - }) - require.NoError(t, err) - default: - t.Fatalf("unsupported storage type: %s", setupOpts.storageType) - } + server, err := resource.NewResourceServer(resource.ResourceServerOptions{ + Backend: backend, + }) + require.NoError(t, err) client := resource.NewLocalResourceClient(server) config := storagebackend.NewDefaultConfig(setupOpts.prefix, setupOpts.codec) @@ -186,82 +124,55 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte } func TestWatch(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t, withStorageType(s)) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatch(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatch(ctx, t, store) } func TestClusterScopedWatch(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestClusterScopedWatch(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestClusterScopedWatch(ctx, t, store) } func TestNamespaceScopedWatch(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestNamespaceScopedWatch(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestNamespaceScopedWatch(ctx, t, store) } func TestDeleteTriggerWatch(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestDeleteTriggerWatch(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestDeleteTriggerWatch(ctx, t, store) } -// Not Supported by unistore because there is no way to differentiate between: -// - SendInitialEvents=nil && resourceVersion=0 -// - sendInitialEvents=false && resourceVersion=0 -// This is a Legacy feature in k8s.io/apiserver/pkg/storage/etcd3/watcher_test.go#196 -// func TestWatchFromZero(t *testing.T) { -// ctx, store, destroyFunc, err := testSetup(t) -// defer destroyFunc() -// assert.NoError(t, err) -// storagetesting.RunTestWatchFromZero(ctx, t, store, nil) -// } +func TestWatchFromZero(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchFromZero(ctx, t, store, nil) +} // TestWatchFromNonZero tests that // - watch from non-0 should just watch changes after given version func TestWatchFromNonZero(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchFromNonZero(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchFromNonZero(ctx, t, store) } -/* -Only valid when using a cached storage func TestDelayedWatchDelivery(t *testing.T) { ctx, store, destroyFunc, err := testSetup(t) defer destroyFunc() assert.NoError(t, err) storagetesting.RunTestDelayedWatchDelivery(ctx, t, store) } -/* /* func TestWatchError(t *testing.T) { @@ -271,36 +182,24 @@ func TestWatchError(t *testing.T) { */ func TestWatchContextCancel(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchContextCancel(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchContextCancel(ctx, t, store) } func TestWatcherTimeout(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatcherTimeout(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatcherTimeout(ctx, t, store) } func TestWatchDeleteEventObjectHaveLatestRV(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchDeleteEventObjectHaveLatestRV(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchDeleteEventObjectHaveLatestRV(ctx, t, store) } // TODO: enable when we support flow control and priority fairness @@ -322,47 +221,31 @@ func TestWatchDeleteEventObjectHaveLatestRV(t *testing.T) { // setting allowWatchBookmarks query param against // etcd implementation doesn't have any effect. func TestWatchDispatchBookmarkEvents(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunTestWatchDispatchBookmarkEvents(ctx, t, store, false) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestWatchDispatchBookmarkEvents(ctx, t, store, false) } func TestSendInitialEventsBackwardCompatibility(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunSendInitialEventsBackwardCompatibility(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunSendInitialEventsBackwardCompatibility(ctx, t, store) } func TestEtcdWatchSemantics(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunWatchSemantics(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunWatchSemantics(ctx, t, store) } func TestEtcdWatchSemanticInitialEventsExtended(t *testing.T) { - for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { - t.Run(string(s), func(t *testing.T) { - ctx, store, destroyFunc, err := testSetup(t) - defer destroyFunc() - assert.NoError(t, err) - storagetesting.RunWatchSemanticInitialEventsExtended(ctx, t, store) - }) - } + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunWatchSemanticInitialEventsExtended(ctx, t, store) } func newPod() runtime.Object { diff --git a/pkg/storage/unified/apistore/watchset.go b/pkg/storage/unified/apistore/watchset.go new file mode 100644 index 00000000000..9c9d214b4b6 --- /dev/null +++ b/pkg/storage/unified/apistore/watchset.go @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Provenance-includes-location: https://github.com/tilt-dev/tilt-apiserver/blob/main/pkg/storage/filepath/watchset.go +// Provenance-includes-license: Apache-2.0 +// Provenance-includes-copyright: The Kubernetes Authors. + +package apistore + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/storage" + "k8s.io/klog/v2" +) + +const ( + UpdateChannelSize = 25 + InitialWatchNodesSize = 20 + InitialBufferedEventsSize = 25 +) + +type eventWrapper struct { + ev watch.Event + // optional: oldObject is only set for modifications for determining their type as necessary (when using predicate filtering) + oldObject runtime.Object +} + +type watchNode struct { + ctx context.Context + s *WatchSet + id uint64 + updateCh chan eventWrapper + outCh chan watch.Event + requestedRV uint64 + // the watch may or may not be namespaced for a namespaced resource. This is always nil for cluster-scoped kinds + watchNamespace *string + predicate storage.SelectionPredicate + versioner storage.Versioner +} + +// Keeps track of which watches need to be notified +type WatchSet struct { + mu sync.RWMutex + // mu protects both nodes and counter + nodes map[uint64]*watchNode + counter atomic.Uint64 + buffered []eventWrapper + bufferedMutex sync.RWMutex +} + +func NewWatchSet() *WatchSet { + return &WatchSet{ + buffered: make([]eventWrapper, 0, InitialBufferedEventsSize), + nodes: make(map[uint64]*watchNode, InitialWatchNodesSize), + } +} + +// Creates a new watch with a unique id, but +// does not start sending events to it until start() is called. +func (s *WatchSet) newWatch(ctx context.Context, requestedRV uint64, p storage.SelectionPredicate, versioner storage.Versioner, namespace *string) *watchNode { + s.counter.Add(1) + + node := &watchNode{ + ctx: ctx, + requestedRV: requestedRV, + id: s.counter.Load(), + s: s, + // updateCh size needs to be > 1 to allow slower clients to not block passing new events + updateCh: make(chan eventWrapper, UpdateChannelSize), + // outCh size needs to be > 1 for single process use-cases such as tests where watch and event seeding from CUD + // events is happening on the same thread + outCh: make(chan watch.Event, UpdateChannelSize), + predicate: p, + watchNamespace: namespace, + versioner: versioner, + } + + return node +} + +func (s *WatchSet) cleanupWatchers() { + s.mu.Lock() + defer s.mu.Unlock() + for _, w := range s.nodes { + w.stop() + } +} + +// oldObject is only passed in the event of a modification +// in case a predicate filtered watch is impacted as a result of modification +// NOTE: this function gives one the misperception that a newly added node will never +// get a double event, one from buffered and one from the update channel +// That perception is not true. Even though this function maintains the lock throughout the function body +// it is not true of the Start function. So basically, the Start function running after this function +// fully stands the chance of another future notifyWatchers double sending it the event through the two means mentioned +func (s *WatchSet) notifyWatchers(ev watch.Event, oldObject runtime.Object) { + s.mu.RLock() + defer s.mu.RUnlock() + + updateEv := eventWrapper{ + ev: ev, + } + if oldObject != nil { + updateEv.oldObject = oldObject + } + + // Events are always buffered. + // this is because of an inadvertent delay which is built into the watch process + // Watch() from storage returns Watch.Interface with a async start func. + // The only way to guarantee that we can interpret the passed RV correctly is to play it against missed events + // (notice the loop below over s.nodes isn't exactly going to work on a new node + // unless start is called on it) + s.bufferedMutex.Lock() + s.buffered = append(s.buffered, updateEv) + s.bufferedMutex.Unlock() + + for _, w := range s.nodes { + w.updateCh <- updateEv + } +} + +// isValid is not necessary to be called on oldObject in UpdateEvents - assuming the Watch pushes correctly setup eventWrapper our way +// first bool is whether the event is valid for current watcher +// second bool is whether checking the old value against the predicate may be valuable to the caller +// second bool may be a helpful aid to establish context around MODIFIED events +// (note that this second bool is only marked true if we pass other checks first, namely RV and namespace) +func (w *watchNode) isValid(e eventWrapper) (bool, bool, error) { + obj, err := meta.Accessor(e.ev.Object) + if err != nil { + klog.Error("Could not get accessor to object in event") + return false, false, nil + } + + eventRV, err := w.getResourceVersionAsInt(e.ev.Object) + if err != nil { + return false, false, err + } + + if eventRV < w.requestedRV { + return false, false, nil + } + + if w.watchNamespace != nil && *w.watchNamespace != obj.GetNamespace() { + return false, false, err + } + + valid, err := w.predicate.Matches(e.ev.Object) + if err != nil { + return false, false, err + } + + return valid, e.ev.Type == watch.Modified, nil +} + +// Only call this method if current object matches the predicate +func (w *watchNode) handleAddedForFilteredList(e eventWrapper) (*watch.Event, error) { + if e.oldObject == nil { + return nil, fmt.Errorf("oldObject should be set for modified events") + } + + ok, err := w.predicate.Matches(e.oldObject) + if err != nil { + return nil, err + } + + if !ok { + e.ev.Type = watch.Added + return &e.ev, nil + } + + return nil, nil +} + +func (w *watchNode) handleDeletedForFilteredList(e eventWrapper) (*watch.Event, error) { + if e.oldObject == nil { + return nil, fmt.Errorf("oldObject should be set for modified events") + } + + ok, err := w.predicate.Matches(e.oldObject) + if err != nil { + return nil, err + } + + if !ok { + return nil, nil + } + + // isn't a match but used to be + e.ev.Type = watch.Deleted + + oldObjectAccessor, err := meta.Accessor(e.oldObject) + if err != nil { + klog.Errorf("Could not get accessor to correct the old RV of filtered out object") + return nil, err + } + + currentRV, err := getResourceVersion(e.ev.Object) + if err != nil { + klog.Errorf("Could not get accessor to object in event") + return nil, err + } + + oldObjectAccessor.SetResourceVersion(currentRV) + e.ev.Object = e.oldObject + + return &e.ev, nil +} + +func (w *watchNode) processEvent(e eventWrapper, isInitEvent bool) error { + if isInitEvent { + // Init events have already been vetted against the predicate and other RV behavior + // Let them pass through + w.outCh <- e.ev + return nil + } + + valid, runDeleteFromFilteredListHandler, err := w.isValid(e) + if err != nil { + klog.Errorf("Could not determine validity of the event: %v", err) + return err + } + if valid { + if e.ev.Type == watch.Modified { + ev, err := w.handleAddedForFilteredList(e) + if err != nil { + return err + } + if ev != nil { + w.outCh <- *ev + } else { + // forward the original event if add handling didn't signal any impact + w.outCh <- e.ev + } + } else { + w.outCh <- e.ev + } + return nil + } + + if runDeleteFromFilteredListHandler { + if e.ev.Type == watch.Modified { + ev, err := w.handleDeletedForFilteredList(e) + if err != nil { + return err + } + if ev != nil { + w.outCh <- *ev + } + } // explicitly doesn't have an event forward for the else case here + return nil + } + + return nil +} + +// Start sending events to this watch. +func (w *watchNode) Start(initEvents ...watch.Event) { + w.s.mu.Lock() + w.s.nodes[w.id] = w + w.s.mu.Unlock() + + go func() { + maxRV := uint64(0) + for _, ev := range initEvents { + currentRV, err := w.getResourceVersionAsInt(ev.Object) + if err != nil { + klog.Errorf("Could not determine init event RV for deduplication of buffered events: %v", err) + continue + } + + if maxRV < currentRV { + maxRV = currentRV + } + + if err := w.processEvent(eventWrapper{ev: ev}, true); err != nil { + klog.Errorf("Could not process event: %v", err) + } + } + + // If we had no init events, simply rely on the passed RV + if maxRV == 0 { + maxRV = w.requestedRV + } + + w.s.bufferedMutex.RLock() + for _, e := range w.s.buffered { + eventRV, err := w.getResourceVersionAsInt(e.ev.Object) + if err != nil { + klog.Errorf("Could not determine RV for deduplication of buffered events: %v", err) + continue + } + + if maxRV >= eventRV { + continue + } else { + maxRV = eventRV + } + + if err := w.processEvent(e, false); err != nil { + klog.Errorf("Could not process event: %v", err) + } + } + w.s.bufferedMutex.RUnlock() + + for { + select { + case e, ok := <-w.updateCh: + if !ok { + close(w.outCh) + return + } + + eventRV, err := w.getResourceVersionAsInt(e.ev.Object) + if err != nil { + klog.Errorf("Could not determine RV for deduplication of channel events: %v", err) + continue + } + + if maxRV >= eventRV { + continue + } else { + maxRV = eventRV + } + + if err := w.processEvent(e, false); err != nil { + klog.Errorf("Could not process event: %v", err) + } + case <-w.ctx.Done(): + close(w.outCh) + return + } + } + }() +} + +func (w *watchNode) Stop() { + w.s.mu.Lock() + defer w.s.mu.Unlock() + w.stop() +} + +// Unprotected func: ensure mutex on the parent watch set is locked before calling +func (w *watchNode) stop() { + if _, ok := w.s.nodes[w.id]; ok { + delete(w.s.nodes, w.id) + close(w.updateCh) + } +} + +func (w *watchNode) ResultChan() <-chan watch.Event { + return w.outCh +} + +func getResourceVersion(obj runtime.Object) (string, error) { + accessor, err := meta.Accessor(obj) + if err != nil { + klog.Error("Could not get accessor to object in event") + return "", err + } + return accessor.GetResourceVersion(), nil +} + +func (w *watchNode) getResourceVersionAsInt(obj runtime.Object) (uint64, error) { + accessor, err := meta.Accessor(obj) + if err != nil { + klog.Error("Could not get accessor to object in event") + return 0, err + } + + return w.versioner.ParseResourceVersion(accessor.GetResourceVersion()) +} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index c44c1ac1ebc..8484653049a 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -7,7 +7,6 @@ import ( "log/slog" "net/http" "sync" - "sync/atomic" "time" "go.opentelemetry.io/otel/trace" @@ -162,15 +161,14 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { var _ ResourceServer = &server{} type server struct { - tracer trace.Tracer - log *slog.Logger - backend StorageBackend - index ResourceIndexServer - diagnostics DiagnosticsServer - access WriteAccessHooks - lifecycle LifecycleHooks - now func() int64 - mostRecentRV atomic.Int64 // The most recent resource version seen by the server + tracer trace.Tracer + log *slog.Logger + backend StorageBackend + index ResourceIndexServer + diagnostics DiagnosticsServer + access WriteAccessHooks + lifecycle LifecycleHooks + now func() int64 // Background watch task -- this has permissions for everything ctx context.Context @@ -345,12 +343,12 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons rsp.Error = e return rsp, nil } + var err error rsp.ResourceVersion, err = s.backend.WriteEvent(ctx, *event) if err != nil { rsp.Error = AsErrorResult(err) } - s.log.Debug("server.WriteEvent", "type", event.Type, "rv", rsp.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "name", event.Key.Name, "resource", event.Key.Resource) return rsp, nil } @@ -556,8 +554,6 @@ func (s *server) initWatcher() error { for { // pipe all events v := <-events - s.log.Debug("Server. Streaming Event", "type", v.Type, "previousRV", v.PreviousRV, "group", v.Key.Group, "namespace", v.Key.Namespace, "resource", v.Key.Resource, "name", v.Key.Name) - s.mostRecentRV.Store(v.ResourceVersion) out <- v } }() @@ -573,67 +569,23 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { return err } - // Start listening -- this will buffer any changes that happen while we backfill. - // If events are generated faster than we can process them, then some events will be dropped. - // TODO: Think of a way to allow the client to catch up. + // Start listening -- this will buffer any changes that happen while we backfill stream, err := s.broadcaster.Subscribe(ctx) if err != nil { return err } defer s.broadcaster.Unsubscribe(stream) - if !req.SendInitialEvents && req.Since == 0 { - // This is a temporary hack only relevant for tests to ensure that the first events are sent. - // This is required because the SQL backend polls the database every 100ms. - // TODO: Implement a getLatestResourceVersion method in the backend. - time.Sleep(10 * time.Millisecond) - } - - mostRecentRV := s.mostRecentRV.Load() // get the latest resource version - var initialEventsRV int64 // resource version coming from the initial events + since := req.Since if req.SendInitialEvents { - // Backfill the stream by adding every existing entities. - initialEventsRV, err = s.backend.ListIterator(ctx, &ListRequest{Options: req.Options}, func(iter ListIterator) error { - for iter.Next() { - if err := iter.Error(); err != nil { - return err - } - if err := srv.Send(&WatchEvent{ - Type: WatchEvent_ADDED, - Resource: &WatchEvent_Resource{ - Value: iter.Value(), - Version: iter.ResourceVersion(), - }, - }); err != nil { - return err - } - } - return nil - }) - if err != nil { - return err - } - } - if req.SendInitialEvents && req.AllowWatchBookmarks { - if err := srv.Send(&WatchEvent{ - Type: WatchEvent_BOOKMARK, - Resource: &WatchEvent_Resource{ - Version: initialEventsRV, - }, - }); err != nil { - return err + fmt.Printf("TODO... query\n") + // All initial events are CREATE + + if req.AllowWatchBookmarks { + fmt.Printf("TODO... send bookmark\n") } } - var since int64 // resource version to start watching from - switch { - case req.SendInitialEvents: - since = initialEventsRV - case req.Since == 0: - since = mostRecentRV - default: - since = req.Since - } for { select { case <-ctx.Done(): @@ -644,39 +596,23 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { s.log.Debug("watch events closed") return nil } - s.log.Debug("Server Broadcasting", "type", event.Type, "rv", event.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "resource", event.Key.Resource, "name", event.Key.Name) + if event.ResourceVersion > since && matchesQueryKey(req.Options.Key, event.Key) { - value := event.Value - // remove the delete marker stored in the value for deleted objects - if event.Type == WatchEvent_DELETED { - value = []byte{} - } - resp := &WatchEvent{ + // Currently sending *every* event + // if req.Options.Labels != nil { + // // match *either* the old or new object + // } + // TODO: return values that match either the old or the new + + if err := srv.Send(&WatchEvent{ Timestamp: event.Timestamp, Type: event.Type, Resource: &WatchEvent_Resource{ - Value: value, + Value: event.Value, Version: event.ResourceVersion, }, - } - if event.PreviousRV > 0 { - prevObj, err := s.Read(ctx, &ReadRequest{Key: event.Key, ResourceVersion: event.PreviousRV}) - if err != nil { - // This scenario should never happen, but if it does, we should log it and continue - // sending the event without the previous object. The client will decide what to do. - s.log.Error("error reading previous object", "key", event.Key, "resource_version", event.PreviousRV, "error", prevObj.Error) - } else { - if prevObj.ResourceVersion != event.PreviousRV { - s.log.Error("resource version mismatch", "key", event.Key, "resource_version", event.PreviousRV, "actual", prevObj.ResourceVersion) - return fmt.Errorf("resource version mismatch") - } - resp.Previous = &WatchEvent_Resource{ - Value: prevObj.Value, - Version: prevObj.ResourceVersion, - } - } - } - if err := srv.Send(resp); err != nil { + // TODO... previous??? + }); err != nil { return err } } diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 913d105babb..ae70e141219 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -22,7 +22,6 @@ import ( ) const trace_prefix = "sql.resource." -const defaultPollingInterval = 100 * time.Millisecond type Backend interface { resource.StorageBackend @@ -31,9 +30,8 @@ type Backend interface { } type BackendOptions struct { - DBProvider db.DBProvider - Tracer trace.Tracer - PollingInterval time.Duration + DBProvider db.DBProvider + Tracer trace.Tracer } func NewBackend(opts BackendOptions) (Backend, error) { @@ -45,17 +43,12 @@ func NewBackend(opts BackendOptions) (Backend, error) { } ctx, cancel := context.WithCancel(context.Background()) - pollingInterval := opts.PollingInterval - if pollingInterval == 0 { - pollingInterval = defaultPollingInterval - } return &backend{ - done: ctx.Done(), - cancel: cancel, - log: log.New("sql-resource-server"), - tracer: opts.Tracer, - dbProvider: opts.DBProvider, - pollingInterval: pollingInterval, + done: ctx.Done(), + cancel: cancel, + log: log.New("sql-resource-server"), + tracer: opts.Tracer, + dbProvider: opts.DBProvider, }, nil } @@ -77,7 +70,6 @@ type backend struct { // watch streaming //stream chan *resource.WatchEvent - pollingInterval time.Duration } func (b *backend) Init(ctx context.Context) error { @@ -188,6 +180,7 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, return nil }) + return newVersion, err } @@ -519,7 +512,8 @@ func (b *backend) WatchWriteEvents(ctx context.Context) (<-chan *resource.Writte } func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan<- *resource.WrittenEvent) { - t := time.NewTicker(b.pollingInterval) + interval := 100 * time.Millisecond // TODO make this configurable + t := time.NewTicker(interval) defer close(stream) defer t.Stop() @@ -532,7 +526,7 @@ func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan grv, err := b.listLatestRVs(ctx) if err != nil { b.log.Error("get the latest resource version", "err", err) - t.Reset(b.pollingInterval) + t.Reset(interval) continue } for group, items := range grv { @@ -549,7 +543,7 @@ func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan next, err := b.poll(ctx, group, resource, since[group][resource], stream) if err != nil { b.log.Error("polling for resource", "err", err) - t.Reset(b.pollingInterval) + t.Reset(interval) continue } if next > since[group][resource] { @@ -558,7 +552,7 @@ func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan } } - t.Reset(b.pollingInterval) + t.Reset(interval) } } } @@ -642,8 +636,7 @@ func (b *backend) poll(ctx context.Context, grp string, res string, since int64, Resource: rec.Key.Resource, Name: rec.Key.Name, }, - Type: resource.WatchEvent_Type(rec.Action), - PreviousRV: rec.PreviousRV, + Type: resource.WatchEvent_Type(rec.Action), }, ResourceVersion: rec.ResourceVersion, // Timestamp: , // TODO: add timestamp @@ -670,16 +663,15 @@ func resourceVersionAtomicInc(ctx context.Context, x db.ContextExecer, d sqltemp if errors.Is(err, sql.ErrNoRows) { // if there wasn't a row associated with the given resource, we create one with - // version 2 to match the etcd behavior. + // version 1 if _, err = dbutil.Exec(ctx, x, sqlResourceVersionInsert, sqlResourceVersionRequest{ - SQLTemplate: sqltemplate.New(d), - Group: key.Group, - Resource: key.Resource, - resourceVersion: &resourceVersion{1}, + SQLTemplate: sqltemplate.New(d), + Group: key.Group, + Resource: key.Resource, }); err != nil { return 0, fmt.Errorf("insert into resource_version: %w", err) } - return 2, nil + return 1, nil } if err != nil { diff --git a/pkg/storage/unified/sql/backend_test.go b/pkg/storage/unified/sql/backend_test.go index 33b7bab7d6a..b24024aef90 100644 --- a/pkg/storage/unified/sql/backend_test.go +++ b/pkg/storage/unified/sql/backend_test.go @@ -227,7 +227,7 @@ func TestResourceVersionAtomicInc(t *testing.T) { v, err := resourceVersionAtomicInc(ctx, b.DB, dialect, resKey) require.NoError(t, err) - require.Equal(t, int64(2), v) + require.Equal(t, int64(1), v) }) t.Run("happy path - update existing row", func(t *testing.T) { @@ -304,7 +304,7 @@ func TestBackend_create(t *testing.T) { v, err := b.create(ctx, event) require.NoError(t, err) - require.Equal(t, int64(2), v) + require.Equal(t, int64(1), v) }) t.Run("error inserting into resource", func(t *testing.T) { @@ -409,7 +409,7 @@ func TestBackend_update(t *testing.T) { v, err := b.update(ctx, event) require.NoError(t, err) - require.Equal(t, int64(2), v) + require.Equal(t, int64(1), v) }) t.Run("error in first update to resource", func(t *testing.T) { @@ -513,7 +513,7 @@ func TestBackend_delete(t *testing.T) { v, err := b.delete(ctx, event) require.NoError(t, err) - require.Equal(t, int64(2), v) + require.Equal(t, int64(1), v) }) t.Run("error deleting resource", func(t *testing.T) { diff --git a/pkg/storage/unified/sql/data/resource_history_insert.sql b/pkg/storage/unified/sql/data/resource_history_insert.sql index 2669ef82447..018b65739d8 100644 --- a/pkg/storage/unified/sql/data/resource_history_insert.sql +++ b/pkg/storage/unified/sql/data/resource_history_insert.sql @@ -6,7 +6,6 @@ INSERT INTO {{ .Ident "resource_history" }} {{ .Ident "namespace" }}, {{ .Ident "name" }}, - {{ .Ident "previous_resource_version"}}, {{ .Ident "value" }}, {{ .Ident "action" }} ) @@ -18,7 +17,6 @@ INSERT INTO {{ .Ident "resource_history" }} {{ .Arg .WriteEvent.Key.Namespace }}, {{ .Arg .WriteEvent.Key.Name }}, - {{ .Arg .WriteEvent.PreviousRV }}, {{ .Arg .WriteEvent.Value }}, {{ .Arg .WriteEvent.Type }} ) diff --git a/pkg/storage/unified/sql/data/resource_history_poll.sql b/pkg/storage/unified/sql/data/resource_history_poll.sql index 8e4a7374fdb..bebfab9286d 100644 --- a/pkg/storage/unified/sql/data/resource_history_poll.sql +++ b/pkg/storage/unified/sql/data/resource_history_poll.sql @@ -5,8 +5,7 @@ SELECT {{ .Ident "resource" | .Into .Response.Key.Resource }}, {{ .Ident "name" | .Into .Response.Key.Name }}, {{ .Ident "value" | .Into .Response.Value }}, - {{ .Ident "action" | .Into .Response.Action }}, - {{ .Ident "previous_resource_version" | .Into .Response.PreviousRV }} + {{ .Ident "action" | .Into .Response.Action }} FROM {{ .Ident "resource_history" }} WHERE 1 = 1 diff --git a/pkg/storage/unified/sql/data/resource_insert.sql b/pkg/storage/unified/sql/data/resource_insert.sql index ccaca2f12f7..e127901ae50 100644 --- a/pkg/storage/unified/sql/data/resource_insert.sql +++ b/pkg/storage/unified/sql/data/resource_insert.sql @@ -7,7 +7,6 @@ INSERT INTO {{ .Ident "resource" }} {{ .Ident "namespace" }}, {{ .Ident "name" }}, - {{ .Ident "previous_resource_version" }}, {{ .Ident "value" }}, {{ .Ident "action" }} ) @@ -18,7 +17,6 @@ INSERT INTO {{ .Ident "resource" }} {{ .Arg .WriteEvent.Key.Namespace }}, {{ .Arg .WriteEvent.Key.Name }}, - {{ .Arg .WriteEvent.PreviousRV }}, {{ .Arg .WriteEvent.Value }}, {{ .Arg .WriteEvent.Type }} ) diff --git a/pkg/storage/unified/sql/data/resource_version_insert.sql b/pkg/storage/unified/sql/data/resource_version_insert.sql index 6c3aab0dcd4..6c2342905da 100644 --- a/pkg/storage/unified/sql/data/resource_version_insert.sql +++ b/pkg/storage/unified/sql/data/resource_version_insert.sql @@ -8,6 +8,6 @@ INSERT INTO {{ .Ident "resource_version" }} VALUES ( {{ .Arg .Group }}, {{ .Arg .Resource }}, - 2 + 1 ) ; diff --git a/pkg/storage/unified/sql/db/migrations/resource_mig.go b/pkg/storage/unified/sql/db/migrations/resource_mig.go index 38824569a05..adfd75a0b73 100644 --- a/pkg/storage/unified/sql/db/migrations/resource_mig.go +++ b/pkg/storage/unified/sql/db/migrations/resource_mig.go @@ -10,7 +10,8 @@ func initResourceTables(mg *migrator.Migrator) string { marker := "Initialize resource tables" mg.AddMigration(marker, &migrator.RawSQLMigration{}) - resource_table := migrator.Table{ + tables := []migrator.Table{} + tables = append(tables, migrator.Table{ Name: "resource", Columns: []*migrator.Column{ // primary identifier @@ -32,8 +33,9 @@ func initResourceTables(mg *migrator.Migrator) string { Indices: []*migrator.Index{ {Cols: []string{"namespace", "group", "resource", "name"}, Type: migrator.UniqueIndex}, }, - } - resource_history_table := migrator.Table{ + }) + + tables = append(tables, migrator.Table{ Name: "resource_history", Columns: []*migrator.Column{ // primary identifier @@ -60,9 +62,7 @@ func initResourceTables(mg *migrator.Migrator) string { // index to support watch poller {Cols: []string{"resource_version"}, Type: migrator.IndexType}, }, - } - - tables := []migrator.Table{resource_table, resource_history_table} + }) // tables = append(tables, migrator.Table{ // Name: "resource_label_set", @@ -97,13 +97,5 @@ func initResourceTables(mg *migrator.Migrator) string { } } - mg.AddMigration("Add column previous_resource_version in resource_history", migrator.NewAddColumnMigration(resource_history_table, &migrator.Column{ - Name: "previous_resource_version", Type: migrator.DB_BigInt, Nullable: false, - })) - - mg.AddMigration("Add column previous_resource_version in resource", migrator.NewAddColumnMigration(resource_table, &migrator.Column{ - Name: "previous_resource_version", Type: migrator.DB_BigInt, Nullable: false, - })) - return marker } diff --git a/pkg/storage/unified/sql/queries.go b/pkg/storage/unified/sql/queries.go index 11882f17cb2..893169c3f3a 100644 --- a/pkg/storage/unified/sql/queries.go +++ b/pkg/storage/unified/sql/queries.go @@ -70,7 +70,6 @@ func (r sqlResourceRequest) Validate() error { type historyPollResponse struct { Key resource.ResourceKey ResourceVersion int64 - PreviousRV int64 Value []byte Action int } @@ -102,7 +101,6 @@ func (r *sqlResourceHistoryPollRequest) Results() (*historyPollResponse, error) Name: r.Response.Key.Name, }, ResourceVersion: r.Response.ResourceVersion, - PreviousRV: r.Response.PreviousRV, Value: r.Response.Value, Action: r.Response.Action, }, nil diff --git a/pkg/storage/unified/sql/queries_test.go b/pkg/storage/unified/sql/queries_test.go index df7ed9167f7..b5ac7f57217 100644 --- a/pkg/storage/unified/sql/queries_test.go +++ b/pkg/storage/unified/sql/queries_test.go @@ -104,18 +104,6 @@ func TestUnifiedStorageQueries(t *testing.T) { }, }, }, - sqlResourceHistoryPoll: { - { - Name: "single path", - Data: &sqlResourceHistoryPollRequest{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Resource: "res", - Group: "group", - SinceResourceVersion: 1234, - Response: new(historyPollResponse), - }, - }, - }, sqlResourceUpdateRV: { { @@ -155,8 +143,7 @@ func TestUnifiedStorageQueries(t *testing.T) { Data: &sqlResourceRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), WriteEvent: resource.WriteEvent{ - Key: &resource.ResourceKey{}, - PreviousRV: 1234, + Key: &resource.ResourceKey{}, }, }, }, diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql index d76132ae625..27f5000fc9f 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql @@ -5,7 +5,6 @@ INSERT INTO `resource_history` `resource`, `namespace`, `name`, - `previous_resource_version`, `value`, `action` ) @@ -15,7 +14,6 @@ INSERT INTO `resource_history` '', '', '', - 1234, '[]', 'UNKNOWN' ) diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_poll-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_poll-single path.sql deleted file mode 100755 index a29cf35d4da..00000000000 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_poll-single path.sql +++ /dev/null @@ -1,16 +0,0 @@ -SELECT - `resource_version`, - `namespace`, - `group`, - `resource`, - `name`, - `value`, - `action`, - `previous_resource_version` - FROM `resource_history` - WHERE 1 = 1 - AND `group` = 'group' - AND `resource` = 'res' - AND `resource_version` > 1234 - ORDER BY `resource_version` ASC -; diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_insert-simple.sql b/pkg/storage/unified/sql/testdata/mysql--resource_insert-simple.sql index 5bf3424e55b..0897963b19c 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_insert-simple.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_insert-simple.sql @@ -5,7 +5,6 @@ INSERT INTO `resource` `resource`, `namespace`, `name`, - `previous_resource_version`, `value`, `action` ) @@ -15,7 +14,6 @@ INSERT INTO `resource` 'rr', 'nn', 'name', - 123, '[]', 'ADDED' ) diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql index f99b2b00148..350f77472ab 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO `resource_version` VALUES ( '', '', - 2 + 1 ) ; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql index a15a8db4b1e..643741bc3b1 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql @@ -5,7 +5,6 @@ INSERT INTO "resource_history" "resource", "namespace", "name", - "previous_resource_version", "value", "action" ) @@ -15,7 +14,6 @@ INSERT INTO "resource_history" '', '', '', - 1234, '[]', 'UNKNOWN' ) diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_poll-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_poll-single path.sql deleted file mode 100755 index d038317381a..00000000000 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_poll-single path.sql +++ /dev/null @@ -1,16 +0,0 @@ -SELECT - "resource_version", - "namespace", - "group", - "resource", - "name", - "value", - "action", - "previous_resource_version" - FROM "resource_history" - WHERE 1 = 1 - AND "group" = 'group' - AND "resource" = 'res' - AND "resource_version" > 1234 - ORDER BY "resource_version" ASC -; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_insert-simple.sql b/pkg/storage/unified/sql/testdata/postgres--resource_insert-simple.sql index fc2d22be1c4..9150eb59fef 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_insert-simple.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_insert-simple.sql @@ -5,7 +5,6 @@ INSERT INTO "resource" "resource", "namespace", "name", - "previous_resource_version", "value", "action" ) @@ -15,7 +14,6 @@ INSERT INTO "resource" 'rr', 'nn', 'name', - 123, '[]', 'ADDED' ) diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql index 14b25955585..99003d5fefe 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO "resource_version" VALUES ( '', '', - 2 + 1 ) ; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql index a15a8db4b1e..643741bc3b1 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql @@ -5,7 +5,6 @@ INSERT INTO "resource_history" "resource", "namespace", "name", - "previous_resource_version", "value", "action" ) @@ -15,7 +14,6 @@ INSERT INTO "resource_history" '', '', '', - 1234, '[]', 'UNKNOWN' ) diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_poll-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_poll-single path.sql deleted file mode 100755 index d038317381a..00000000000 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_poll-single path.sql +++ /dev/null @@ -1,16 +0,0 @@ -SELECT - "resource_version", - "namespace", - "group", - "resource", - "name", - "value", - "action", - "previous_resource_version" - FROM "resource_history" - WHERE 1 = 1 - AND "group" = 'group' - AND "resource" = 'res' - AND "resource_version" > 1234 - ORDER BY "resource_version" ASC -; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_insert-simple.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_insert-simple.sql index fc2d22be1c4..9150eb59fef 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_insert-simple.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_insert-simple.sql @@ -5,7 +5,6 @@ INSERT INTO "resource" "resource", "namespace", "name", - "previous_resource_version", "value", "action" ) @@ -15,7 +14,6 @@ INSERT INTO "resource" 'rr', 'nn', 'name', - 123, '[]', 'ADDED' ) diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql index 14b25955585..99003d5fefe 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO "resource_version" VALUES ( '', '', - 2 + 1 ) ; From 2d89a277412905fab6db595dd3058c409e52d9e3 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 1 Oct 2024 15:03:34 -0400 Subject: [PATCH 130/174] Update CHANGELOG.md (#94103) Updates Changelog to include CVE to 10.3.11 and 10.4.10 versions --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50b9951ebad..f8eb7845ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - **AzureMonitor:** Deduplicate resource picker rows [#93702](https://github.com/grafana/grafana/pull/93702), [@aangelisc](https://github.com/aangelisc) - **Correlations:** Limit access to correlations page to users who can access Explore [#93673](https://github.com/grafana/grafana/pull/93673), [@ifrost](https://github.com/ifrost) +- **Alerting:** Fixed CVE-2024-8118. @@ -43,6 +44,7 @@ ### Bug fixes - **Correlations:** Limit access to correlations page to users who can access Explore [#93672](https://github.com/grafana/grafana/pull/93672), [@ifrost](https://github.com/ifrost) +- **Alerting:** Fixed CVE-2024-8118. From 518afa5a24b1c6e344365b2a2ff32575125ff716 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:26:27 -0400 Subject: [PATCH 131/174] Release: update changelog for 11.1.7 (#94102) * Update changelog * Add CVE --------- Co-authored-by: github-actions[bot] Co-authored-by: Yuri Tseretyan --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8eb7845ca4..69582892b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ + + +# 11.1.7 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93355](https://github.com/grafana/grafana/pull/93355), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) + +### Bug fixes + +- **Alerting:** Fix preview of silences when label name contains spaces [#93050](https://github.com/grafana/grafana/pull/93050), [@tomratcliffe](https://github.com/tomratcliffe) +- **Alerting:** Make query wrapper match up datasource UIDs if necessary [#93115](https://github.com/grafana/grafana/pull/93115), [@tomratcliffe](https://github.com/tomratcliffe) +- **AzureMonitor:** Deduplicate resource picker rows [#93704](https://github.com/grafana/grafana/pull/93704), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Improve resource picker efficiency [#93439](https://github.com/grafana/grafana/pull/93439), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Remove Basic Logs retention warning [#93122](https://github.com/grafana/grafana/pull/93122), [@aangelisc](https://github.com/aangelisc) +- **Correlations:** Limit access to correlations page to users who can access Explore [#93675](https://github.com/grafana/grafana/pull/93675), [@ifrost](https://github.com/ifrost) +- **Plugins:** Avoid returning 404 for `AutoEnabled` apps [#93487](https://github.com/grafana/grafana/pull/93487), [@wbrowne](https://github.com/wbrowne) +- **Alerting:** Fixed CVE-2024-8118. + + # 11.0.6 (2024-10-01) From 26c3ed89a348b95f791f86672d1c2c796405a6e8 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:03:38 -0500 Subject: [PATCH 132/174] CI: upgrade grabpl v3.0.53 (#94112) * upgrade grabpl to v3.0.53 * upgrade grabpl to v3.0.53 --- .drone.yml | 22 +++++++++++----------- scripts/drone/variables.star | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.drone.yml b/.drone.yml index 03989a5e0e4..207e0d20ecb 100644 --- a/.drone.yml +++ b/.drone.yml @@ -547,7 +547,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1003,7 +1003,7 @@ steps: name: clone-enterprise - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1972,7 +1972,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2525,7 +2525,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2730,7 +2730,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -3157,7 +3157,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3402,7 +3402,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3533,7 +3533,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4557,7 +4557,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -5359,7 +5359,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -6151,6 +6151,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: e35ebf7a31abb198c576ca8f623b63fb2bd9d84de2a6111e28b2415587d5377b +hmac: 766cd43d479f82bdb5bbaa3b48ed87ad13ea71d3418deb5d0c89ec7b77ae0475 ... diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index 9cc69bb2829..0897f328cce 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -2,7 +2,7 @@ global variables """ -grabpl_version = "v3.0.50" +grabpl_version = "v3.0.53" golang_version = "1.23.1" # nodejs_version should match what's in ".nvmrc", but without the v prefix. From 28d9cc73103700eeea6e7cc7bf07d6708a97cf5c Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Tue, 1 Oct 2024 15:09:39 -0500 Subject: [PATCH 133/174] Explore metrics: Fix bug that turns off otel experience when selecting otel variables (#94106) fix bug that turns of otel experience when selecting otel variables --- public/app/features/trails/DataTrail.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index fd689bc5fb4..dfc4a52a88e 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -381,7 +381,7 @@ export class DataTrail extends SceneObjectBase { } } /** - * This function is used to update state and otel variables + * This function is used to update state and otel variables. * * 1. Set the otelResources adhoc tagKey and tagValues filter functions 2. Get the otel join query for state and variable @@ -392,6 +392,11 @@ export class DataTrail extends SceneObjectBase { - has otel resources flag - isStandardOtel flag (for enabliing the otel experience toggle) - and useOtelExperience + * + * This function is called on start and when variables change. + * On start will provide the deploymentEnvironments and hasOtelResources parameters. + * In the variable change case, we will not provide these parameters. It is assumed that the + * data source has been checked for otel resources and standardization and the otel variables are enabled at this point. * @param datasourceUid * @param timeRange * @param otelDepEnvVariable @@ -504,7 +509,6 @@ export class DataTrail extends SceneObjectBase { this.setState({ otelTargets, otelJoinQuery, - useOtelExperience: false, }); } } From f55f7f2634c5b12cd32c80f4c9303cc9c0831866 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 2 Oct 2024 09:44:18 +0300 Subject: [PATCH 134/174] Routing: Replace Redirect component with Navigate (#94072) * Routing: Replace Redirect with Navigate * Use replace state * Update routes.tsx * Fix test --- public/app/AppWrapper.tsx | 6 ++-- .../core/components/FormPrompt/FormPrompt.tsx | 5 +-- .../features/alerting/unified/MuteTimings.tsx | 5 +-- .../unified/RedirectToRuleViewer.test.tsx | 6 ++-- .../alerting/unified/RedirectToRuleViewer.tsx | 6 ++-- .../unified/components/rules/CloneRule.tsx | 5 +-- .../app/features/connections/Connections.tsx | 17 +++++---- public/app/features/trails/DataTrailsHome.tsx | 4 +-- public/app/routes/routes.tsx | 36 +++++++++++-------- 9 files changed, 52 insertions(+), 38 deletions(-) diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index 768a0ff0dff..91a9e5efd4b 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -1,8 +1,8 @@ import { Action, KBarProvider } from 'kbar'; import { Component, ComponentType } from 'react'; import { Provider } from 'react-redux'; -import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'; -import { CompatRoute } from 'react-router-dom-v5-compat'; +import { Switch, RouteComponentProps } from 'react-router-dom'; +import { CompatRoute, Navigate } from 'react-router-dom-v5-compat'; import { config, navigationLogger, reportInteraction } from '@grafana/runtime'; import { ErrorBoundaryAlert, GlobalStyles, PortalContainer } from '@grafana/ui'; @@ -67,7 +67,7 @@ export class AppWrapper extends Component { // TODO[Router]: test this logic if (roles?.length) { if (!roles.some((r: string) => contextSrv.hasRole(r))) { - return ; + return ; } } diff --git a/public/app/core/components/FormPrompt/FormPrompt.tsx b/public/app/core/components/FormPrompt/FormPrompt.tsx index dac60471129..7e5edbc4cb7 100644 --- a/public/app/core/components/FormPrompt/FormPrompt.tsx +++ b/public/app/core/components/FormPrompt/FormPrompt.tsx @@ -1,7 +1,8 @@ import { css } from '@emotion/css'; import history from 'history'; import { useEffect, useState } from 'react'; -import { Prompt, Redirect } from 'react-router-dom'; +import { Prompt } from 'react-router-dom'; +import { Navigate } from 'react-router-dom-v5-compat'; import { Button, Modal } from '@grafana/ui'; @@ -80,7 +81,7 @@ export const FormPrompt = ({ confirmRedirect, onDiscard, onLocationChange }: Pro return ( <> - {blockedLocation && changesDiscarded && } + {blockedLocation && changesDiscarded && } ); diff --git a/public/app/features/alerting/unified/MuteTimings.tsx b/public/app/features/alerting/unified/MuteTimings.tsx index 08e10eb1b66..10deee135e4 100644 --- a/public/app/features/alerting/unified/MuteTimings.tsx +++ b/public/app/features/alerting/unified/MuteTimings.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; -import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; +import { Route, Switch, useRouteMatch } from 'react-router-dom'; +import { Navigate } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; import { useGetMuteTiming } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings'; @@ -23,7 +24,7 @@ const EditTimingRoute = () => { }); if (!name) { - return ; + return ; } return ( diff --git a/public/app/features/alerting/unified/RedirectToRuleViewer.test.tsx b/public/app/features/alerting/unified/RedirectToRuleViewer.test.tsx index 3372d095755..fa32c6e3bf9 100644 --- a/public/app/features/alerting/unified/RedirectToRuleViewer.test.tsx +++ b/public/app/features/alerting/unified/RedirectToRuleViewer.test.tsx @@ -12,9 +12,9 @@ import { getRulesSourceByName } from './utils/datasource'; jest.mock('./hooks/useCombinedRule'); jest.mock('./utils/datasource'); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - Redirect: jest.fn(({}) => `Redirected`), +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + Navigate: jest.fn(({}) => `Redirected`), })); jest.mock('react-use', () => ({ diff --git a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx index 9462ea4686f..e5375ad9133 100644 --- a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx +++ b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; +import { Navigate } from 'react-router-dom-v5-compat'; import { useLocation } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; @@ -53,7 +53,7 @@ export function RedirectToRuleViewer(): JSX.Element | null { } = useCloudCombinedRulesMatching(name, sourceName, { namespace, groupName: group }); if (!name || !sourceName) { - return ; + return ; } if (error) { @@ -95,7 +95,7 @@ export function RedirectToRuleViewer(): JSX.Element | null { if (rules.length === 1) { const [rule] = rules; const to = createViewLink(rulesSource, rule, '/alerting/list').replace(subUrl, ''); - return ; + return ; } if (rules.length === 0) { diff --git a/public/app/features/alerting/unified/components/rules/CloneRule.tsx b/public/app/features/alerting/unified/components/rules/CloneRule.tsx index 646aace7945..cb0d0cdbc80 100644 --- a/public/app/features/alerting/unified/components/rules/CloneRule.tsx +++ b/public/app/features/alerting/unified/components/rules/CloneRule.tsx @@ -1,5 +1,6 @@ import { forwardRef, useState } from 'react'; -import { Redirect, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import { Navigate } from 'react-router-dom-v5-compat'; import { Button, ConfirmModal } from '@grafana/ui'; import { RuleIdentifier } from 'app/types/unified-alerting'; @@ -33,7 +34,7 @@ export function RedirectToCloneRule({ returnTo: redirectTo ? returnTo : '', }); - return ; + return ; } return ( diff --git a/public/app/features/connections/Connections.tsx b/public/app/features/connections/Connections.tsx index 600dbb958c0..fd0a97b1125 100644 --- a/public/app/features/connections/Connections.tsx +++ b/public/app/features/connections/Connections.tsx @@ -1,4 +1,5 @@ -import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; +import { Route, Switch, useLocation } from 'react-router-dom'; +import { Navigate } from 'react-router-dom-v5-compat'; import { DataSourcesRoutesContext } from 'app/features/datasources/state'; import { StoreState, useSelector } from 'app/types'; @@ -16,7 +17,8 @@ import { function RedirectToAddNewConnection() { const { search } = useLocation(); return ( - {/* Redirect to "Add new connection" by default */} - } /> + } /> @@ -54,11 +56,14 @@ export default function Connections() { {/* Redirect from earlier routes to updated routes */} - - + } + /> + } /> {/* Not found */} - } /> + } /> ); diff --git a/public/app/features/trails/DataTrailsHome.tsx b/public/app/features/trails/DataTrailsHome.tsx index 3c77390f6a7..59f11dea664 100644 --- a/public/app/features/trails/DataTrailsHome.tsx +++ b/public/app/features/trails/DataTrailsHome.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { useState } from 'react'; -import { Redirect } from 'react-router-dom'; +import { Navigate } from 'react-router-dom-v5-compat'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; @@ -57,7 +57,7 @@ export class DataTrailsHome extends SceneObjectBase { // If there are no recent trails, don't show home page and create a new trail if (!getTrailStore().recent.length) { const trail = newMetricsTrail(getDatasourceForNewTrail()); - return ; + return ; } return ( diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 261b3cf5726..b496c73550c 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -1,4 +1,4 @@ -import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { Navigate, useParams } from 'react-router-dom-v5-compat'; import { isTruthy } from '@grafana/data'; import { NavLandingPage } from 'app/core/components/NavLandingPage/NavLandingPage'; @@ -107,23 +107,19 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: DATASOURCES_ROUTES.List, - component: () => , + component: () => , }, { path: DATASOURCES_ROUTES.Edit, - component: (props: RouteComponentProps<{ uid: string }>) => ( - - ), + component: DataSourceEditRoute, }, { path: DATASOURCES_ROUTES.Dashboards, - component: (props: RouteComponentProps<{ uid: string }>) => ( - - ), + component: DataSourceDashboardRoute, }, { path: DATASOURCES_ROUTES.New, - component: () => , + component: () => , }, { path: '/datasources/correlations', @@ -219,7 +215,7 @@ export function getAppRoutes(): RouteDescriptor[] { { path: '/org/users', // Org users page has been combined with admin users - component: () => , + component: () => , }, { path: '/org/users/invite', @@ -292,7 +288,7 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "AdminAuthentication" */ '../features/auth-config/AuthProvidersListPage') ) - : () => , + : () => , }, { path: '/admin/authentication/ldap', @@ -309,7 +305,7 @@ export function getAppRoutes(): RouteDescriptor[] { ? SafeDynamicImport( () => import(/* webpackChunkName: "AdminAuthentication" */ '../features/auth-config/ProviderConfigPage') ) - : () => , + : () => , }, { path: '/admin/settings', @@ -357,7 +353,7 @@ export function getAppRoutes(): RouteDescriptor[] { ? SafeDynamicImport( () => import(/* webpackChunkName: "AdminFeatureTogglesPage" */ 'app/features/admin/AdminFeatureTogglesPage') ) - : () => , + : () => , }, { path: '/admin/storage/:path*', @@ -398,7 +394,7 @@ export function getAppRoutes(): RouteDescriptor[] { { path: '/verify', component: !config.verifyEmailEnabled - ? () => + ? () => : SafeDynamicImport( () => import(/* webpackChunkName "VerifyEmailPage"*/ 'app/core/components/Signup/VerifyEmailPage') ), @@ -408,7 +404,7 @@ export function getAppRoutes(): RouteDescriptor[] { { path: '/signup', component: config.disableUserSignUp - ? () => + ? () => : SafeDynamicImport(() => import(/* webpackChunkName "SignupPage"*/ 'app/core/components/Signup/SignupPage')), pageClass: 'login-page', chromeless: true, @@ -556,3 +552,13 @@ export function getSupportBundleRoutes(cfg = config): RouteDescriptor[] { }, ]; } + +function DataSourceDashboardRoute() { + const { uid = '' } = useParams(); + return ; +} + +function DataSourceEditRoute() { + const { uid = '' } = useParams(); + return ; +} From dd8c50ec1247d2500443f865ecaed6f2582dcc58 Mon Sep 17 00:00:00 2001 From: David Garcia <87497958+Dgarc359@users.noreply.github.com> Date: Wed, 2 Oct 2024 03:34:23 -0400 Subject: [PATCH 135/174] FIX: typo in generic oauth org mapping json (#94117) --- .../configure-authentication/generic-oauth/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 91b0e40eec5..fa56bac67b5 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -313,7 +313,7 @@ Payload: "roles": [ "org_foo", "org_bar", - "another_org' + "another_org" ], ... }, From 3f6a64cc57b0a38b2c925f3d4f422c1ff42af5b4 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 2 Oct 2024 10:02:28 +0200 Subject: [PATCH 136/174] Navigation: Don't show "add new connection" if user has no permissions (#94058) Navigation: Don't show "add new connection" if user does not have permissions --- pkg/services/datasources/accesscontrol.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/services/datasources/accesscontrol.go b/pkg/services/datasources/accesscontrol.go index 03d88e8e3bd..f6e5b9c8a0c 100644 --- a/pkg/services/datasources/accesscontrol.go +++ b/pkg/services/datasources/accesscontrol.go @@ -25,7 +25,6 @@ var ( var ( // ConfigurationPageAccess is used to protect the "Configure > Data sources" tab access ConfigurationPageAccess = accesscontrol.EvalAny( - accesscontrol.EvalPermission(accesscontrol.ActionDatasourcesExplore), accesscontrol.EvalPermission(ActionCreate), accesscontrol.EvalAll( accesscontrol.EvalPermission(ActionRead), From 763163603c0541505d564caa852ba047dc738fe5 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:42:23 +0200 Subject: [PATCH 137/174] SSO LDAP: Bug-bashing follow-up changes (#94093) * fix html encoding rendering * Redirect to providers page * Fix cert isEmpty * Rework input fields into multiselect * add disable button * Rework MultiSelect design * Remove prompt modal --- public/app/features/admin/ldap/LdapDrawer.tsx | 94 +++++++++++-------- .../features/admin/ldap/LdapSettingsPage.tsx | 69 +++++++------- public/locales/en-US/grafana.json | 18 ++-- public/locales/pseudo-LOCALE/grafana.json | 18 ++-- 4 files changed, 102 insertions(+), 97 deletions(-) diff --git a/public/app/features/admin/ldap/LdapDrawer.tsx b/public/app/features/admin/ldap/LdapDrawer.tsx index caa5edefa1f..a42cb0c08a0 100644 --- a/public/app/features/admin/ldap/LdapDrawer.tsx +++ b/public/app/features/admin/ldap/LdapDrawer.tsx @@ -17,7 +17,6 @@ import { Select, Stack, Switch, - Text, TextLink, Tooltip, RadioButtonGroup, @@ -73,6 +72,18 @@ export const LdapDrawerComponent = ({ return value; }; + const attributesLabel = ( + + ); + const groupMappingsLabel = (