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

This commit is contained in:
Ryan McKinley
2025-07-11 09:16:19 -07:00
511 changed files with 28676 additions and 2983 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ module.exports = [
'data/',
'deployment_tools_config.json',
'devenv',
'e2e/test-plugins',
'e2e-playwright/test-plugins',
'e2e/tmp',
'packages/grafana-ui/src/components/Icon/iconBundle.ts',
'pkg',
-4
View File
@@ -2785,10 +2785,6 @@ exports[`better eslint`] = {
"public/app/features/transformers/editors/CalculateFieldTransformerEditor/WindowOptionsEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`CalculateFieldTransformerEditor\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`calculateFieldTransformRegistryItem\`)", "1"]
],
"public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
+3 -3
View File
@@ -289,7 +289,7 @@ steps:
- commands:
- npx wait-on@7.0.1 http://$HOST:$PORT
- yarn playwright install --with-deps chromium
- yarn e2e:playwright
- yarn e2e:playwright --grep @plugins
depends_on:
- grafana-server
- build-test-plugins
@@ -761,7 +761,7 @@ steps:
- commands:
- npx wait-on@7.0.1 http://$HOST:$PORT
- yarn playwright install --with-deps chromium
- yarn e2e:playwright
- yarn e2e:playwright --grep @plugins
depends_on:
- grafana-server
- build-test-plugins
@@ -2986,6 +2986,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 1198b1489e48a9ced211633a0325d112814553246847fc7320fb5ac2bcb32b7d
hmac: d20f1d6e2e8347701f82114ad352f53db57dc95b5b3831941fa93d063a92b9d8
...
+13 -2
View File
@@ -80,6 +80,7 @@
/apps/dashboard/ @grafana/grafana-app-platform-squad @grafana/dashboards-squad
/apps/folder/ @grafana/grafana-app-platform-squad
/apps/playlist/ @grafana/grafana-app-platform-squad
/apps/secret/ @grafana/grafana-operator-experience-squad
/apps/investigations/ @fcjack @matryer @svennergr
/apps/advisor/ @grafana/plugins-platform-backend
/apps/iam/ @grafana/access-squad
@@ -400,8 +401,10 @@
/public/app/core/internationalization/ @grafana/grafana-frontend-platform
/e2e/ @grafana/grafana-frontend-platform
/e2e/cloud-plugins-suite/ @grafana/partner-datasources
/e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend
/e2e-playwright/ @grafana/grafana-frontend-platform
/e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources
/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
/e2e-playwright/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend
# Packages
/packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
@@ -477,6 +480,7 @@
/cypress.config.js @grafana/grafana-frontend-platform
/.levignore.js @grafana/plugins-platform-frontend
playwright.config.ts @grafana/plugins-platform-frontend
playwright.storybook.config.ts @grafana/grafana-frontend-platform
# public folder
/public/app/api/ @grafana/grafana-frontend-platform
@@ -747,6 +751,7 @@ embed.go @grafana/grafana-as-code
/pkg/kinds/ @grafana/grafana-as-code
/pkg/registry/ @grafana/grafana-as-code
/pkg/registry/apis/ @grafana/grafana-app-platform-squad
/pkg/registry/apis/folders @grafana/grafana-search-and-storage
/pkg/registry/apis/query @grafana/grafana-datasources-core-services
/pkg/registry/apis/secret @grafana/grafana-operator-experience-squad
/pkg/registry/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend
@@ -770,6 +775,7 @@ embed.go @grafana/grafana-as-code
/.github/pr-checks.json @tolzhabayev
/.github/pr-commands.json @tolzhabayev
/.github/renovate.json5 @grafana/frontend-ops
/.github/actions/check-jobs/action.yml @grafana/grafana-frontend-platform
/.github/actions/setup-enterprise/action.yml @grafana/grafana-backend-group
/.github/actions/setup-grafana-bench/ @Proximyst
/.github/actions/build-package @grafana/grafana-developer-enablement-squad
@@ -817,6 +823,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend
/.github/workflows/stale.yml @grafana/grafana-developer-enablement-squad
/.github/workflows/storybook-verification.yml @grafana/grafana-frontend-platform
/.github/workflows/storybook-verification-playwright.yml @grafana/grafana-frontend-platform
/.github/workflows/update-make-docs.yml @grafana/docs-tooling
/.github/workflows/scripts/kinds/verify-kinds.go @grafana/platform-monitoring
/.github/workflows/scripts/create-security-branch/create-security-branch.sh @grafana/grafana-developer-enablement-squad
@@ -876,3 +883,7 @@ embed.go @grafana/grafana-as-code
/conf/provisioning/datasources/ @grafana/plugins-platform-backend
/conf/provisioning/plugins/ @grafana/plugins-platform-backend
/conf/provisioning/sample/ @grafana/grafana-git-ui-sync-team
# Security
/relyance.yaml @grafana/security-team
/.github/workflows/relyance-scan.yml @grafana/security-team
+48
View File
@@ -0,0 +1,48 @@
name: Check jobs results
description: Checks if any jobs have failed and exits with error if failures are found. Use to check the results of matrix test runs.
inputs:
needs:
description: JSON string containing the needs context from the workflow
required: true
failure-message:
description: Custom message to display when failures are found
required: false
default: "One or more jobs have failed"
success-message:
description: Custom message to display when all jobs pass
required: false
default: "All jobs passed successfully"
outputs:
any-failed:
description: Whether any jobs failed
value: ${{ steps.check-jobs.outputs.any-failed }}
runs:
using: "composite"
steps:
- name: Check test suites
id: check-jobs
shell: bash
env:
NEEDS: ${{ inputs.needs }}
FAILURE_MSG: ${{ inputs.failure-message }}
SUCCESS_MSG: ${{ inputs.success-message }}
run: |
set -euo pipefail
# Print the needs context, debugging
echo "$NEEDS" | jq
# Extract failures
FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')"
# Check if there are any failures
if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then
echo "❌ $FAILURE_MSG"
echo "Failed suites:"
echo "$FAILURES" | jq -r 'to_entries[] | "- \(.key): \(.value)"'
echo "any-failed=true" >> "$GITHUB_OUTPUT"
exit 1
fi
echo "✅ $SUCCESS_MSG"
+1
View File
@@ -8,6 +8,7 @@ updates:
directories:
- "/"
- "/apps/playlist"
- "/apps/secret"
- "/apps/investigations"
- "/pkg/aggregator"
- "/pkg/apimachinery"
+172 -19
View File
@@ -7,12 +7,17 @@ on:
- main
- release-*.*.*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
# TODO: re-enable this before merging
# concurrency:
# group: ${{ github.workflow }}-${{ github.ref }}
# cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
permissions: {}
env:
ACTIONS_STEP_DEBUG: true
RUNNER_DEBUG: 1
jobs:
detect-changes:
name: Detect whether code changed
@@ -44,22 +49,56 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
path: ./grafana
persist-credentials: false
- uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
# TODO: add a cleanup workflow to remove the cache when the PR is closed
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy
# TODO: maybe we could just use the cache to store the build, instead of uploading as an artifact?
- uses: actions/cache@v4
id: cache
with:
key: "build-grafana-${{ runner.os }}-${{ hashFiles('yarn.lock', 'public/*', 'packages/*', 'pkg/**/*.go', '**/go.mod', '**/go.sum', '!**_test.go', '!**.test.ts', '!**.test.tsx') }}"
path: |
build-dir
# If no cache hit, build Grafana
- name: Build Grafana
if: steps.cache.outputs.cache-hit != 'true'
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with:
verb: run
args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt
- run: mv "$(cat out.txt)" grafana.tar.gz
- run: echo "artifact=grafana-e2e-${{github.run_number}}" >> "$GITHUB_OUTPUT"
args: go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}" > out.txt
- name: Cat built artifact
if: steps.cache.outputs.cache-hit != 'true'
run: cat out.txt
- name: Move built artifact
if: steps.cache.outputs.cache-hit != 'true'
run: |
mkdir -p build-dir
mv "$(cat out.txt)" build-dir/grafana.tar.gz
# If cache hit, validate the artifact is present
- name: Validate artifact
if: steps.cache.outputs.cache-hit == 'true'
run: |
if [ ! -f build-dir/grafana.tar.gz ]; then
echo "Error: build-dir/grafana.tar.gz not found in cache"
exit 1
fi
- name: Set artifact name
run: echo "artifact=grafana-server-${{github.run_number}}" >> "$GITHUB_OUTPUT"
id: artifact
- uses: actions/upload-artifact@v4
- name: Upload artifact
uses: actions/upload-artifact@v4
id: upload
with:
retention-days: 1
name: ${{ steps.artifact.outputs.artifact }}
path: grafana.tar.gz
path: build-dir/grafana.tar.gz
# TODO: we won't need this when we only have playwright
build-e2e-runner:
needs: detect-changes
if: needs.detect-changes.outputs.changed == 'true'
@@ -159,6 +198,119 @@ jobs:
path: videos
retention-days: 1
run-playwright-tests:
needs:
- build-grafana
name: Playwright E2E tests (${{ matrix.shard }}/${{ matrix.shardTotal }})
runs-on: ubuntu-latest-8-cores
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build-grafana.outputs.artifact }}
- name: Run E2E tests
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with:
verb: run
args: go run ./pkg/build/e2e-playwright --package=grafana.tar.gz --shard=${{ matrix.shard }}/${{ matrix.shardTotal }} --blob-dir=./blob-report
- uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: playwright-blob-${{ github.run_number }}-${{ matrix.shard }}
path: ./blob-report
retention-days: 1
required-playwright-tests:
needs:
- run-playwright-tests
- build-grafana
if: ${{ !cancelled() }}
name: All Playwright tests complete
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: blobs
pattern: playwright-blob-*
merge-multiple: true
- name: Check blob reports
run: |
if [ ! "$(ls -A ./blobs)" ]; then
echo "Error: No blob reports found in ./blobs directory"
echo "Did the Playwright tests run at all?"
exit 1
fi
echo "Found blob reports in ./blobs:"
ls -lah ./blobs
- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./blobs
- name: Merge into JSON Report
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: /tmp/playwright-results.json
run: npx playwright merge-reports --reporter=json ./blobs
- name: Bench report
run: |
docker run --rm \
--volume="/tmp/playwright-results.json:/home/bench/tests/playwright-results.json" \
us-docker.pkg.dev/grafanalabs-global/docker-grafana-bench-prod/grafana-bench:v0.5.1 report \
--grafana-url "http://localhost:3000" \
--grafana-version "CI- ${{ github.sha }}" \
--test-suite-name "FrontendCore" \
--report-input playwright \
--report-output log \
--log-level DEBUG \
/home/bench/tests/playwright-results.json
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: playwright-html-${{ github.run_number }}
path: playwright-report
retention-days: 7
- name: Check test suites
id: check-jobs
uses: ./.github/actions/check-jobs
continue-on-error: true # Failure will be reported on Show test results step
with:
needs: ${{ toJson(needs) }}
failure-message: "One or more E2E test suites have failed"
success-message: "All E2E test suites completed successfully"
- name: Show test results
env:
FAILED: ${{ steps.check-jobs.outputs.any-failed }}
# sed removes the leading `../../src/` from the paths
run: |
npx playwright merge-reports --reporter list ./all-blob-reports | sed 's|\(\.\./\)\{1,\}src/|/|g'
if [ "$FAILED" = "true" ]; then
exit 1
fi
run-a11y-test:
needs:
- build-grafana
@@ -193,6 +345,7 @@ jobs:
required-e2e-tests:
needs:
- run-e2e-tests
- build-grafana
# a11y test is not listed on purpose: it is not an important E2E test.
# It is also totally fine to fail right now.
# always() is the best function here.
@@ -203,13 +356,13 @@ jobs:
name: All E2E tests complete
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Check test suites
env:
NEEDS: ${{ toJson(needs) }}
run: |
FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')"
echo "$FAILURES"
if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then
exit 1
fi
echo "All OK!"
uses: ./.github/actions/check-jobs
with:
needs: ${{ toJson(needs) }}
failure-message: "One or more E2E test suites have failed"
success-message: "All E2E test suites completed successfully"
+1 -1
View File
@@ -208,7 +208,7 @@ jobs:
if: ${{ inputs.bump == true || inputs.bump == 'true' }}
run: |
git add package.json lerna.json yarn.lock packages public
test -e e2e/test-plugins && git add e2e/test-plugins
test -e e2e-playwright/test-plugins && git add e2e-playwright/test-plugins
git commit -m "Update version to $VERSION"
- name: Git push
+33
View File
@@ -0,0 +1,33 @@
name: Relyance Compliance Inspection
on:
schedule:
- cron: '0 0 * * *' # Run daily at 00:00 UTC
workflow_dispatch: # Allow for manual trigger
jobs:
relyance-compliance-inspector:
permissions:
contents: read
id-token: write # Needed for Vault access
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Get API key
id: vault-secrets
uses: grafana/shared-workflows/actions/get-vault-secrets@5d7e361bc7e0a183cde8afe9899fb7b596d2659b # get-vault-secrets-v1.2.0
with:
repo_secrets: |
API_KEY=relyance:API_KEY
- name: Run Relyance scan
env:
API_KEY: "${{ fromJSON(steps.vault-secrets.outputs.secrets).API_KEY }}"
run: |
docker pull gcr.io/relyance-ext/compliance_inspector:release && \
docker run --rm -v ${{ github.workspace }}:/repo --env "API_KEY=${{ env.API_KEY }}" gcr.io/relyance-ext/compliance_inspector:release
@@ -64,7 +64,7 @@ jobs:
tools/
public/
conf/
e2e/test-plugins/
e2e-playwright/test-plugins/
devenv/
key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }}
# only rebuild grafana if search files have changed ( or dependencies )
@@ -116,7 +116,7 @@ jobs:
tools/
public/
conf/
e2e/test-plugins/
e2e-playwright/test-plugins/
devenv/
key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }}
- name: Set the step name
@@ -0,0 +1,47 @@
name: Verify Storybook (Playwright)
on:
pull_request:
paths:
- 'packages/grafana-ui/**'
- 'e2e-playwright/storybook/**'
- '!docs/**'
- '!*.md'
push:
branches:
- main
paths:
- 'packages/grafana-ui/**'
- 'e2e-playwright/storybook/**'
- '!docs/**'
- '!*.md'
permissions: {}
jobs:
verify-storybook:
name: Verify Storybook (Playwright)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Storybook and E2E tests
run: yarn e2e:playwright:storybook
+1 -1
View File
@@ -237,4 +237,4 @@ public/app/plugins/**/dist/
# Mock service worker used for fake API responses in frontend development
public/mockServiceWorker.js
/e2e/test-plugins/*/dist
/e2e-playwright/test-plugins/*/dist
+8
View File
@@ -83,6 +83,14 @@ linters:
deny:
- pkg: github.com/grafana/grafana/pkg
desc: apps/playlist is not allowed to import grafana core
apps-secret:
list-mode: lax
files:
- ./apps/secret/*
- ./apps/secret/**/*
deny:
- pkg: github.com/grafana/grafana/pkg
desc: apps/secret is not allowed to import grafana core
coreplugins:
files:
- '**/pkg/tsdb/grafana-pyroscope-datasource/*'
+1
View File
@@ -83,6 +83,7 @@ COPY pkg/storage/unified/apistore pkg/storage/unified/apistore
COPY pkg/semconv pkg/semconv
COPY pkg/aggregator pkg/aggregator
COPY apps/playlist apps/playlist
COPY apps/secret apps/secret
COPY apps/investigations apps/investigations
COPY apps/advisor apps/advisor
COPY apps/dashboard apps/dashboard
+1
View File
@@ -472,6 +472,7 @@ protobuf: ## Compile protobuf definitions
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0
buf generate pkg/plugins/backendplugin/pluginextensionv2 --template pkg/plugins/backendplugin/pluginextensionv2/buf.gen.yaml
buf generate pkg/apis/secret/v0alpha1/decrypt --template pkg/apis/secret/v0alpha1/decrypt/buf.gen.yaml
buf generate apps/secret/decrypt/v1beta1 --template apps/secret/decrypt/v1beta1/buf.gen.yaml
buf generate pkg/storage/unified/proto --template pkg/storage/unified/proto/buf.gen.yaml
buf generate pkg/services/authz/proto/v1 --template pkg/services/authz/proto/v1/buf.gen.yaml
buf generate pkg/services/ngalert/store/proto/v1 --template pkg/services/ngalert/store/proto/v1/buf.gen.yaml
-2
View File
@@ -14,8 +14,6 @@ import (
v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
)
var ()
var appManifestData = app.ManifestData{
AppName: "iam",
Group: "iam.grafana.app",
+29
View File
@@ -0,0 +1,29 @@
APP_SDK_VERSION := v0.39.2
APP_SDK_DIR := $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION)
APP_SDK_BIN := $(APP_SDK_DIR)/grafana-app-sdk
.PHONY: install-app-sdk
install-app-sdk: $(APP_SDK_BIN) ## Install the Grafana App SDK
$(APP_SDK_BIN):
@echo "Installing Grafana App SDK version $(APP_SDK_VERSION)"
@mkdir -p $(APP_SDK_DIR)
# The only way to install specific versions of binaries using `go install`
# is by setting GOBIN to the directory you want to install the binary to.
GOBIN=$(APP_SDK_DIR) go install github.com/grafana/grafana-app-sdk/cmd/grafana-app-sdk@$(APP_SDK_VERSION)
@touch $@
.PHONY: update-app-sdk
update-app-sdk: ## Update the Grafana App SDK dependency in go.mod
go get github.com/grafana/grafana-app-sdk@$(APP_SDK_VERSION)
go mod tidy
.PHONY: generate
generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generation
@$(APP_SDK_BIN) generate \
--source=./kinds/ \
--gogenpath=./pkg/apis \
--grouping=group \
--defencoding=none \
--noschemasinmanifest \
--postprocess
+12
View File
@@ -0,0 +1,12 @@
version: v1
plugins:
- plugin: go
out: apps/secret/decrypt/v1beta1
opt:
- paths=source_relative
- plugin: go-grpc
out: apps/secret/decrypt/v1beta1
opt:
- paths=source_relative
- require_unimplemented_servers=false
+7
View File
@@ -0,0 +1,7 @@
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
+311
View File
@@ -0,0 +1,311 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.5
// protoc (unknown)
// source: decrypt.proto
package decryptv1beta1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type SecureValueDecryptRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The stack_id or org_id.
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
// A list of secure value names to decrypt.
Names []string `protobuf:"bytes,2,rep,name=names,proto3" json:"names,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SecureValueDecryptRequest) Reset() {
*x = SecureValueDecryptRequest{}
mi := &file_decrypt_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SecureValueDecryptRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SecureValueDecryptRequest) ProtoMessage() {}
func (x *SecureValueDecryptRequest) ProtoReflect() protoreflect.Message {
mi := &file_decrypt_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SecureValueDecryptRequest.ProtoReflect.Descriptor instead.
func (*SecureValueDecryptRequest) Descriptor() ([]byte, []int) {
return file_decrypt_proto_rawDescGZIP(), []int{0}
}
func (x *SecureValueDecryptRequest) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *SecureValueDecryptRequest) GetNames() []string {
if x != nil {
return x.Names
}
return nil
}
type SecureValueDecryptResponseCollection struct {
state protoimpl.MessageState `protogen:"open.v1"`
// A map of secure value names and their decrypted values.
// The value will be an error message if the requestor does not have permissions to read it, or if the value does not exist.
// It will never return a 404 Not Found to avoid scanning of valid secure values.
DecryptedValues map[string]*Result `protobuf:"bytes,1,rep,name=decrypted_values,json=decryptedValues,proto3" json:"decrypted_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SecureValueDecryptResponseCollection) Reset() {
*x = SecureValueDecryptResponseCollection{}
mi := &file_decrypt_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SecureValueDecryptResponseCollection) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SecureValueDecryptResponseCollection) ProtoMessage() {}
func (x *SecureValueDecryptResponseCollection) ProtoReflect() protoreflect.Message {
mi := &file_decrypt_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SecureValueDecryptResponseCollection.ProtoReflect.Descriptor instead.
func (*SecureValueDecryptResponseCollection) Descriptor() ([]byte, []int) {
return file_decrypt_proto_rawDescGZIP(), []int{1}
}
func (x *SecureValueDecryptResponseCollection) GetDecryptedValues() map[string]*Result {
if x != nil {
return x.DecryptedValues
}
return nil
}
type Result struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Result:
//
// *Result_Value
// *Result_ErrorMessage
Result isResult_Result `protobuf_oneof:"result"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Result) Reset() {
*x = Result{}
mi := &file_decrypt_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Result) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Result) ProtoMessage() {}
func (x *Result) ProtoReflect() protoreflect.Message {
mi := &file_decrypt_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Result.ProtoReflect.Descriptor instead.
func (*Result) Descriptor() ([]byte, []int) {
return file_decrypt_proto_rawDescGZIP(), []int{2}
}
func (x *Result) GetResult() isResult_Result {
if x != nil {
return x.Result
}
return nil
}
func (x *Result) GetValue() string {
if x != nil {
if x, ok := x.Result.(*Result_Value); ok {
return x.Value
}
}
return ""
}
func (x *Result) GetErrorMessage() string {
if x != nil {
if x, ok := x.Result.(*Result_ErrorMessage); ok {
return x.ErrorMessage
}
}
return ""
}
type isResult_Result interface {
isResult_Result()
}
type Result_Value struct {
Value string `protobuf:"bytes,1,opt,name=value,proto3,oneof"`
}
type Result_ErrorMessage struct {
ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3,oneof"`
}
func (*Result_Value) isResult_Result() {}
func (*Result_ErrorMessage) isResult_Result() {}
var File_decrypt_proto protoreflect.FileDescriptor
var file_decrypt_proto_rawDesc = string([]byte{
0x0a, 0x0d, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x22,
0x4f, 0x0a, 0x19, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65,
0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61,
0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73,
0x22, 0xf8, 0x01, 0x0a, 0x24, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43,
0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x74, 0x0a, 0x10, 0x64, 0x65, 0x63,
0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62,
0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43,
0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70,
0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f,
0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a,
0x5a, 0x0a, 0x14, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c,
0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79,
0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x06, 0x52,
0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x16, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a,
0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0x8e,
0x01, 0x0a, 0x14, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65,
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x72, 0x12, 0x76, 0x0a, 0x13, 0x44, 0x65, 0x63, 0x72, 0x79,
0x70, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x29,
0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e,
0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79,
0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x64, 0x65, 0x63, 0x72,
0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72,
0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42,
0x47, 0x5a, 0x45, 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, 0x61, 0x70,
0x70, 0x73, 0x2f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2f, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70,
0x74, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x3b, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70,
0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})
var (
file_decrypt_proto_rawDescOnce sync.Once
file_decrypt_proto_rawDescData []byte
)
func file_decrypt_proto_rawDescGZIP() []byte {
file_decrypt_proto_rawDescOnce.Do(func() {
file_decrypt_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_decrypt_proto_rawDesc), len(file_decrypt_proto_rawDesc)))
})
return file_decrypt_proto_rawDescData
}
var file_decrypt_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_decrypt_proto_goTypes = []any{
(*SecureValueDecryptRequest)(nil), // 0: decryptv1beta1.SecureValueDecryptRequest
(*SecureValueDecryptResponseCollection)(nil), // 1: decryptv1beta1.SecureValueDecryptResponseCollection
(*Result)(nil), // 2: decryptv1beta1.Result
nil, // 3: decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry
}
var file_decrypt_proto_depIdxs = []int32{
3, // 0: decryptv1beta1.SecureValueDecryptResponseCollection.decrypted_values:type_name -> decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry
2, // 1: decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry.value:type_name -> decryptv1beta1.Result
0, // 2: decryptv1beta1.SecureValueDecrypter.DecryptSecureValues:input_type -> decryptv1beta1.SecureValueDecryptRequest
1, // 3: decryptv1beta1.SecureValueDecrypter.DecryptSecureValues:output_type -> decryptv1beta1.SecureValueDecryptResponseCollection
3, // [3:4] is the sub-list for method output_type
2, // [2:3] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_decrypt_proto_init() }
func file_decrypt_proto_init() {
if File_decrypt_proto != nil {
return
}
file_decrypt_proto_msgTypes[2].OneofWrappers = []any{
(*Result_Value)(nil),
(*Result_ErrorMessage)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_decrypt_proto_rawDesc), len(file_decrypt_proto_rawDesc)),
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_decrypt_proto_goTypes,
DependencyIndexes: file_decrypt_proto_depIdxs,
MessageInfos: file_decrypt_proto_msgTypes,
}.Build()
File_decrypt_proto = out.File
file_decrypt_proto_goTypes = nil
file_decrypt_proto_depIdxs = nil
}
+32
View File
@@ -0,0 +1,32 @@
syntax = "proto3";
package decryptv1beta1;
option go_package = "github.com/grafana/grafana/apps/secret/decrypt/v1beta1;decryptv1beta1";
message SecureValueDecryptRequest {
// The stack_id or org_id.
string namespace = 1;
// A list of secure value names to decrypt.
repeated string names = 2;
}
message SecureValueDecryptResponseCollection {
// A map of secure value names and their decrypted values.
// The value will be an error message if the requestor does not have permissions to read it, or if the value does not exist.
// It will never return a 404 Not Found to avoid scanning of valid secure values.
map<string, Result> decrypted_values = 1;
}
message Result {
oneof result {
string value = 1;
string error_message = 2;
}
}
service SecureValueDecrypter {
// Decrypts a list of secure values and returns them as a map<name, decrypted_value>.
rpc DecryptSecureValues(SecureValueDecryptRequest) returns (SecureValueDecryptResponseCollection);
}
@@ -0,0 +1,110 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.4.0
// - protoc (unknown)
// source: decrypt.proto
package decryptv1beta1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.62.0 or later.
const _ = grpc.SupportPackageIsVersion8
const (
SecureValueDecrypter_DecryptSecureValues_FullMethodName = "/decryptv1beta1.SecureValueDecrypter/DecryptSecureValues"
)
// SecureValueDecrypterClient is the client API for SecureValueDecrypter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type SecureValueDecrypterClient interface {
// Decrypts a list of secure values and returns them as a map<name, decrypted_value>.
DecryptSecureValues(ctx context.Context, in *SecureValueDecryptRequest, opts ...grpc.CallOption) (*SecureValueDecryptResponseCollection, error)
}
type secureValueDecrypterClient struct {
cc grpc.ClientConnInterface
}
func NewSecureValueDecrypterClient(cc grpc.ClientConnInterface) SecureValueDecrypterClient {
return &secureValueDecrypterClient{cc}
}
func (c *secureValueDecrypterClient) DecryptSecureValues(ctx context.Context, in *SecureValueDecryptRequest, opts ...grpc.CallOption) (*SecureValueDecryptResponseCollection, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SecureValueDecryptResponseCollection)
err := c.cc.Invoke(ctx, SecureValueDecrypter_DecryptSecureValues_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// SecureValueDecrypterServer is the server API for SecureValueDecrypter service.
// All implementations should embed UnimplementedSecureValueDecrypterServer
// for forward compatibility
type SecureValueDecrypterServer interface {
// Decrypts a list of secure values and returns them as a map<name, decrypted_value>.
DecryptSecureValues(context.Context, *SecureValueDecryptRequest) (*SecureValueDecryptResponseCollection, error)
}
// UnimplementedSecureValueDecrypterServer should be embedded to have forward compatible implementations.
type UnimplementedSecureValueDecrypterServer struct {
}
func (UnimplementedSecureValueDecrypterServer) DecryptSecureValues(context.Context, *SecureValueDecryptRequest) (*SecureValueDecryptResponseCollection, error) {
return nil, status.Errorf(codes.Unimplemented, "method DecryptSecureValues not implemented")
}
// UnsafeSecureValueDecrypterServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to SecureValueDecrypterServer will
// result in compilation errors.
type UnsafeSecureValueDecrypterServer interface {
mustEmbedUnimplementedSecureValueDecrypterServer()
}
func RegisterSecureValueDecrypterServer(s grpc.ServiceRegistrar, srv SecureValueDecrypterServer) {
s.RegisterService(&SecureValueDecrypter_ServiceDesc, srv)
}
func _SecureValueDecrypter_DecryptSecureValues_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SecureValueDecryptRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SecureValueDecrypterServer).DecryptSecureValues(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SecureValueDecrypter_DecryptSecureValues_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SecureValueDecrypterServer).DecryptSecureValues(ctx, req.(*SecureValueDecryptRequest))
}
return interceptor(ctx, in, info, handler)
}
// SecureValueDecrypter_ServiceDesc is the grpc.ServiceDesc for SecureValueDecrypter service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var SecureValueDecrypter_ServiceDesc = grpc.ServiceDesc{
ServiceName: "decryptv1beta1.SecureValueDecrypter",
HandlerType: (*SecureValueDecrypterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "DecryptSecureValues",
Handler: _SecureValueDecrypter_DecryptSecureValues_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "decrypt.proto",
}
+66
View File
@@ -0,0 +1,66 @@
module github.com/grafana/grafana/apps/secret
go 1.24.4
require (
github.com/grafana/grafana-app-sdk v0.39.2
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf
github.com/stretchr/testify v1.10.0
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.33.2
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/getkin/kin-openapi v0.132.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-test/deep v1.1.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/client-go v0.33.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
+189
View File
@@ -0,0 +1,189 @@
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8=
github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY=
github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU=
github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+4
View File
@@ -0,0 +1,4 @@
module: "github.com/grafana/grafana/apps/secret/kinds"
language: {
version: "v0.9.0"
}
+26
View File
@@ -0,0 +1,26 @@
package secret
import "github.com/grafana/grafana/apps/secret/kinds/v1beta1"
keeper: {
kind: "Keeper"
pluralName: "Keepers"
current: "v1beta1"
scope: "Namespaced"
current: "v1beta1"
codegen: {
ts: {
enabled: false
}
go: {
enabled: true
}
}
versions: {
"v1beta1": {
schema: {
spec: v1beta1.KeeperSpec
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
package secret
manifest: {
appName: "secret"
groupOverride: "secret.grafana.app"
kinds: [
securevalue,
keeper,
]
}
+27
View File
@@ -0,0 +1,27 @@
package secret
import "github.com/grafana/grafana/apps/secret/kinds/v1beta1"
securevalue: {
kind: "SecureValue"
pluralName: "SecureValues"
current: "v1beta1"
scope: "Namespaced"
current: "v1beta1"
codegen: {
ts: {
enabled: false
}
go: {
enabled: true
}
}
versions: {
"v1beta1": {
schema: {
spec: v1beta1.SecureValueSpec
status: v1beta1.SecureValueStatus
}
}
}
}
+68
View File
@@ -0,0 +1,68 @@
package v1beta1
import "strings"
KeeperSpec: {
// Short description for the Keeper.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=253
description: string & strings.MinRunes(1) & strings.MaxRunes(253)
// AWS Keeper Configuration.
// +structType=atomic
// +optional
aws?: #AWSConfig
// Azure Keeper Configuration.
// +structType=atomic
// +optional
azure?: #AzureConfig
// GCP Keeper Configuration.
// +structType=atomic
// +optional
gcp?: #GCPConfig
// HashiCorp Vault Keeper Configuration.
// +structType=atomic
// +optional
hashiCorpVault?: #HashiCorpConfig
}
#AWSConfig: {
accessKeyID: #CredentialValue
secretAccessKey: #CredentialValue
kmsKeyID?: string
}
#AzureConfig: {
keyVaultName: string
tenantID: string
clientID: string
clientSecret: #CredentialValue
}
#GCPConfig: {
projectID: string
credentialsFile: string
}
#HashiCorpConfig: {
address: string
token: #CredentialValue
}
#CredentialValue: {
// The name of the secure value that holds the actual value.
// +optional
secureValueName: string
// The value is taken from the environment variable.
// +optional
valueFromEnv: string
// The value is taken from the Grafana config file.
// TODO: how do we explain that this is a path to the config file?
// +optional
valueFromConfig: string
}
+53
View File
@@ -0,0 +1,53 @@
package v1beta1
import (
"list"
"strings"
)
// ExposedSecureValue contains the raw decrypted secure value.
#ExposedSecureValue: string
SecureValueSpec: {
// Short description that explains the purpose of this SecureValue.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=25
description: string & strings.MinRunes(1) & strings.MaxRunes(25)
// The raw value is only valid for write. Read/List will always be empty.
// There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.
// Minimum and maximum lengths in bytes.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=24576
// +optional
value?: #ExposedSecureValue
// When using a third-party keeper, the `ref` is used to reference a value inside the remote storage.
// This should not contain sensitive information.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=1024
// +optional
ref?: string & strings.MinRunes(1) & strings.MaxRunes(1024)
// Name of the keeper, being the actual storage of the secure value.
// If not specified, the default keeper for the namespace will be used.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=253
// +optional
keeper?: string & strings.MinRunes(1) & strings.MaxRunes(253)
// The Decrypters that are allowed to decrypt this secret.
// An empty list means no service can decrypt it.
// +k8s:validation:maxItems=64
// +k8s:validation:uniqueItems=true
// +listType=atomic
// +optional
decrypters?: [...string] & list.UniqueItems() & list.MaxItems(64)
}
SecureValueStatus: {
version: int64 & >=0
// +optional
externalID: string
}
@@ -0,0 +1,18 @@
package v1beta1
import "k8s.io/apimachinery/pkg/runtime/schema"
const (
// APIGroup is the API group used by all kinds in this package
APIGroup = "secret.grafana.app"
// APIVersion is the API version used by all kinds in this package
APIVersion = "v1beta1"
)
var (
// GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package
GroupVersion = schema.GroupVersion{
Group: APIGroup,
Version: APIVersion,
}
)
@@ -0,0 +1,70 @@
//
// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT.
//
package v1beta1
import (
"encoding/json"
"fmt"
"strconv"
"gopkg.in/yaml.v3"
)
const redacted = "[REDACTED]"
// ExposedSecureValue contains the raw decrypted secure value.
type ExposedSecureValue = SecureValueExposedSecureValue
var (
_ fmt.Stringer = (*ExposedSecureValue)(nil)
_ fmt.Formatter = (*ExposedSecureValue)(nil)
_ fmt.GoStringer = (*ExposedSecureValue)(nil)
_ json.Marshaler = (*ExposedSecureValue)(nil)
_ yaml.Marshaler = (*ExposedSecureValue)(nil)
)
// NewExposedSecureValue creates a new exposed secure value wrapper.
func NewExposedSecureValue(v string) ExposedSecureValue {
return ExposedSecureValue(v)
}
// DangerouslyExposeAndConsumeValue will move the decrypted secure value out of the wrapper and return it.
// Further attempts to call this method will panic.
// The function name is intentionally kept long and weird because this is a dangerous operation and should be used carefully!
func (s *ExposedSecureValue) DangerouslyExposeAndConsumeValue() string {
if *s == "" {
panic("underlying value is empty or was consumed")
}
tmp := *s
*s = ""
return string(tmp)
}
// String must not return the exposed secure value.
func (s ExposedSecureValue) String() string {
return redacted
}
// Format must not return the exposed secure value.
func (s ExposedSecureValue) Format(f fmt.State, _verb rune) {
_, _ = fmt.Fprint(f, redacted)
}
// GoString must not return the exposed secure value.
func (s ExposedSecureValue) GoString() string {
return redacted
}
// MarshalJSON must not return the exposed secure value.
func (s ExposedSecureValue) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(redacted)), nil
}
// MarshalYAML must not return the exposed secure value.
func (s ExposedSecureValue) MarshalYAML() (any, error) {
return redacted, nil
}
@@ -0,0 +1,51 @@
//
// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT.
//
package v1beta1
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestExposedSecureValue(t *testing.T) {
expected := "[REDACTED]"
rawValue := "a-password"
esv := NewExposedSecureValue(rawValue)
// String must not return the exposed secure value.
require.Equal(t, expected, esv.String())
// Format/GoString must not return the exposed secure value.
require.Equal(t, expected, fmt.Sprintf("%+#v", esv))
require.Equal(t, expected, fmt.Sprintf("%v", esv))
require.Equal(t, expected, fmt.Sprintf("%s", esv))
buf := new(bytes.Buffer)
_, err := fmt.Fprintf(buf, "%#v", esv)
require.NoError(t, err)
require.Equal(t, expected, buf.String())
// MarshalJSON must not return the exposed secure value.
bytes, err := json.Marshal(esv)
require.NoError(t, err)
require.Equal(t, `"`+expected+`"`, string(bytes))
// MarshalYAML must not return the exposed secure value.
bytes, err = yaml.Marshal(esv)
require.NoError(t, err)
require.Equal(t, "'"+expected+"'\n", string(bytes))
// DangerouslyExposeAndConsumeValue returns the raw value.
require.Equal(t, rawValue, esv.DangerouslyExposeAndConsumeValue())
// Further calls to DangerouslyExposeAndConsumeValue will panic.
require.Panics(t, func() { esv.DangerouslyExposeAndConsumeValue() })
}
@@ -0,0 +1,28 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1beta1
import (
"encoding/json"
"io"
"github.com/grafana/grafana-app-sdk/resource"
)
// KeeperJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type KeeperJSONCodec struct{}
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
func (*KeeperJSONCodec) Read(reader io.Reader, into resource.Object) error {
return json.NewDecoder(reader).Decode(into)
}
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
func (*KeeperJSONCodec) Write(writer io.Writer, from resource.Object) error {
return json.NewEncoder(writer).Encode(from)
}
// Interface compliance checks
var _ resource.Codec = &KeeperJSONCodec{}
@@ -0,0 +1,31 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1beta1
import (
time "time"
)
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
type KeeperMetadata struct {
UpdateTimestamp time.Time `json:"updateTimestamp"`
CreatedBy string `json:"createdBy"`
Uid string `json:"uid"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
ResourceVersion string `json:"resourceVersion"`
Generation int64 `json:"generation"`
UpdatedBy string `json:"updatedBy"`
Labels map[string]string `json:"labels"`
}
// NewKeeperMetadata creates a new KeeperMetadata object.
func NewKeeperMetadata() *KeeperMetadata {
return &KeeperMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}
}
@@ -0,0 +1,319 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1beta1
import (
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"time"
)
// +k8s:openapi-gen=true
type Keeper struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the Keeper
Spec KeeperSpec `json:"spec" yaml:"spec"`
Status KeeperStatus `json:"status" yaml:"status"`
}
func (o *Keeper) GetSpec() any {
return o.Spec
}
func (o *Keeper) SetSpec(spec any) error {
cast, ok := spec.(KeeperSpec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
o.Spec = cast
return nil
}
func (o *Keeper) GetSubresources() map[string]any {
return map[string]any{
"status": o.Status,
}
}
func (o *Keeper) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.Status, true
default:
return nil, false
}
}
func (o *Keeper) SetSubresource(name string, value any) error {
switch name {
case "status":
cast, ok := value.(KeeperStatus)
if !ok {
return fmt.Errorf("cannot set status type %#v, not of type KeeperStatus", value)
}
o.Status = cast
return nil
default:
return fmt.Errorf("subresource '%s' does not exist", name)
}
}
func (o *Keeper) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
Namespace: o.ObjectMeta.Namespace,
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
}
}
func (o *Keeper) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
Group: metadata.Group,
Version: metadata.Version,
Kind: metadata.Kind,
})
}
func (o *Keeper) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
deletionTimestamp = &dt.Time
}
// Legacy ExtraFields support
extraFields := make(map[string]any)
if o.Annotations != nil {
extraFields["annotations"] = o.Annotations
}
if o.ManagedFields != nil {
extraFields["managedFields"] = o.ManagedFields
}
if o.OwnerReferences != nil {
extraFields["ownerReferences"] = o.OwnerReferences
}
return resource.CommonMetadata{
UID: string(o.UID),
ResourceVersion: o.ResourceVersion,
Generation: o.Generation,
Labels: o.Labels,
CreationTimestamp: o.CreationTimestamp.Time,
DeletionTimestamp: deletionTimestamp,
Finalizers: o.Finalizers,
UpdateTimestamp: o.GetUpdateTimestamp(),
CreatedBy: o.GetCreatedBy(),
UpdatedBy: o.GetUpdatedBy(),
ExtraFields: extraFields,
}
}
func (o *Keeper) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
o.Labels = metadata.Labels
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
if metadata.DeletionTimestamp != nil {
dt := metav1.NewTime(*metadata.DeletionTimestamp)
o.DeletionTimestamp = &dt
} else {
o.DeletionTimestamp = nil
}
o.Finalizers = metadata.Finalizers
if o.Annotations == nil {
o.Annotations = make(map[string]string)
}
if !metadata.UpdateTimestamp.IsZero() {
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
}
if metadata.CreatedBy != "" {
o.SetCreatedBy(metadata.CreatedBy)
}
if metadata.UpdatedBy != "" {
o.SetUpdatedBy(metadata.UpdatedBy)
}
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
if metadata.ExtraFields != nil {
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
if cast, ok := annotations.(map[string]string); ok {
o.Annotations = cast
}
}
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
o.ManagedFields = cast
}
}
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
o.OwnerReferences = cast
}
}
}
}
func (o *Keeper) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *Keeper) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *Keeper) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
return parsed
}
func (o *Keeper) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *Keeper) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *Keeper) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *Keeper) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *Keeper) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *Keeper) DeepCopy() *Keeper {
cpy := &Keeper{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *Keeper) DeepCopyInto(dst *Keeper) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
o.Status.DeepCopyInto(&dst.Status)
}
// Interface compliance compile-time check
var _ resource.Object = &Keeper{}
// +k8s:openapi-gen=true
type KeeperList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []Keeper `json:"items" yaml:"items"`
}
func (o *KeeperList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *KeeperList) Copy() resource.ListObject {
cpy := &KeeperList{
TypeMeta: o.TypeMeta,
Items: make([]Keeper, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*Keeper); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *KeeperList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
}
return items
}
func (o *KeeperList) SetItems(items []resource.Object) {
o.Items = make([]Keeper, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*Keeper)
}
}
func (o *KeeperList) DeepCopy() *KeeperList {
cpy := &KeeperList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *KeeperList) DeepCopyInto(dst *KeeperList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &KeeperList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *KeeperSpec) DeepCopy() *KeeperSpec {
cpy := &KeeperSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *KeeperSpec) DeepCopyInto(dst *KeeperSpec) {
resource.CopyObjectInto(dst, s)
}
// DeepCopy creates a full deep copy of KeeperStatus
func (s *KeeperStatus) DeepCopy() *KeeperStatus {
cpy := &KeeperStatus{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies KeeperStatus into another KeeperStatus object
func (s *KeeperStatus) DeepCopyInto(dst *KeeperStatus) {
resource.CopyObjectInto(dst, s)
}
@@ -0,0 +1,34 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1beta1
import (
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaKeeper = resource.NewSimpleSchema("secret.grafana.app", "v1beta1", &Keeper{}, &KeeperList{}, resource.WithKind("Keeper"),
resource.WithPlural("keepers"), resource.WithScope(resource.NamespacedScope))
kindKeeper = resource.Kind{
Schema: schemaKeeper,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &KeeperJSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func KeeperKind() resource.Kind {
return kindKeeper
}
// Schema returns a resource.SimpleSchema representation of Keeper
func KeeperSchema() *resource.SimpleSchema {
return schemaKeeper
}
// Interface compliance checks
var _ resource.Schema = kindKeeper
@@ -0,0 +1,105 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1beta1
// +k8s:openapi-gen=true
type KeeperAWSConfig struct {
AccessKeyID KeeperCredentialValue `json:"accessKeyID"`
SecretAccessKey KeeperCredentialValue `json:"secretAccessKey"`
KmsKeyID *string `json:"kmsKeyID,omitempty"`
}
// NewKeeperAWSConfig creates a new KeeperAWSConfig object.
func NewKeeperAWSConfig() *KeeperAWSConfig {
return &KeeperAWSConfig{
AccessKeyID: *NewKeeperCredentialValue(),
SecretAccessKey: *NewKeeperCredentialValue(),
}
}
// +k8s:openapi-gen=true
type KeeperCredentialValue struct {
// The name of the secure value that holds the actual value.
// +optional
SecureValueName string `json:"secureValueName"`
// The value is taken from the environment variable.
// +optional
ValueFromEnv string `json:"valueFromEnv"`
// The value is taken from the Grafana config file.
// TODO: how do we explain that this is a path to the config file?
// +optional
ValueFromConfig string `json:"valueFromConfig"`
}
// NewKeeperCredentialValue creates a new KeeperCredentialValue object.
func NewKeeperCredentialValue() *KeeperCredentialValue {
return &KeeperCredentialValue{}
}
// +k8s:openapi-gen=true
type KeeperAzureConfig struct {
KeyVaultName string `json:"keyVaultName"`
TenantID string `json:"tenantID"`
ClientID string `json:"clientID"`
ClientSecret KeeperCredentialValue `json:"clientSecret"`
}
// NewKeeperAzureConfig creates a new KeeperAzureConfig object.
func NewKeeperAzureConfig() *KeeperAzureConfig {
return &KeeperAzureConfig{
ClientSecret: *NewKeeperCredentialValue(),
}
}
// +k8s:openapi-gen=true
type KeeperGCPConfig struct {
ProjectID string `json:"projectID"`
CredentialsFile string `json:"credentialsFile"`
}
// NewKeeperGCPConfig creates a new KeeperGCPConfig object.
func NewKeeperGCPConfig() *KeeperGCPConfig {
return &KeeperGCPConfig{}
}
// +k8s:openapi-gen=true
type KeeperHashiCorpConfig struct {
Address string `json:"address"`
Token KeeperCredentialValue `json:"token"`
}
// NewKeeperHashiCorpConfig creates a new KeeperHashiCorpConfig object.
func NewKeeperHashiCorpConfig() *KeeperHashiCorpConfig {
return &KeeperHashiCorpConfig{
Token: *NewKeeperCredentialValue(),
}
}
// +k8s:openapi-gen=true
type KeeperSpec struct {
// Short description for the Keeper.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=253
Description string `json:"description"`
// AWS Keeper Configuration.
// +structType=atomic
// +optional
Aws *KeeperAWSConfig `json:"aws,omitempty"`
// Azure Keeper Configuration.
// +structType=atomic
// +optional
Azure *KeeperAzureConfig `json:"azure,omitempty"`
// GCP Keeper Configuration.
// +structType=atomic
// +optional
Gcp *KeeperGCPConfig `json:"gcp,omitempty"`
// HashiCorp Vault Keeper Configuration.
// +structType=atomic
// +optional
HashiCorpVault *KeeperHashiCorpConfig `json:"hashiCorpVault,omitempty"`
}
// NewKeeperSpec creates a new KeeperSpec object.
func NewKeeperSpec() *KeeperSpec {
return &KeeperSpec{}
}
@@ -0,0 +1,44 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1beta1
// +k8s:openapi-gen=true
type KeeperstatusOperatorState struct {
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State KeeperStatusOperatorStateState `json:"state"`
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]interface{} `json:"details,omitempty"`
}
// NewKeeperstatusOperatorState creates a new KeeperstatusOperatorState object.
func NewKeeperstatusOperatorState() *KeeperstatusOperatorState {
return &KeeperstatusOperatorState{}
}
// +k8s:openapi-gen=true
type KeeperStatus struct {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]KeeperstatusOperatorState `json:"operatorStates,omitempty"`
// additionalFields is reserved for future use
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
}
// NewKeeperStatus creates a new KeeperStatus object.
func NewKeeperStatus() *KeeperStatus {
return &KeeperStatus{}
}
// +k8s:openapi-gen=true
type KeeperStatusOperatorStateState string
const (
KeeperStatusOperatorStateStateSuccess KeeperStatusOperatorStateState = "success"
KeeperStatusOperatorStateStateInProgress KeeperStatusOperatorStateState = "in_progress"
KeeperStatusOperatorStateStateFailed KeeperStatusOperatorStateState = "failed"
)
@@ -0,0 +1,63 @@
//
// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT.
//
package v1beta1
// KeeperType represents the type of a Keeper.
type KeeperType string
const (
AWSKeeperType KeeperType = "aws"
AzureKeeperType KeeperType = "azure"
GCPKeeperType KeeperType = "gcp"
HashiCorpKeeperType KeeperType = "hashicorp"
)
func (kt KeeperType) String() string {
return string(kt)
}
// KeeperConfig is an interface that all keeper config types must implement.
type KeeperConfig interface {
Type() KeeperType
}
func (s *KeeperSpec) GetType() KeeperType {
if s.Aws != nil {
return AWSKeeperType
}
if s.Azure != nil {
return AzureKeeperType
}
if s.Gcp != nil {
return GCPKeeperType
}
if s.HashiCorpVault != nil {
return HashiCorpKeeperType
}
return ""
}
// System Keeper.
type SystemKeeperConfig struct{}
func (*SystemKeeperConfig) Type() KeeperType {
return "system"
}
func (s *KeeperAWSConfig) Type() KeeperType {
return AWSKeeperType
}
func (s *KeeperAzureConfig) Type() KeeperType {
return AzureKeeperType
}
func (s *KeeperGCPConfig) Type() KeeperType {
return GCPKeeperType
}
func (s *KeeperHashiCorpConfig) Type() KeeperType {
return HashiCorpKeeperType
}
@@ -0,0 +1,73 @@
//
// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT.
//
package v1beta1
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
var SecureValuesResourceInfo = utils.NewResourceInfo(
APIGroup, APIVersion,
"securevalues", "securevalue", "SecureValue",
func() runtime.Object { return &SecureValue{} },
func() runtime.Object { return &SecureValueList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"},
{Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"},
{Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"},
},
Reader: func(obj any) ([]any, error) {
if r, ok := obj.(*SecureValue); ok {
return []any{r.Name, r.Spec.Description, r.Spec.Keeper, r.Spec.Ref}, nil
}
return nil, fmt.Errorf("expected SecureValue but got %T", obj)
},
},
)
var KeeperResourceInfo = utils.NewResourceInfo(
APIGroup, APIVersion,
"keepers", "keeper", "Keeper",
func() runtime.Object { return &Keeper{} },
func() runtime.Object { return &KeeperList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Description", Type: "string", Format: "string", Description: "Short description for the Keeper"},
},
Reader: func(obj any) ([]any, error) {
if r, ok := obj.(*Keeper); ok {
return []any{r.Name, r.Spec.Description}, nil
}
return nil, fmt.Errorf("expected Keeper but got %T", obj)
},
},
)
// SchemeGroupVersion is group version used to register these objects.
var SchemeGroupVersion = schema.GroupVersion{Group: APIGroup, Version: APIVersion}
// Adds the list of known types to the given scheme.
func AddKnownTypes(scheme *runtime.Scheme, version string) error {
scheme.AddKnownTypes(
schema.GroupVersion{Group: APIGroup, Version: version},
&SecureValue{},
&SecureValueList{},
&Keeper{},
&KeeperList{},
)
return nil
}
@@ -0,0 +1,28 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1beta1
import (
"encoding/json"
"io"
"github.com/grafana/grafana-app-sdk/resource"
)
// SecureValueJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type SecureValueJSONCodec struct{}
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
func (*SecureValueJSONCodec) Read(reader io.Reader, into resource.Object) error {
return json.NewDecoder(reader).Decode(into)
}
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
func (*SecureValueJSONCodec) Write(writer io.Writer, from resource.Object) error {
return json.NewEncoder(writer).Encode(from)
}
// Interface compliance checks
var _ resource.Codec = &SecureValueJSONCodec{}
@@ -0,0 +1,31 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1beta1
import (
time "time"
)
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
type SecureValueMetadata struct {
UpdateTimestamp time.Time `json:"updateTimestamp"`
CreatedBy string `json:"createdBy"`
Uid string `json:"uid"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
ResourceVersion string `json:"resourceVersion"`
Generation int64 `json:"generation"`
UpdatedBy string `json:"updatedBy"`
Labels map[string]string `json:"labels"`
}
// NewSecureValueMetadata creates a new SecureValueMetadata object.
func NewSecureValueMetadata() *SecureValueMetadata {
return &SecureValueMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}
}
@@ -0,0 +1,319 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1beta1
import (
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"time"
)
// +k8s:openapi-gen=true
type SecureValue struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the SecureValue
Spec SecureValueSpec `json:"spec" yaml:"spec"`
Status SecureValueStatus `json:"status" yaml:"status"`
}
func (o *SecureValue) GetSpec() any {
return o.Spec
}
func (o *SecureValue) SetSpec(spec any) error {
cast, ok := spec.(SecureValueSpec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
o.Spec = cast
return nil
}
func (o *SecureValue) GetSubresources() map[string]any {
return map[string]any{
"status": o.Status,
}
}
func (o *SecureValue) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.Status, true
default:
return nil, false
}
}
func (o *SecureValue) SetSubresource(name string, value any) error {
switch name {
case "status":
cast, ok := value.(SecureValueStatus)
if !ok {
return fmt.Errorf("cannot set status type %#v, not of type SecureValueStatus", value)
}
o.Status = cast
return nil
default:
return fmt.Errorf("subresource '%s' does not exist", name)
}
}
func (o *SecureValue) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
Namespace: o.ObjectMeta.Namespace,
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
}
}
func (o *SecureValue) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
Group: metadata.Group,
Version: metadata.Version,
Kind: metadata.Kind,
})
}
func (o *SecureValue) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
deletionTimestamp = &dt.Time
}
// Legacy ExtraFields support
extraFields := make(map[string]any)
if o.Annotations != nil {
extraFields["annotations"] = o.Annotations
}
if o.ManagedFields != nil {
extraFields["managedFields"] = o.ManagedFields
}
if o.OwnerReferences != nil {
extraFields["ownerReferences"] = o.OwnerReferences
}
return resource.CommonMetadata{
UID: string(o.UID),
ResourceVersion: o.ResourceVersion,
Generation: o.Generation,
Labels: o.Labels,
CreationTimestamp: o.CreationTimestamp.Time,
DeletionTimestamp: deletionTimestamp,
Finalizers: o.Finalizers,
UpdateTimestamp: o.GetUpdateTimestamp(),
CreatedBy: o.GetCreatedBy(),
UpdatedBy: o.GetUpdatedBy(),
ExtraFields: extraFields,
}
}
func (o *SecureValue) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
o.Labels = metadata.Labels
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
if metadata.DeletionTimestamp != nil {
dt := metav1.NewTime(*metadata.DeletionTimestamp)
o.DeletionTimestamp = &dt
} else {
o.DeletionTimestamp = nil
}
o.Finalizers = metadata.Finalizers
if o.Annotations == nil {
o.Annotations = make(map[string]string)
}
if !metadata.UpdateTimestamp.IsZero() {
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
}
if metadata.CreatedBy != "" {
o.SetCreatedBy(metadata.CreatedBy)
}
if metadata.UpdatedBy != "" {
o.SetUpdatedBy(metadata.UpdatedBy)
}
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
if metadata.ExtraFields != nil {
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
if cast, ok := annotations.(map[string]string); ok {
o.Annotations = cast
}
}
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
o.ManagedFields = cast
}
}
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
o.OwnerReferences = cast
}
}
}
}
func (o *SecureValue) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *SecureValue) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *SecureValue) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
return parsed
}
func (o *SecureValue) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *SecureValue) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *SecureValue) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *SecureValue) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *SecureValue) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *SecureValue) DeepCopy() *SecureValue {
cpy := &SecureValue{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *SecureValue) DeepCopyInto(dst *SecureValue) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
o.Status.DeepCopyInto(&dst.Status)
}
// Interface compliance compile-time check
var _ resource.Object = &SecureValue{}
// +k8s:openapi-gen=true
type SecureValueList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []SecureValue `json:"items" yaml:"items"`
}
func (o *SecureValueList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *SecureValueList) Copy() resource.ListObject {
cpy := &SecureValueList{
TypeMeta: o.TypeMeta,
Items: make([]SecureValue, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*SecureValue); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *SecureValueList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
}
return items
}
func (o *SecureValueList) SetItems(items []resource.Object) {
o.Items = make([]SecureValue, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*SecureValue)
}
}
func (o *SecureValueList) DeepCopy() *SecureValueList {
cpy := &SecureValueList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *SecureValueList) DeepCopyInto(dst *SecureValueList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &SecureValueList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *SecureValueSpec) DeepCopy() *SecureValueSpec {
cpy := &SecureValueSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *SecureValueSpec) DeepCopyInto(dst *SecureValueSpec) {
resource.CopyObjectInto(dst, s)
}
// DeepCopy creates a full deep copy of SecureValueStatus
func (s *SecureValueStatus) DeepCopy() *SecureValueStatus {
cpy := &SecureValueStatus{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies SecureValueStatus into another SecureValueStatus object
func (s *SecureValueStatus) DeepCopyInto(dst *SecureValueStatus) {
resource.CopyObjectInto(dst, s)
}
@@ -0,0 +1,34 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1beta1
import (
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaSecureValue = resource.NewSimpleSchema("secret.grafana.app", "v1beta1", &SecureValue{}, &SecureValueList{}, resource.WithKind("SecureValue"),
resource.WithPlural("securevalues"), resource.WithScope(resource.NamespacedScope))
kindSecureValue = resource.Kind{
Schema: schemaSecureValue,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &SecureValueJSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func SecureValueKind() resource.Kind {
return kindSecureValue
}
// Schema returns a resource.SimpleSchema representation of SecureValue
func SecureValueSchema() *resource.SimpleSchema {
return schemaSecureValue
}
// Interface compliance checks
var _ resource.Schema = kindSecureValue
@@ -0,0 +1,46 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1beta1
// ExposedSecureValue contains the raw decrypted secure value.
// +k8s:openapi-gen=true
type SecureValueExposedSecureValue string
// +k8s:openapi-gen=true
type SecureValueSpec struct {
// Short description that explains the purpose of this SecureValue.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=25
Description string `json:"description"`
// The raw value is only valid for write. Read/List will always be empty.
// There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.
// Minimum and maximum lengths in bytes.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=24576
// +optional
Value *SecureValueExposedSecureValue `json:"value,omitempty"`
// When using a third-party keeper, the `ref` is used to reference a value inside the remote storage.
// This should not contain sensitive information.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=1024
// +optional
Ref *string `json:"ref,omitempty"`
// Name of the keeper, being the actual storage of the secure value.
// If not specified, the default keeper for the namespace will be used.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=253
// +optional
Keeper *string `json:"keeper,omitempty"`
// The Decrypters that are allowed to decrypt this secret.
// An empty list means no service can decrypt it.
// +k8s:validation:maxItems=64
// +k8s:validation:uniqueItems=true
// +listType=atomic
// +optional
Decrypters []string `json:"decrypters,omitempty"`
}
// NewSecureValueSpec creates a new SecureValueSpec object.
func NewSecureValueSpec() *SecureValueSpec {
return &SecureValueSpec{}
}
@@ -0,0 +1,47 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1beta1
// +k8s:openapi-gen=true
type SecureValuestatusOperatorState struct {
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State SecureValueStatusOperatorStateState `json:"state"`
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]interface{} `json:"details,omitempty"`
}
// NewSecureValuestatusOperatorState creates a new SecureValuestatusOperatorState object.
func NewSecureValuestatusOperatorState() *SecureValuestatusOperatorState {
return &SecureValuestatusOperatorState{}
}
// +k8s:openapi-gen=true
type SecureValueStatus struct {
Version int64 `json:"version"`
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]SecureValuestatusOperatorState `json:"operatorStates,omitempty"`
// +optional
ExternalID string `json:"externalID"`
// additionalFields is reserved for future use
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
}
// NewSecureValueStatus creates a new SecureValueStatus object.
func NewSecureValueStatus() *SecureValueStatus {
return &SecureValueStatus{}
}
// +k8s:openapi-gen=true
type SecureValueStatusOperatorStateState string
const (
SecureValueStatusOperatorStateStateSuccess SecureValueStatusOperatorStateState = "success"
SecureValueStatusOperatorStateStateInProgress SecureValueStatusOperatorStateState = "in_progress"
SecureValueStatusOperatorStateStateFailed SecureValueStatusOperatorStateState = "failed"
)
@@ -0,0 +1,736 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// Code generated by grafana-app-sdk. DO NOT EDIT.
package v1beta1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
ptr "k8s.io/utils/ptr"
)
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper": schema_pkg_apis_secret_v1beta1_Keeper(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig": schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig": schema_pkg_apis_secret_v1beta1_KeeperAzureConfig(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue": schema_pkg_apis_secret_v1beta1_KeeperCredentialValue(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig": schema_pkg_apis_secret_v1beta1_KeeperGCPConfig(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig": schema_pkg_apis_secret_v1beta1_KeeperHashiCorpConfig(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperList": schema_pkg_apis_secret_v1beta1_KeeperList(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec": schema_pkg_apis_secret_v1beta1_KeeperSpec(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus": schema_pkg_apis_secret_v1beta1_KeeperStatus(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState": schema_pkg_apis_secret_v1beta1_KeeperstatusOperatorState(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue": schema_pkg_apis_secret_v1beta1_SecureValue(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueList": schema_pkg_apis_secret_v1beta1_SecureValueList(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec": schema_pkg_apis_secret_v1beta1_SecureValueSpec(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus": schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref),
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState": schema_pkg_apis_secret_v1beta1_SecureValuestatusOperatorState(ref),
}
}
func schema_pkg_apis_secret_v1beta1_Keeper(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Spec is the spec of the Keeper",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec"),
},
},
"status": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus"),
},
},
},
Required: []string{"metadata", "spec", "status"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"accessKeyID": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"),
},
},
"secretAccessKey": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"),
},
},
"kmsKeyID": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"accessKeyID", "secretAccessKey"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperAzureConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"keyVaultName": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"tenantID": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"clientID": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"clientSecret": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"),
},
},
},
Required: []string{"keyVaultName", "tenantID", "clientID", "clientSecret"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperCredentialValue(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"secureValueName": {
SchemaProps: spec.SchemaProps{
Description: "The name of the secure value that holds the actual value.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"valueFromEnv": {
SchemaProps: spec.SchemaProps{
Description: "The value is taken from the environment variable.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"valueFromConfig": {
SchemaProps: spec.SchemaProps{
Description: "The value is taken from the Grafana config file.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperGCPConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"projectID": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"credentialsFile": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"projectID", "credentialsFile"},
},
},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperHashiCorpConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"address": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"token": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"),
},
},
},
Required: []string{"address", "token"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper"),
},
},
},
},
},
},
Required: []string{"metadata", "items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"description": {
SchemaProps: spec.SchemaProps{
Description: "Short description for the Keeper.",
Default: "",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](253),
Type: []string{"string"},
Format: "",
},
},
"aws": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "AWS Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig"),
},
},
"azure": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "Azure Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig"),
},
},
"gcp": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "GCP Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig"),
},
},
"hashiCorpVault": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "HashiCorp Vault Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig"),
},
},
},
Required: []string{"description"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig"},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"operatorStates": {
SchemaProps: spec.SchemaProps{
Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState"),
},
},
},
},
},
"additionalFields": {
SchemaProps: spec.SchemaProps{
Description: "additionalFields is reserved for future use",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState"},
}
}
func schema_pkg_apis_secret_v1beta1_KeeperstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"lastEvaluation": {
SchemaProps: spec.SchemaProps{
Description: "lastEvaluation is the ResourceVersion last evaluated",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"state": {
SchemaProps: spec.SchemaProps{
Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"descriptiveState": {
SchemaProps: spec.SchemaProps{
Description: "descriptiveState is an optional more descriptive state field which has no requirements on format",
Type: []string{"string"},
Format: "",
},
},
"details": {
SchemaProps: spec.SchemaProps{
Description: "details contains any extra information that is operator-specific",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
},
Required: []string{"lastEvaluation", "state"},
},
},
}
}
func schema_pkg_apis_secret_v1beta1_SecureValue(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Spec is the spec of the SecureValue",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec"),
},
},
"status": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus"),
},
},
},
Required: []string{"metadata", "spec", "status"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_secret_v1beta1_SecureValueList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue"),
},
},
},
},
},
},
Required: []string{"metadata", "items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_secret_v1beta1_SecureValueSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"description": {
SchemaProps: spec.SchemaProps{
Description: "Short description that explains the purpose of this SecureValue.",
Default: "",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](25),
Type: []string{"string"},
Format: "",
},
},
"value": {
SchemaProps: spec.SchemaProps{
Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. Minimum and maximum lengths in bytes.",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](24576),
Type: []string{"string"},
Format: "",
},
},
"ref": {
SchemaProps: spec.SchemaProps{
Description: "When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. This should not contain sensitive information.",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](1024),
Type: []string{"string"},
Format: "",
},
},
"keeper": {
SchemaProps: spec.SchemaProps{
Description: "Name of the keeper, being the actual storage of the secure value. If not specified, the default keeper for the namespace will be used.",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](253),
Type: []string{"string"},
Format: "",
},
},
"decrypters": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "The Decrypters that are allowed to decrypt this secret. An empty list means no service can decrypt it.",
MaxItems: ptr.To[int64](64),
UniqueItems: true,
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"description"},
},
},
}
}
func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"version": {
SchemaProps: spec.SchemaProps{
Default: 0,
Type: []string{"integer"},
Format: "int64",
},
},
"operatorStates": {
SchemaProps: spec.SchemaProps{
Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState"),
},
},
},
},
},
"externalID": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"additionalFields": {
SchemaProps: spec.SchemaProps{
Description: "additionalFields is reserved for future use",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
},
Required: []string{"version"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState"},
}
}
func schema_pkg_apis_secret_v1beta1_SecureValuestatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"lastEvaluation": {
SchemaProps: spec.SchemaProps{
Description: "lastEvaluation is the ResourceVersion last evaluated",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"state": {
SchemaProps: spec.SchemaProps{
Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"descriptiveState": {
SchemaProps: spec.SchemaProps{
Description: "descriptiveState is an optional more descriptive state field which has no requirements on format",
Type: []string{"string"},
Format: "",
},
},
"details": {
SchemaProps: spec.SchemaProps{
Description: "details contains any extra information that is operator-specific",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
},
Required: []string{"lastEvaluation", "state"},
},
},
}
}
+65
View File
@@ -0,0 +1,65 @@
//
// This file is generated by grafana-app-sdk
// DO NOT EDIT
//
package apis
import (
"fmt"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/resource"
v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
)
var ()
var appManifestData = app.ManifestData{
AppName: "secret",
Group: "secret.grafana.app",
Kinds: []app.ManifestKind{
{
Kind: "SecureValue",
Scope: "Namespaced",
Conversion: false,
Versions: []app.ManifestKindVersion{
{
Name: "v1beta1",
},
},
},
{
Kind: "Keeper",
Scope: "Namespaced",
Conversion: false,
Versions: []app.ManifestKindVersion{
{
Name: "v1beta1",
},
},
},
},
}
func LocalManifest() app.Manifest {
return app.NewEmbeddedManifest(appManifestData)
}
func RemoteManifest() app.Manifest {
return app.NewAPIServerManifest("secret")
}
var kindVersionToGoType = map[string]resource.Kind{
"SecureValue/v1beta1": v1beta1.SecureValueKind(),
"Keeper/v1beta1": v1beta1.KeeperKind(),
}
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
// If there is no association for the provided Kind and Version, exists will return false.
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
return goType, exists
}
+2 -2
View File
@@ -6,9 +6,9 @@ When end-to-end testing Grafana plugins, a best practice is to use the [`@grafan
## Add end-to-end tests for a core plugin
You can add Playwright end-to-end tests for plugins to the [`e2e/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e) directory.
You can add Playwright end-to-end tests for plugins to the [`e2e-playwright/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e) directory.
1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e). This is the directory where your plugin tests will be kept.
1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e). This is the directory where your plugin tests will be kept.
1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration.
In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests.
@@ -239,7 +239,7 @@ If the user is deleted from Grafana, the user will be not be able to login and r
### Team Sync
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
With Team Sync, it's possible to set up synchronization between teams in your authentication provider and Grafana. You can send Grafana values as part of an HTTP header and have Grafana map them to your team structure. This allows you to put users into specific teams automatically.
@@ -421,7 +421,7 @@ auto_login = true
### Team Sync
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
With Team Sync you can map your Entra ID groups to teams in Grafana so that your users will automatically be added to
@@ -320,7 +320,7 @@ org_mapping = org_foo:org_foo:Viewer org_bar:org_bar:Editor *:org_baz:Editor
## Configure team synchronization
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
By using Team Sync, you can link your OAuth2 groups to teams within Grafana. This will automatically assign users to the appropriate teams.
@@ -211,7 +211,7 @@ role_attribute_path = [login=='octocat'][0] && 'GrafanaAdmin' || 'Viewer'
## Configure team synchronization
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
By using Team Sync, you can map teams from your GitHub organization to teams within Grafana. This will automatically assign users to the appropriate teams.
@@ -237,7 +237,7 @@ use_refresh_token = true
## Configure team synchronization
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams.
@@ -160,7 +160,7 @@ auto_login = true
### Configure team synchronization
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
With team sync, you can easily add users to teams by utilizing their Google groups. To set up team sync for Google OAuth, refer to the following example.
@@ -109,7 +109,7 @@ viewer
## Team sync
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
[Teamsync](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-team-sync/) is a feature that allows you to map groups from your identity provider to Grafana teams. This is useful if you want to give your users access to specific dashboards or folders based on their group membership.
@@ -238,7 +238,7 @@ org_mapping = ["Group 1:org_foo:Viewer", "Group 2:org_bar:Editor", "*:3:Editor"]
### Configure team synchronization
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
By using Team Sync, you can link your Okta groups to teams within Grafana. This will automatically assign users to the appropriate teams.
@@ -12,7 +12,7 @@ weight: 540
# Configure team sync for SAML
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
To use SAML Team sync, set [`assertion_attribute_groups`](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_groups) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab.
@@ -17,7 +17,7 @@ weight: 1000
Team sync lets you set up synchronization between your auth providers teams and teams in Grafana. This enables LDAP, OAuth, or SAML users who are members of certain teams or groups to automatically be added or removed as members of certain teams in Grafana.
{{< admonition type="note" >}}
Only available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
Available in [Grafana Enterprise](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/).
{{< /admonition >}}
Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized in the team members list, see `LDAP` label in screenshot.
@@ -24,6 +24,8 @@ killercoda:
In this tutorial, we walk you through the process of setting up your first alert in just a few minutes. You'll witness your alert in action with real-time data, as well as sending alert notifications.
{{< youtube id="YVhKNbQNNss" >}}
In this tutorial you will:
- Create a contact point.
@@ -0,0 +1,544 @@
import { readFileSync } from 'fs';
import { load } from 'js-yaml';
import { Page } from 'playwright-core';
import { v4 as uuidv4 } from 'uuid';
import {
test,
expect,
CreateDataSourcePageArgs,
DataSourceConfigPage,
E2ESelectorGroups,
DashboardPage,
} from '@grafana/plugin-e2e';
import { AzureQueryType } from '../../public/app/plugins/datasource/azuremonitor/dataquery.gen';
import { selectors as azMonSelectors } from '../../public/app/plugins/datasource/azuremonitor/e2e/selectors';
import {
AzureMonitorDataSourceJsonData,
AzureMonitorDataSourceSecureJsonData,
} from '../../public/app/plugins/datasource/azuremonitor/types/types';
const provisioningPath = 'provisioning/datasources/azmonitor-ds.yaml';
type AzureMonitorConfig = {
secureJsonData: AzureMonitorDataSourceSecureJsonData;
jsonData: AzureMonitorDataSourceJsonData;
};
type AzureMonitorProvision = { datasources: AzureMonitorConfig[] };
const dataSourceName = `Azure Monitor E2E Tests - ${uuidv4()}`;
const storageAcctName = 'azmonteststorage';
const logAnalyticsName = 'az-mon-test-logs';
const applicationInsightsName = 'az-mon-test-ai-a';
const rootSubscription = 'grafanalabs-datasources-dev';
async function provisionAzureMonitorDatasources(
createDataSourceConfigPage: (args: CreateDataSourcePageArgs) => Promise<DataSourceConfigPage>,
datasourceConfig: AzureMonitorConfig,
page: Page,
selectors: E2ESelectorGroups
) {
const configPage = await createDataSourceConfigPage({
type: 'grafana-azure-monitor-datasource',
name: dataSourceName,
});
const azureCloudInput = page.getByTestId(azMonSelectors.components.configEditor.azureCloud.input).locator('input');
await azureCloudInput.fill('Azure');
await azureCloudInput.press('Enter');
const tenantIdInput = page.getByTestId(azMonSelectors.components.configEditor.tenantID.input).locator('input');
await tenantIdInput.fill(datasourceConfig.jsonData.tenantId!);
const clientIdInput = page.getByTestId(azMonSelectors.components.configEditor.clientID.input).locator('input');
await clientIdInput.fill(datasourceConfig.jsonData.clientId!);
const clientSecretInput = page
.getByTestId(azMonSelectors.components.configEditor.clientSecret.input)
.locator('input');
await clientSecretInput.fill(datasourceConfig.secureJsonData.clientSecret!);
const loadSubscriptionsButton = page.getByTestId(azMonSelectors.components.configEditor.loadSubscriptions.button);
await loadSubscriptionsButton.click();
const defaultSubscriptionInput = page
.getByTestId(azMonSelectors.components.configEditor.defaultSubscription.input)
.locator('input');
await defaultSubscriptionInput.fill('datasources');
await configPage
.getByGrafanaSelector(selectors.components.Select.option)
.filter({
hasText: 'datasources',
})
.click();
await configPage.saveAndTest();
}
// TODO unskip when we've figured out how to populate the credentials in CI
test.describe.skip(
'Azure Monitor datasource',
{
tag: ['@cloud-plugins'],
},
() => {
let datasourceConfig: AzureMonitorConfig;
test.beforeAll(async () => {
// Check if we're running in CI
const CI = process.env.CI;
if (CI) {
const outputs = JSON.parse(readFileSync('outputs.json', 'utf8'));
datasourceConfig = {
jsonData: {
cloudName: 'Azure',
tenantId: outputs.tenantId,
clientId: outputs.clientId,
},
secureJsonData: { clientSecret: outputs.clientSecret },
};
} else {
const yamlContent = readFileSync(provisioningPath, 'utf8');
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const yaml = load(yamlContent) as AzureMonitorProvision;
datasourceConfig = yaml.datasources[0];
}
});
test('create dashboard, add panel for metrics, log analytics, ARG, and traces queries', async ({
createDataSourceConfigPage,
gotoDashboardPage,
selectors,
page,
}) => {
// this test can absolutely take longer than the default 30s timeout
test.setTimeout(120000);
await provisionAzureMonitorDatasources(createDataSourceConfigPage, datasourceConfig, page, selectors);
// Create new dashboard
const dashboardPage = await gotoDashboardPage({
timeRange: {
from: 'now-6h',
to: 'now',
zone: 'Coordinated Universal Time',
},
});
// Add metrics panel
const metricsPanel = await dashboardPage.addPanel();
// Select Azure Monitor datasource
const datasourcePicker = metricsPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2);
await datasourcePicker.fill(dataSourceName);
const datasourceList = metricsPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList);
await datasourceList.getByText(dataSourceName).click();
// Configure metrics query
const resourcePickerButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.select.button);
await resourcePickerButton.click();
await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 });
const resourceSearchInput = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.search.input);
await resourceSearchInput.fill(storageAcctName);
await expect(page.getByText(storageAcctName)).toBeVisible({ timeout: 30000 });
await page.getByText(storageAcctName).click();
const applyButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.apply.button);
await applyButton.click();
await expect(page.getByText('microsoft.storage/storageaccounts')).toBeVisible();
const metricNameInput = page
.getByTestId(azMonSelectors.components.queryEditor.metricsQueryEditor.metricName.input)
.locator('input');
await metricNameInput.fill('Used capacity');
await metricNameInput.press('Enter');
// Save and go back to dashboard
metricsPanel.backToDashboard();
// Add logs panel
const logsPanel = await dashboardPage.addPanel();
// Select Azure Monitor datasource
await datasourcePicker.fill(dataSourceName);
await datasourceList.getByText(dataSourceName).click();
// Switch to Logs query type
const queryTypeSelect = page.getByTestId(azMonSelectors.components.queryEditor.header.select).locator('input');
await queryTypeSelect.fill('Logs');
await queryTypeSelect.press('Enter');
// Configure logs query
await resourcePickerButton.click();
await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 });
await resourceSearchInput.fill(logAnalyticsName);
await expect(page.getByText(logAnalyticsName)).toBeVisible({ timeout: 30000 });
await page.getByText(logAnalyticsName).click();
await applyButton.click();
let codeEditor = logsPanel.getByGrafanaSelector(selectors.components.CodeEditor.container).locator('textarea');
await codeEditor.fill('AzureDiagnostics', { force: true });
const formatSelection = page
.getByTestId(azMonSelectors.components.queryEditor.logsQueryEditor.formatSelection.input)
.locator('input');
await formatSelection.fill('Time series');
await formatSelection.press('Enter');
// Save and go back to dashboard
logsPanel.backToDashboard();
// Add Azure Resource Graph panel
const resourceGraphPanel = await dashboardPage.addPanel();
// Select Azure Monitor datasource
await datasourcePicker.fill(dataSourceName);
await datasourceList.getByText(dataSourceName).click();
// Switch to Azure Resource Graph query type
await queryTypeSelect.fill('Azure Resource Graph');
await queryTypeSelect.press('Enter');
// Configure Azure Resource Graph query
const subscriptionsInput = page
.getByTestId(azMonSelectors.components.queryEditor.argsQueryEditor.subscriptions.input)
.locator('input');
await subscriptionsInput.fill('datasources');
await subscriptionsInput.press('Enter');
codeEditor = resourceGraphPanel
.getByGrafanaSelector(selectors.components.CodeEditor.container)
.locator('textarea');
await codeEditor.fill(
"Resources | where resourceGroup == 'cloud-plugins-e2e-test-azmon' | project name, resourceGroup"
);
// Save and go back to dashboard
resourceGraphPanel.backToDashboard();
// Add traces panel
const tracesPanel = await dashboardPage.addPanel();
// Select Azure Monitor datasource
await datasourcePicker.fill(dataSourceName);
await datasourceList.getByText(dataSourceName).click();
// Switch to Traces query type
await queryTypeSelect.fill('Traces');
await queryTypeSelect.press('Enter');
// Configure traces query
await resourcePickerButton.click();
await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 });
await resourceSearchInput.fill(applicationInsightsName);
await expect(page.getByText(applicationInsightsName)).toBeVisible({ timeout: 30000 });
await page.getByText(applicationInsightsName).click();
await applyButton.click();
await formatSelection.fill('Trace');
await formatSelection.press('Enter');
});
test('create dashboard with template variables', async ({
createDataSourceConfigPage,
gotoDashboardPage,
page,
selectors,
}) => {
await provisionAzureMonitorDatasources(createDataSourceConfigPage, datasourceConfig, page, selectors);
// Create new dashboard
const dashboardPage = await gotoDashboardPage({
timeRange: {
from: 'now-6h',
to: 'now',
},
});
// Add subscription variable
await addAzureMonitorVariable(
page,
dashboardPage,
selectors,
'subscription',
AzureQueryType.SubscriptionsQuery,
true
);
// Add resource groups variable
await addAzureMonitorVariable(
page,
dashboardPage,
selectors,
'resourceGroups',
AzureQueryType.ResourceGroupsQuery,
false,
{
subscription: '$subscription',
}
);
// Add namespaces variable
await addAzureMonitorVariable(
page,
dashboardPage,
selectors,
'namespaces',
AzureQueryType.NamespacesQuery,
false,
{
subscription: '$subscription',
resourceGroup: '$resourceGroups',
}
);
// Add region variable
await addAzureMonitorVariable(page, dashboardPage, selectors, 'region', AzureQueryType.LocationsQuery, false, {
subscription: '$subscription',
});
// Add resource variable
await addAzureMonitorVariable(
page,
dashboardPage,
selectors,
'resource',
AzureQueryType.ResourceNamesQuery,
false,
{
subscription: '$subscription',
resourceGroup: '$resourceGroups',
namespace: '$namespaces',
region: '$region',
}
);
// Set variable values
const subscriptionVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('subscription'))
.locator('..')
.locator('input');
await subscriptionVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.Select.option)
.filter({ hasText: 'grafanalabs-datasources-dev' })
.click();
const resourceGroupsVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('resourceGroups'))
.locator('..')
.locator('input');
await resourceGroupsVariable.fill('cloud-plugins-e2e-test-azmon');
await resourceGroupsVariable.press('ArrowDown');
await resourceGroupsVariable.press('Enter');
const namespacesVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('namespaces'))
.locator('..')
.locator('input');
await namespacesVariable.fill('microsoft.storage/storageaccounts');
await namespacesVariable.press('ArrowDown');
await namespacesVariable.press('Enter');
const regionVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('region'))
.locator('..')
.locator('input');
await regionVariable.fill('uk south');
await regionVariable.press('ArrowDown');
await regionVariable.press('Enter');
const resourceVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('resource'))
.locator('..')
.locator('input');
await resourceVariable.fill(storageAcctName);
await resourceVariable.press('ArrowDown');
await resourceVariable.press('Enter');
// Add panel with template variables
const newPanel = await dashboardPage.addPanel();
const datasourcePicker = newPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2);
await datasourcePicker.fill(dataSourceName);
const datasourceList = newPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList);
await datasourceList.getByText(dataSourceName).click();
const resourcePickerButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.select.button);
await resourcePickerButton.click();
const advancedCollapse = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.collapse);
await advancedCollapse.click();
const advancedSubscription = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.subscription.input)
.locator('input');
await advancedSubscription.fill('$subscription');
const advancedResourceGroup = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.resourceGroup.input)
.locator('input');
await advancedResourceGroup.fill('$resourceGroups');
const advancedNamespace = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.namespace.input)
.locator('input');
await advancedNamespace.fill('$namespaces');
const advancedRegion = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.region.input)
.locator('input');
await advancedRegion.fill('$region');
const advancedResource = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.resource.input)
.locator('input');
await advancedResource.fill('$resource');
const applyButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.apply.button);
await applyButton.click();
const metricNameInput = page
.getByTestId(azMonSelectors.components.queryEditor.metricsQueryEditor.metricName.input)
.locator('input');
await metricNameInput.fill('Transactions');
await metricNameInput.press('Enter');
});
// Helper function to add template variables
async function addAzureMonitorVariable(
page: Page,
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
name: string,
type: AzureQueryType,
isFirst: boolean,
options?: {
subscription?: string;
resourceGroup?: string;
namespace?: string;
resource?: string;
region?: string;
}
) {
// Navigate to variables settings
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.settingsButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Variables')).click();
if (isFirst) {
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.List.addVariableCTAV2)
.click();
} else {
await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click();
}
// Configure variable
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.clear();
await nameInput.fill(name);
const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2);
await datasourcePicker.fill(dataSourceName);
const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList);
await datasourceList.getByText(dataSourceName).click();
const queryTypeInput = page
.getByTestId(azMonSelectors.components.variableEditor.queryType.input)
.locator('input');
await queryTypeInput.fill(type.replace('Azure', '').trim());
await queryTypeInput.press('Enter');
// Configure type-specific options
switch (type) {
case AzureQueryType.ResourceGroupsQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
break;
case AzureQueryType.LocationsQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
break;
case AzureQueryType.NamespacesQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
if (options?.resourceGroup) {
const resourceGroupInput = page
.getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input)
.locator('input');
await resourceGroupInput.fill(options.resourceGroup);
await resourceGroupInput.press('Enter');
}
break;
case AzureQueryType.ResourceNamesQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
if (options?.resourceGroup) {
const resourceGroupInput = page
.getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input)
.locator('input');
await resourceGroupInput.fill(options.resourceGroup);
await resourceGroupInput.press('Enter');
}
if (options?.namespace) {
const namespaceInput = page
.getByTestId(azMonSelectors.components.variableEditor.namespace.input)
.locator('input');
await namespaceInput.fill(options.namespace);
await namespaceInput.press('Enter');
}
if (options?.region) {
const regionInput = page
.getByTestId(azMonSelectors.components.variableEditor.region.input)
.locator('input');
await regionInput.fill(options.region);
await regionInput.press('Enter');
}
break;
case AzureQueryType.MetricNamesQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
if (options?.resourceGroup) {
const resourceGroupInput = page
.getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input)
.locator('input');
await resourceGroupInput.fill(options.resourceGroup);
await resourceGroupInput.press('Enter');
}
if (options?.namespace) {
const namespaceInput = page
.getByTestId(azMonSelectors.components.variableEditor.namespace.input)
.locator('input');
await namespaceInput.fill(options.namespace);
await namespaceInput.press('Enter');
}
if (options?.resource) {
const resourceInput = page
.getByTestId(azMonSelectors.components.variableEditor.resource.input)
.locator('input');
await resourceInput.fill(options.resource);
await resourceInput.press('Enter');
}
break;
}
// Save variable
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton)
.click();
// Go back to dashboard
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
}
}
);
@@ -0,0 +1,56 @@
import { test, expect } from '@grafana/plugin-e2e';
import testV2Dashboard from '../dashboards/TestV2Dashboard.json';
import { flows } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
test.describe(
'Dashboard panels',
{
tag: ['@dashboards'],
},
() => {
test('can duplicate a panel', async ({ dashboardPage, selectors, page }) => {
await page.goto(selectors.pages.ImportDashboard.url);
await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard));
await page.getByTestId(selectors.components.DashboardImportPage.submit).click();
await page.getByTestId(selectors.components.ImportDashboardForm.name).fill('Paste tab');
await page.getByTestId(selectors.components.ImportDashboardForm.submit).click();
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
const oldPanelTitle = 'New panel';
const panelTitle = 'Unique';
await flows.changePanelTitle(dashboardPage, selectors, oldPanelTitle, panelTitle);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.menu(panelTitle))
.click({ force: true });
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('More...')).hover();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Duplicate')).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))).toHaveCount(
2
);
// Save, reload, and ensure duplicate has persisted
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click();
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))).toHaveCount(
2
);
});
}
);
@@ -0,0 +1,804 @@
import { Page } from 'playwright-core';
import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e';
import testV2Dashboard from '../dashboards/TestV2Dashboard.json';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
// these tests require a larger viewport
test.use({
viewport: { width: 1280, height: 1080 },
});
test.describe(
'Grouping panels',
{
tag: ['@dashboards'],
},
() => {
// Helper functions
async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string) {
await page.goto(selectors.pages.ImportDashboard.url);
await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard));
await page.getByTestId(selectors.components.DashboardImportPage.submit).click();
await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title);
await page.getByTestId(selectors.components.ImportDashboardForm.submit).click();
}
async function saveDashboard(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click();
}
async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click();
await page.getByText('Group into row').click();
}
async function groupIntoTab(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click();
await page.getByText('Group into tab').click();
}
async function ungroupPanels(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.ungroup).click();
}
async function addPanel(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click();
}
/*
* Rows
*/
test('can group and ungroup new panels into row', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Group new panels into row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Group into row
await groupIntoRow(page, dashboardPage, selectors);
// Verify row and panel titles
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify row and panel titles after reload
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Ungroup
await ungroupPanels(dashboardPage, selectors);
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can add and remove several rows', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Add and remove rows');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click();
await addPanel(dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click();
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(5);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(5);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
.locator('..')
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
.locator('..')
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can paste a copied row', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Paste row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// Copy by selecting row and using copy button
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Copy' }).click();
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.pasteRow).click();
// scroll `New row` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
await saveDashboard(dashboardPage, selectors);
await page.reload();
// scroll last `New panel` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
.last()
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
});
test('can duplicate a row', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Duplicate row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// Duplicate by selecting row and using duplicate button
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
// scroll `New row` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
await saveDashboard(dashboardPage, selectors);
await page.reload();
// scroll last `New panel` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
.last()
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
});
test('can collapse rows', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Collapse rows');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// Duplicate row
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
// scroll `New row` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
// Collapse rows by clicking on their titles
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(0);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(0);
});
test('can convert rows into tabs when changing layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Rows to tabs');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// Duplicate row
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
// Go back to dashboard options
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
// Expand layouts section
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category'))
.click();
// Select tabs layout
await page.getByLabel('Tabs').click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can group and ungroup new panels into row with tab', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Group new panels into tab with row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Group into row with tab
await groupIntoRow(page, dashboardPage, selectors);
await groupIntoTab(page, dashboardPage, selectors);
// Verify tab and panel titles
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify tab, row and panel titles after reload
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Ungroup
await ungroupPanels(dashboardPage, selectors); // ungroup tabs
await ungroupPanels(dashboardPage, selectors); // ungroup rows
// Verify tab and row titles is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
/*
* Tabs
*/
test('can group and ungroup new panels into tab', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Group new panels into tab');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Group into tab
await groupIntoTab(page, dashboardPage, selectors);
// Verify tab and panel titles
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify row and panel titles after reload
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Ungroup
await ungroupPanels(dashboardPage, selectors);
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can add and remove several tabs', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Add and remove tabs');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click();
await addPanel(dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click();
await addPanel(dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toHaveAttribute(
'aria-selected',
'true'
);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(1);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toHaveAttribute(
'aria-selected',
'true'
);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(1);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can paste a copied tab', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Paste tab');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
// Copy by selecting tab and using copy button
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Copy' }).click();
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.pasteTab).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can duplicate a tab', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Duplicate tab');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
// Duplicate by selecting tab and using duplicate button
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can convert tabs into rows when changing layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Tabs to rows');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
// Duplicate tab twice
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible();
// Go back to dashboard options
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
// Expand layouts section
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category'))
.click();
// Select rows layout
await page.getByLabel('Rows').click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab'))
).toBeVisible();
// Wait for panels to load
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first()
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1'))
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1'))
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2'))
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2'))
).toBeVisible();
// scroll `New row` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(9);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab'))
).toBeVisible();
// Wait for panels to load
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first()
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1'))
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1'))
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2'))
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2'))
).toBeVisible();
// scroll last `New panel` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
.last()
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(9);
});
test('can group and ungroup new panels into tab with row', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Group new panels into tab with row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Group into tab
await groupIntoTab(page, dashboardPage, selectors);
await groupIntoRow(page, dashboardPage, selectors);
// Verify tab and panel titles
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify tab, row and panel titles after reload
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Ungroup
await ungroupPanels(dashboardPage, selectors); // ungroup rows
await ungroupPanels(dashboardPage, selectors); // ungroup tabs
// Verify tab and row titles is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
}
);
@@ -0,0 +1,38 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
test.describe(
'Dashboard Outline',
{
tag: ['@dashboards'],
},
() => {
test('can use dashboard outline', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click();
// Should be able to click Variables item in outline to see add variable button
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
).toBeVisible();
// Clicking a panel should scroll that panel in view
await expect(page.getByText('Dashboard panel 48')).toBeHidden();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Panel #48')).click();
await expect(page.getByText('Dashboard panel 48')).toBeVisible();
});
}
);
@@ -0,0 +1,44 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard panels',
{
tag: ['@dashboards'],
},
() => {
test('can add a new panel', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click();
// Check that new panel has been added
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toBeVisible();
// Check that pressing the configure button shows the panel editor
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.content)
.filter({ hasText: 'Configure' })
.click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible();
});
}
);
@@ -0,0 +1,91 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - Ad hoc variables',
{
tag: ['@dashboards'],
},
() => {
test('can add a new adhoc variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'adhoc',
name: 'VariableUnderTest',
value: 'label1',
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// Select datasource for the ad hoc variable
const dataSource = 'gdev-loki';
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect)
.click();
await page.getByText(dataSource).click();
// mock the API call to get the labels
const labels = ['label1', 'label2'];
await page.route('**/resources/labels*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labels,
}),
});
});
// Select the variable in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click();
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// mock the API call to get the label values
const labelValues = ['label2Value1'];
await page.route(`**/resources/label/${labels[1]}/values*`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labelValues,
}),
});
});
// choose the label and value
await page.getByText(labels[1]).click();
await page.getByText('=', { exact: true }).click();
await page.getByText(labelValues[0]).click();
await page.keyboard.press('Escape');
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${labels[1]}="${labelValues[0]}"`);
});
}
);
@@ -0,0 +1,62 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - datasource variables',
{
tag: ['@dashboards'],
},
() => {
test('can add a new datasource variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const dsType = 'cloudwatch';
const variable: Variable = {
type: 'datasource',
name: 'VariableUnderTest',
label: 'VariableUnderTest',
value: `gdev-${dsType}`,
};
// Common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect)
.click();
await page.getByText(dsType).click();
const regexFilter = 'cloud';
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.nameFilter)
.fill(regexFilter);
// Assert the variable dropdown is visible with correct label
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// Assert the variable values are correctly displayed in the panel
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`${variable.name}: ${variable.value}`);
});
}
);
@@ -0,0 +1,89 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - Group By variables',
{
tag: ['@dashboards'],
},
() => {
test('can add a new group by variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'groupby',
name: 'VariableUnderTest',
value: 'label1',
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// select datasource for the group by variable
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.dataSourceSelect)
.click();
const dataSource = 'gdev-loki';
await page.getByText(dataSource).click();
// mock the API call to get the labels
const labels = ['label1', 'label2'];
await page.route('**/resources/labels*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labels,
}),
});
});
// open the variable dropdown in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).locator('> div').click();
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// mock the API call to get the label values
const labelValues = ['label2Value1'];
await page.route(`**/resources/label/${labels[1]}/values*`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labelValues,
}),
});
});
// choose the label and value
await page.getByText(labels[1]).click();
await page.keyboard.press('Escape');
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${labels[1]}`);
});
}
);
@@ -0,0 +1,58 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = '5SdHCadmz/panel-tests-graph';
test.describe(
'Dashboard',
{
tag: ['@dashboards'],
},
() => {
test('can edit panel title and description', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
const oldTitle = 'No Data Points Warning';
const firstPanelTitle = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.first()
.locator('h2')
.first();
await expect(firstPanelTitle).toHaveText(oldTitle);
const newDescription = 'A description of this panel';
await flows.changePanelDescription(dashboardPage, selectors, oldTitle, newDescription);
const newTitle = 'New Panel Title';
await flows.changePanelTitle(dashboardPage, selectors, oldTitle, newTitle);
// Check that new title is reflected in panel header
const updatedPanelTitle = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.first()
.locator('h2')
.first();
await expect(updatedPanelTitle).toHaveText(newTitle);
// Reveal description tooltip and check that its value is as expected
const descriptionIcon = page.locator('[data-testid="title-items-container"] > span').first();
await descriptionIcon.click({ force: true });
// Get the tooltip ID from the aria-describedby attribute
const tooltipId = await descriptionIcon.getAttribute('aria-describedby');
const tooltip = page.locator(`[id="${tooltipId}"]`);
await expect(tooltip).toHaveText(`${newDescription}\n`);
});
}
);
@@ -0,0 +1,43 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = '5SdHCadmz/panel-tests-graph';
test.describe(
'Dashboard',
{
tag: ['@dashboards'],
},
() => {
test('can toggle transparent background switch', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^No Data Points Warning$/ })
.first()
.click();
const panelTitle = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title('No Data Points Warning')
);
const initialBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
expect(initialBackground).not.toMatch(/rgba\(0, 0, 0, 0\)/);
await page.locator('#transparent-background').click({ force: true });
const transparentBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
expect(transparentBackground).toMatch(/rgba\(0, 0, 0, 0\)/);
});
}
);
@@ -0,0 +1,81 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - Query variable',
{
tag: ['@dashboards'],
},
() => {
test('can add a new query variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const queryVariableOptions = ['default'];
const variable: Variable = {
type: 'query',
name: 'VariableUnderTest',
value: queryVariableOptions[0],
label: 'VariableUnderTest', // constant doesn't really need a label
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// open the modal query variable editor
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsOpenButton)
.click();
// select a core data source that just runs a query during preview
await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click();
const dataSource = 'gdev-cloudwatch';
// this will trigger an API call to get the query options
await page.getByText(dataSource).click();
// show the preview of the query results
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.previewButton)
.click();
// assert the query results are shown
const firstPreviewOption = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption)
.first();
await expect(firstPreviewOption).toBeVisible();
const previewOptionText = await firstPreviewOption.textContent();
const previewOption = previewOptionText?.trim() || '';
// close the modal
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.closeButton)
.click();
// assert the query variable values are in the variable value select
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.name!)
);
const nextElement = variableLabel.locator('+ *');
await expect(nextElement).toHaveText(previewOption);
// Assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${previewOption}`);
});
}
);
@@ -0,0 +1,183 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - variables',
{
tag: ['@dashboards'],
},
() => {
test('can add a new custom variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'custom',
name: 'foo',
label: 'Foo',
value: 'one,two,three',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// set the custom variable value
const customValueInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
);
await customValueInput.fill(variable.value);
await customValueInput.blur();
// assert the dropdown for the variable is visible and has the correct values
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
const values = variable.value.split(',');
const firstValueLink = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(values[0])
);
await expect(firstValueLink).toBeVisible();
// check that variable deletion works
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await expect(variableLabel).toBeHidden();
});
test('can add a new constant variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'constant',
name: 'VariableUnderTest',
value: 'foo',
label: 'VariableUnderTest', // constant doesn't really need a label
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// set the constant variable value
const type = 'variable-type Value';
const fieldLabel = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel(type)
);
await expect(fieldLabel).toBeVisible();
const inputField = fieldLabel.locator('input');
await expect(inputField).toBeVisible();
await inputField.fill(variable.value);
await inputField.blur();
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`);
});
test('can add a new textbox variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'textbox',
name: 'VariableUnderTest',
value: 'foo',
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// set the textbox variable value
const type = 'variable-type Value';
const fieldLabel = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel(type)
);
await expect(fieldLabel).toBeVisible();
const inputField = fieldLabel.locator('input');
await expect(inputField).toBeVisible();
await inputField.fill(variable.value);
await inputField.blur();
// select the variable in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click();
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`);
});
test('can add a new interval variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'interval',
name: 'VariableUnderTest',
value: '1m',
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// enable the auto option
await page.getByText('Auto option').click();
// select the variable in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click();
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`);
// select the variable in the dashboard and set the Auto option
const variableDropdown = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.name!)
);
const nextElement = variableDropdown.locator('+ *');
await expect(nextElement).toHaveText('1m');
await nextElement.click();
await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'Auto' }).click();
// assert the panel is visible and has the correct "Auto" value
await expect(panelContent).toBeVisible();
await expect(markdownContent).toContainText('VariableUnderTest: 10m');
});
}
);
@@ -0,0 +1,120 @@
import { Page } from 'playwright-core';
import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'ed155665/annotation-filtering';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
// these tests require a larger viewport
viewport: { width: 1920, height: 1080 },
});
test.describe(
'Dashboard',
{
tag: ['@dashboards'],
},
() => {
test('can drag and drop panels', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?orgId=1` });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Move panel three to panel one position
await movePanel(dashboardPage, selectors, /^Panel three$/, /^Panel one$/);
// Verify panel three is now above panel one
const panel3Position = await getPanelPosition(dashboardPage, selectors, /^Panel three$/);
const panel1Position = await getPanelPosition(dashboardPage, selectors, /^Panel one$/);
expect(panel3Position?.y).toBeLessThan(panel1Position?.y || 0);
// Move panel two to panel three position
await movePanel(dashboardPage, selectors, /^Panel two$/, /^Panel three$/);
// Verify panel two is now above panel three
const panel2Position = await getPanelPosition(dashboardPage, selectors, /^Panel two$/);
const panel3PositionAfter = await getPanelPosition(dashboardPage, selectors, /^Panel three$/);
expect(panel2Position?.y).toBeLessThan(panel3PositionAfter?.y || 0);
});
// Note, moving a panel from a nested row to a parent row currently just deletes the panel
// This test will need to be updated once the correct behavior is implemented.
test('can move panel from nested row to parent row', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?orgId=1` });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await groupIntoRow(page, dashboardPage, selectors);
const firstRowElement = dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
.first();
const rowBoundingBox = await firstRowElement.boundingBox();
if (!rowBoundingBox) {
throw new Error('Row element not found');
}
const panel1Element = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^Panel one$/ });
await panel1Element.hover();
await page.mouse.down();
await page.mouse.move(rowBoundingBox.x, rowBoundingBox.y);
await page.mouse.up();
await expect(
dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^Panel one$/ })
).toBeHidden();
});
}
);
// Helper functions
async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click();
await page.getByText('Group into row').click();
}
async function getPanelPosition(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
panelTitle: string | RegExp
) {
const panel = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: panelTitle });
const boundingBox = await panel.boundingBox();
return boundingBox;
}
async function movePanel(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
sourcePanel: string | RegExp,
targetPanel: string | RegExp
) {
// Get target panel position
const targetPanelElement = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: targetPanel });
// Get source panel element
const sourcePanelElement = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: sourcePanel });
// Perform drag and drop
await sourcePanelElement.dragTo(targetPanelElement);
}
@@ -0,0 +1,409 @@
import { Page } from 'playwright-core';
import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e';
import testV2Dashboard from '../dashboards/TestV2Dashboard.json';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
test.describe(
'Dashboard Panel Layouts',
{
tag: ['@dashboards'],
},
() => {
test('can switch to auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Switch to auto grid');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await checkAutoGridLayoutInputs(dashboardPage, selectors);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await checkAutoGridLayoutInputs(dashboardPage, selectors);
});
test('can change min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set min column width');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
// Get initial positions - standard width should have panels on different rows
const firstPanelTop = await getPanelTop(dashboardPage, selectors);
const lastPanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).last();
const lastPanelBox = await lastPanel.boundingBox();
const lastPanelTop = lastPanelBox?.y || 0;
// Verify standard layout has panels on different rows
expect(lastPanelTop).toBeGreaterThan(firstPanelTop);
// Change to narrow min column width
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
.click();
await page.getByRole('option', { name: 'Narrow' }).click();
// Verify narrow layout has all panels on same row
const firstPanelTopNarrow = await getPanelTop(dashboardPage, selectors);
const lastPanelBoxNarrow = await lastPanel.boundingBox();
const lastPanelTopNarrow = lastPanelBoxNarrow?.y || 0;
expect(lastPanelTopNarrow).toBe(firstPanelTopNarrow);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth
)
).toHaveValue('Narrow');
const firstPanelTopReload = await getPanelTop(dashboardPage, selectors);
const lastPanelBoxReload = await lastPanel.boundingBox();
const lastPanelTopReload = lastPanelBoxReload?.y || 0;
expect(lastPanelTopReload).toBe(firstPanelTopReload);
});
test('can change to custom min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set custom min column width');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
.click();
await page.getByRole('option', { name: 'Custom' }).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth)
.fill('900');
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth)
.blur();
// Changing to 900 custom width should have each panel span the whole row (stacked vertically)
await verifyPanelsStackedVertically(dashboardPage, selectors);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await verifyPanelsStackedVertically(dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth
)
).toHaveValue('900');
await verifyPanelsStackedVertically(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomMinColumnWidth)
.click();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth
)
).toHaveValue('Standard');
});
test('can change max columns in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set max columns');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
.click();
await page.getByRole('option', { name: '1', exact: true }).click();
// Changing to 1 max column should have each panel span the whole row (stacked vertically)
await verifyPanelsStackedVertically(dashboardPage, selectors);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await verifyPanelsStackedVertically(dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
).toHaveValue('1');
await verifyPanelsStackedVertically(dashboardPage, selectors);
});
test('can change row height in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set row height');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
.click();
await page.getByRole('option', { name: 'Short' }).click();
const shortHeight = await getPanelHeight(dashboardPage, selectors);
expect(shortHeight).toBeLessThan(regularRowHeight);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
.click();
await page.getByRole('option', { name: 'Tall' }).click();
const tallHeight = await getPanelHeight(dashboardPage, selectors);
expect(tallHeight).toBeGreaterThan(regularRowHeight);
await saveDashboard(dashboardPage, selectors);
await page.reload();
const tallHeightAfterReload = await getPanelHeight(dashboardPage, selectors);
expect(tallHeightAfterReload).toBeGreaterThan(regularRowHeight);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
).toHaveValue('Tall');
const tallHeightAfterEdit = await getPanelHeight(dashboardPage, selectors);
expect(tallHeightAfterEdit).toBeGreaterThan(regularRowHeight);
});
test('can change to custom row height in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set custom row height');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
.click();
await page.getByRole('option', { name: 'Custom' }).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight)
.fill('800');
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight)
.blur();
const customHeight = await getPanelHeight(dashboardPage, selectors);
expect(customHeight).toBeCloseTo(800, 5); // Allow some tolerance for rendering differences
expect(customHeight).toBeGreaterThan(regularRowHeight);
await saveDashboard(dashboardPage, selectors);
await page.reload();
const customHeightAfterReload = await getPanelHeight(dashboardPage, selectors);
expect(customHeightAfterReload).toBeCloseTo(800, 5);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight
)
).toHaveValue('800');
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomRowHeight)
.click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
).toHaveValue('Standard');
});
test('can change fill screen in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set fill screen');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
// Set narrow column width first to ensure panels fit horizontally
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
.click();
await page.getByRole('option', { name: 'Narrow' }).click();
const initialHeight = await getPanelHeight(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen)
.click({ force: true });
const fillScreenHeight = await getPanelHeight(dashboardPage, selectors);
expect(fillScreenHeight).toBeGreaterThan(initialHeight);
await saveDashboard(dashboardPage, selectors);
await page.reload();
const fillScreenHeightAfterReload = await getPanelHeight(dashboardPage, selectors);
expect(fillScreenHeightAfterReload).toBeGreaterThan(initialHeight);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen)
).toBeChecked();
const fillScreenHeightAfterEdit = await getPanelHeight(dashboardPage, selectors);
expect(fillScreenHeightAfterEdit).toBeGreaterThan(initialHeight);
});
}
);
// Helper functions
async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string) {
await page.goto(selectors.pages.ImportDashboard.url);
await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard));
await page.getByTestId(selectors.components.DashboardImportPage.submit).click();
await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title);
await page.getByTestId(selectors.components.ImportDashboardForm.submit).click();
}
async function saveDashboard(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click();
}
async function checkAutoGridLayoutInputs(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen)
).toBeVisible();
}
async function verifyPanelsStackedVertically(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
const panels = await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).all();
let previousTop = 0;
for (const panel of panels) {
const boundingBox = await panel.boundingBox();
if (boundingBox) {
if (previousTop === 0) {
previousTop = boundingBox.y;
} else {
expect(boundingBox.y).toBeGreaterThan(previousTop);
previousTop = boundingBox.y;
}
}
}
}
async function getPanelHeight(dashboardPage: DashboardPage, selectors: E2ESelectorGroups): Promise<number> {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first();
const boundingBox = await panel.boundingBox();
return boundingBox?.height || 0;
}
async function getPanelTop(dashboardPage: DashboardPage, selectors: E2ESelectorGroups): Promise<number> {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first();
const boundingBox = await panel.boundingBox();
return boundingBox?.y || 0;
}
@@ -0,0 +1,66 @@
import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
test.describe(
'Dashboard panels',
{
tag: ['@dashboards'],
},
() => {
test('can remove a panel', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Remove Panel #1
await removePanelsByTitle(dashboardPage, selectors, ['Panel #1']);
// Check that panel has been deleted
await expect(
dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^Panel #1$/ })
).toBeHidden();
});
test('can remove several panels at once', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Remove multiple panels
await removePanelsByTitle(dashboardPage, selectors, ['Panel #1', 'Panel #2', 'Panel #3']);
// Check that panels have been deleted
await expect(
dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^Panel #[123]$/ })
).toBeHidden();
});
}
);
// Helper function to remove a panel by its title
async function removePanelsByTitle(dashboardPage: DashboardPage, selectors: E2ESelectorGroups, panelTitles: string[]) {
for (const panelTitle of panelTitles) {
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: panelTitle })
.click({
modifiers: ['Shift'],
});
}
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
}
@@ -0,0 +1,47 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'ed155665/annotation-filtering';
test.describe(
'Dashboard',
{
tag: ['@dashboards'],
},
() => {
test('can change dashboard description and title', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Check that current dashboard title is visible in breadcrumb
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('Annotation filtering'))
).toBeVisible();
const titleInput = page.locator('[aria-label="dashboard-options Title field property editor"] input');
await expect(titleInput).toHaveValue('Annotation filtering');
await titleInput.fill('New dashboard title');
await expect(titleInput).toHaveValue('New dashboard title');
// Check that new dashboard title is reflected in breadcrumb
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('New dashboard title'))
).toBeVisible();
// Check that we can successfully change the dashboard description
const descriptionTextArea = page.locator(
'[aria-label="dashboard-options Description field property editor"] textarea'
);
await descriptionTextArea.fill('Dashboard description');
await expect(descriptionTextArea).toHaveValue('Dashboard description');
});
}
);
@@ -0,0 +1,84 @@
import { DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
const deselectPanels = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => {
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls).click({
position: { x: 0, y: 0 },
});
};
export const flows = {
deselectPanels,
async changePanelTitle(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
oldPanelTitle: string,
newPanelTitle: string
) {
await deselectPanels(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: oldPanelTitle })
.first()
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'))
.fill(newPanelTitle);
},
async changePanelDescription(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
panelTitle: string,
newDescription: string
) {
await deselectPanels(dashboardPage, selectors);
const panelTitleRegex = new RegExp(`^${panelTitle}$`);
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: panelTitleRegex })
.first()
.click();
const descriptionTextArea = dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('panel-options Description'))
.locator('textarea');
await descriptionTextArea.fill(newDescription);
},
async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
.click();
},
async newEditPanelCommonVariableInputs(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
variable: Variable
) {
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.variableType(variable.type))
.click();
const variableNameInput = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.variableNameInput
);
await variableNameInput.click();
await variableNameInput.fill(variable.name);
await variableNameInput.blur();
if (variable.label) {
const variableLabelInput = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.variableLabelInput
);
await variableLabelInput.click();
await variableLabelInput.fill(variable.label);
await variableLabelInput.blur();
}
},
};
export type Variable = {
type: string;
name: string;
label?: string;
description?: string;
value: string;
};
@@ -0,0 +1,63 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Dashboard search',
{
tag: ['@dashboards-search'],
},
() => {
test.use({ viewport: { width: 1280, height: 1080 } });
test.beforeEach(async ({ page, selectors }) => {
await page.goto('/dashboards');
await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))).toBeVisible();
});
test('Search - Dashboards list', async ({ page, selectors }) => {
await toggleSearchView(page, selectors);
await assertResultsCount(page, 24);
});
test('Search - Filter by search input', async ({ page, selectors }) => {
await toggleSearchView(page, selectors);
await assertResultsCount(page, 24);
const searchInput = await page.getByTestId('input-wrapper');
await searchInput.click();
await page.keyboard.type('Datasource tests - MySQL');
await assertResultsCount(page, 2);
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('Backspace');
await page.keyboard.type('Datasource tests - MySQL (unittest)');
await assertResultsCount(page, 1);
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('Backspace');
await page.keyboard.type('- MySQL');
await assertResultsCount(page, 2);
});
}
);
async function assertResultsCount(page, length) {
const rowGroup = await page.getByRole('rowgroup');
await expect(rowGroup).toHaveCount(1);
const rows = await rowGroup.first().getByRole('row');
await expect(rows).toHaveCount(length);
}
async function toggleSearchView(page, selectors) {
const toggleButtons = await page.getByTestId(selectors.pages.Dashboards.toggleView);
await expect(toggleButtons).toHaveCount(2);
const listRadioButton = await toggleButtons.nth(1).locator('input');
await expect(listRadioButton).toBeChecked({ checked: false });
await listRadioButton.check();
await expect(listRadioButton).toBeChecked({ checked: true });
}
@@ -0,0 +1,126 @@
import { test, expect } from '@grafana/plugin-e2e';
import { makeNewDashboardRequestBody } from './utils/makeDashboard';
const NUM_ROOT_FOLDERS = 60;
const NUM_ROOT_DASHBOARDS = 60;
const NUM_NESTED_FOLDERS = 60;
const NUM_NESTED_DASHBOARDS = 60;
// TODO change this test so it doesn't conflict with the existing dashboard browse test
// probably needs a separate user
test.describe.skip(
'Dashboard browse (nested)',
{
tag: ['@dashboards'],
},
() => {
const dashboardUIDsToCleanUp: string[] = [];
const folderUIDsToCleanUp: string[] = [];
test.beforeAll(async ({ request }) => {
// Add root folders
for (let i = 0; i < NUM_ROOT_FOLDERS; i++) {
const response = await request.post('/api/folders', {
data: {
title: `Root folder ${i.toString().padStart(2, '0')}`,
},
});
const responseBody = await response.json();
folderUIDsToCleanUp.push(responseBody.uid);
}
// Add root dashboards
for (let i = 0; i < NUM_ROOT_DASHBOARDS; i++) {
const response = await request.post('/api/dashboards/db', {
data: makeNewDashboardRequestBody(`Root dashboard ${i.toString().padStart(2, '0')}`),
});
const responseBody = await response.json();
dashboardUIDsToCleanUp.push(responseBody.uid);
}
// Add folder with children
const folderResponse = await request.post('/api/folders', {
data: {
title: 'A root folder with children',
},
});
const folderResponseBody = await folderResponse.json();
const folderUid = folderResponseBody.uid;
folderUIDsToCleanUp.push(folderUid);
// Add nested folders
for (let i = 0; i < NUM_NESTED_FOLDERS; i++) {
await request.post('/api/folders', {
data: {
title: `Nested folder ${i.toString().padStart(2, '0')}`,
parentUid: folderUid,
},
headers: {
'Content-Type': 'application/json',
},
});
}
// Add nested dashboards
for (let i = 0; i < NUM_NESTED_DASHBOARDS; i++) {
await request.post('/api/dashboards/db', {
data: makeNewDashboardRequestBody(`Nested dashboard ${i.toString().padStart(2, '0')}`, folderUid),
headers: {
'Content-Type': 'application/json',
},
});
}
});
test.afterAll(async ({ request }) => {
// Clean up root dashboards
for (const dashboardUID of dashboardUIDsToCleanUp) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
// Clean up root folders (cascading delete will remove any nested folders and dashboards)
for (const folderUID of folderUIDsToCleanUp) {
await request.delete(`/api/folders/${folderUID}`, {
params: {
forceDeleteRules: false,
},
});
}
});
test('pagination works correctly for folders and root', async ({ page, selectors }) => {
// Navigate to dashboards page
await page.goto('/dashboards');
// Wait for and verify the root folder with children is visible
await expect(page.getByText('A root folder with children')).toBeVisible();
// Expand A root folder with children
await page.getByLabel('Expand folder A root folder with children').click();
await expect(page.getByText('Nested folder 00')).toBeVisible();
// Get the table body container for scrolling
const tableBody = page.getByTestId(selectors.pages.BrowseDashboards.table.body).locator('> div');
// Scroll the page and check visibility of next set of items
await tableBody.evaluate((el) => el.scrollTo(0, 2100));
await expect(page.getByText('Nested folder 59')).toBeVisible();
await expect(page.getByText('Nested dashboard 00')).toBeVisible();
// Scroll the page and check visibility of next set of items
await tableBody.evaluate((el) => el.scrollTo(0, 4200));
await expect(page.getByText('Nested dashboard 59')).toBeVisible();
await expect(page.getByText('Root folder 00')).toBeVisible();
// Scroll the page and check visibility of next set of items
await tableBody.evaluate((el) => el.scrollTo(0, 6300));
await expect(page.getByText('Root folder 59')).toBeVisible();
await expect(page.getByText('Root dashboard 00')).toBeVisible();
// Scroll the page and check visibility of next set of items
await tableBody.evaluate((el) => el.scrollTo(0, 8400));
await expect(page.getByText('Root dashboard 59')).toBeVisible();
});
}
);
@@ -0,0 +1,102 @@
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/TestDashboard.json';
test.describe(
'Dashboard browse',
{
tag: ['@dashboards'],
},
() => {
let dashboardUID: string;
test.beforeAll(async ({ request }) => {
// Import the test dashboard
const response = await request.post('/api/dashboards/import', {
data: {
dashboard: testDashboard,
folderUid: '',
overwrite: true,
inputs: [],
},
});
const responseBody = await response.json();
dashboardUID = responseBody.uid;
});
test.afterAll(async ({ request }) => {
// Clean up the imported dashboard
if (dashboardUID) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
});
test('Manage Dashboards tests', async ({ page, selectors }) => {
// Navigate to dashboards page
await page.goto('/dashboards');
// Folders and dashboards should be visible
await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))).toBeVisible();
await expect(
page.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard'))
).toBeVisible();
// gdev dashboards folder is collapsed - its content should not be visible
await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('Bar Gauge Demo'))).toBeHidden();
// should click a folder and see its children
await page
.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))
.getByLabel(/Expand folder/)
.click();
await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('Bar Gauge Demo'))).toBeVisible();
// Open the new folder drawer
await page.getByText('New').click();
await page.getByRole('menuitem', { name: 'New folder' }).click();
// And create a new folder
await page.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.nameInput).fill('My new folder');
await page
.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.form)
.getByRole('button', { name: 'Create' })
.click();
// await page.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.form).getByRole('button', { name: 'Create' }).click({ force: true });
// Verify success alert and close it
const alert = page.getByTestId(selectors.components.Alert.alertV2('success'));
await expect(alert).toBeVisible();
await alert.getByLabel('Close alert').click();
await expect(page.getByRole('heading', { name: 'My new folder' })).toBeVisible();
// Delete the folder and expect to go back to the root
await page.getByRole('button', { name: 'Folder actions' }).click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByPlaceholder('Type "Delete" to confirm').fill('Delete');
await page.getByTestId(selectors.pages.ConfirmModal.delete).click();
await expect(page.getByRole('heading', { name: 'Dashboards' })).toBeVisible();
// Can collapse the gdev folder and delete the dashboard we imported
await page
.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))
.getByLabel(/Collapse folder/)
.click();
// Select the imported dashboard using checkbox
await page
.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard'))
.getByRole('checkbox')
.click({ force: true });
// Delete the selected dashboard
await page.getByRole('button', { name: 'Delete' }).click();
// Confirm deletion in modal
await page.getByPlaceholder('Type "Delete" to confirm').fill('Delete');
await page.getByTestId(selectors.pages.ConfirmModal.delete).click();
await expect(
page.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard'))
).toBeHidden();
});
}
);
@@ -0,0 +1,66 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Export as JSON',
{
tag: ['@dashboards'],
},
() => {
test('Export for internal and external use', async ({ gotoDashboardPage, page, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: 'ZqZnVvFZz',
});
// Open the export drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.Menu.exportAsJson)
.click();
await expect(page).toHaveURL(/.*shareView=export.*/);
// Export as JSON
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.container)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle)
).toBeChecked({
checked: false,
});
await expect(dashboardPage.getByGrafanaSelector(selectors.components.CodeEditor.container)).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.saveToFileButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.cancelButton)
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton)
.click();
// TODO failing in CI - fix it
// let clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
// expect(clipboardContent).not.toContain('__inputs');
await dashboardPage
.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle)
.click({ force: true });
await dashboardPage
.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton)
.click();
// TODO failing in CI - fix it
// clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
// expect(clipboardContent).toContain('__inputs');
await dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.cancelButton).click();
await expect(page).not.toHaveURL(/.*shareView=export.*/);
});
}
);
@@ -0,0 +1,60 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Dashboard keybindings',
{
tag: ['@dashboards'],
},
() => {
// the "should collapse and expand all rows" test requires a larger viewport
// otherwise the final panel is not visible when everything is expanded
test.use({
viewport: { width: 1280, height: 1080 },
});
test('should collapse and expand all rows', async ({ gotoDashboardPage, page, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: 'Repeating-rows-uid/repeating-rows' });
const panelContents = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content);
await expect(panelContents).toHaveCount(5);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = A, pod = Bob'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = B, pod = Bob'))
).toBeVisible();
// Collapse all rows using keyboard shortcut: d + Shift+C
await page.keyboard.press('d');
await page.keyboard.press('Shift+C');
await expect(panelContents).toHaveCount(0);
await expect(page.getByText('server = A, pod = Bob')).toBeHidden();
await expect(page.getByText('server = B, pod = Bob')).toBeHidden();
// Expand all rows using keyboard shortcut: d + Shift+E
await page.keyboard.press('d');
await page.keyboard.press('Shift+E');
await expect(panelContents).toHaveCount(6);
await expect(page.getByText('server = A, pod = Bob')).toBeVisible();
await expect(page.getByText('server = B, pod = Bob')).toBeVisible();
});
test('should open panel inspect', async ({ gotoDashboardPage, page, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: 'edediimbjhdz4b/a-tall-dashboard' });
// Find Panel #1 and press 'i' to open inspector
const panel1 = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1'));
await expect(panel1).toBeVisible();
await panel1.press('i');
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible();
// Press Escape to close inspector
await page.keyboard.press('Escape');
await expect(page.getByTestId(selectors.components.PanelInspector.Json.content)).toBeHidden();
});
}
);
@@ -0,0 +1,65 @@
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json';
test.describe(
'Dashboard with data links that have no slug',
{
tag: ['@dashboards'],
},
() => {
let dashboardUID: string;
test.beforeAll(async ({ request }) => {
// Import the test dashboard
const response = await request.post('/api/dashboards/import', {
data: {
dashboard: testDashboard,
folderUid: '',
overwrite: true,
inputs: [],
},
});
const responseBody = await response.json();
dashboardUID = responseBody.uid;
});
test.afterAll(async ({ request }) => {
// Clean up the imported dashboard
if (dashboardUID) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
});
test('Should not reload if linking to same dashboard', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: dashboardUID });
const panel = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title('Data links without slug')
);
await expect(panel).toBeVisible();
const urlShouldContain = '/d/data-link-no-slug/data-link-without-slug-test';
await dashboardPage
.getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink)
.getByText(/9yy21uzzxypg/)
.click();
await expect(page.getByText(/Loading/)).toBeHidden();
await expect(page).toHaveURL(new RegExp(urlShouldContain));
await dashboardPage
.getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink)
.getByText(/dr199bpvpcru/)
.click();
await expect(page.getByText(/Loading/)).toBeHidden();
await expect(page).toHaveURL(new RegExp(urlShouldContain));
await dashboardPage
.getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink)
.getByText(/dre33fzyxcrz/)
.click();
await expect(page.getByText(/Loading/)).toBeHidden();
await expect(page).toHaveURL(new RegExp(urlShouldContain));
});
}
);
@@ -0,0 +1,48 @@
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/DashboardLiveTest.json';
test.describe(
'Dashboard Live streaming support',
{
tag: ['@dashboards'],
},
() => {
let dashboardUID: string;
test.beforeAll(async ({ request }) => {
// Import the test dashboard
const response = await request.post('/api/dashboards/import', {
data: {
dashboard: testDashboard,
folderUid: '',
overwrite: true,
inputs: [],
},
});
const responseBody = await response.json();
dashboardUID = responseBody.uid;
});
test.afterAll(async ({ request }) => {
// Clean up the imported dashboard
if (dashboardUID) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
});
test('Should receive streaming data', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: dashboardUID });
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Live'))).toBeVisible();
await expect
.poll(
async () =>
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body)
.getByRole('row')
.count()
)
.toBeGreaterThan(5);
});
}
);
@@ -0,0 +1,211 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
newDashboardSharingComponent: false,
},
});
test.describe(
'Public dashboards',
{
tag: ['@dashboards'],
},
() => {
test('Create, open and disable a public dashboard', async ({ page, gotoDashboardPage, selectors, request }) => {
// Navigate to dashboard without template variables
let dashboardPage = await gotoDashboardPage({ uid: 'ZqZnVvFZz' });
// Open sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select public dashboards tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click();
// Create button should be disabled
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
).toBeDisabled();
// Create flow shouldn't show these elements
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton)
).toBeHidden();
// Acknowledge checkboxes
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox)
.click({ force: true });
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox)
.click({ force: true });
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox)
.click({ force: true });
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
).toBeEnabled();
// Create public dashboard
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
.click();
// These elements shouldn't be rendered after creating public dashboard
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
).toBeHidden();
// These elements should be rendered
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown)
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown)
.click();
// These elements should be rendered once the Settings dropdown is opened
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch)
).toBeVisible();
// Close the sharing modal
await page.getByRole('button', { name: 'Close', exact: true }).click();
// Tag indicating a dashboard is public
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.publicDashboardTag)
).toBeVisible();
// Open sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select public dashboards tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown)
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown)
.click();
// These elements should be rendered once the Settings dropdown is opened
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch)
).toBeVisible();
// Make a request to public dashboards api endpoint without authentication
let copyUrlInput = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput
);
let url = await copyUrlInput.inputValue();
let publicDashboardApiUrl = getPublicDashboardAPIUrl(url);
// Create a new context without authentication
let response = await request.get(publicDashboardApiUrl);
expect(response.status()).toBe(200);
// Close the sharing modal
await page.getByRole('button', { name: 'Close', exact: true }).click();
// Navigate to dashboard without template variables
dashboardPage = await gotoDashboardPage({ uid: 'ZqZnVvFZz' });
// Open sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select public dashboards tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click();
// Save url before disabling public dashboard
copyUrlInput = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput
);
url = await copyUrlInput.inputValue();
// Switch off enabling toggle
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeEnabled();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
.click({ force: true });
// Url should be disabled
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput)
).toBeDisabled();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton)
).toBeDisabled();
// Make a request to public dashboards api endpoint without authentication
publicDashboardApiUrl = getPublicDashboardAPIUrl(url);
response = await request.get(publicDashboardApiUrl);
expect(response.status()).toBe(403);
});
}
);
const getPublicDashboardAPIUrl = (url: string): string => {
const accessToken = url.split('/').pop();
return `/api/public/dashboards/${accessToken}`;
};
@@ -0,0 +1,56 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
newDashboardSharingComponent: false,
},
});
test.describe(
'Public dashboard with template variables',
{
tag: ['@dashboards'],
},
() => {
test('Create a public dashboard with template variables shows a template variable warning', async ({
gotoDashboardPage,
selectors,
}) => {
// Navigate to dashboard with template variables
const dashboardPage = await gotoDashboardPage({
uid: 'HYaGDGIMk',
});
// Open sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select public dashboards tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click();
// Warning Alert dashboard cannot be made public because it has template variables
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert
)
).toBeVisible();
// Configuration elements for public dashboards should exist
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeHidden();
});
}
);
@@ -0,0 +1,191 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
scenes: true,
newDashboardSharingComponent: true,
},
});
const DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8';
const DASHBOARD_UID_2 = 'edediimbjhdz4b';
test.describe(
'Shared dashboards',
{
tag: ['@dashboards'],
},
() => {
test('Close share externally drawer', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID });
// Open share externally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareExternally)
.click();
await expect(page).toHaveURL(/.*shareView=public_dashboard.*/);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.container)
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.cancelButton)
.click();
await expect(page).not.toHaveURL(/.*shareView=public_dashboard.*/);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.container)
).toBeHidden();
});
test('Create and disable a shared dashboard and check API', async ({
page,
gotoDashboardPage,
selectors,
request,
}) => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID_2 });
// Open share externally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareExternally)
.click();
// Create button should be disabled
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton
)
).toBeDisabled();
// Create flow shouldn't show these elements
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton
)
).toBeHidden();
// Acknowledge checkbox
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox
)
).toBeEnabled();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox)
.click({ force: true });
// Create shared dashboard
const createResponse = page.waitForResponse(
(response) =>
response.url().includes(`/api/dashboards/uid/${DASHBOARD_UID_2}/public-dashboards`) &&
response.request().method() === 'POST'
);
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton
)
).toBeEnabled();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton)
.click();
let response = await createResponse;
let publicDashboard = await response.json();
// Test API access with the created dashboard
let apiResponse = await request.get(`/api/public/dashboards/${publicDashboard.accessToken}`);
expect(apiResponse.status()).toBe(200);
// These elements shouldn't be rendered after creating public dashboard
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton
)
).toBeHidden();
// These elements should be rendered
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton
)
).toBeVisible();
// Switch off enabling toggle
const updateResponse = page.waitForResponse(
(response) =>
response.url().includes(`/api/dashboards/uid/${DASHBOARD_UID_2}/public-dashboards/`) &&
response.request().method() === 'PATCH'
);
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton
)
).toBeEnabled();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton)
.click({ force: true });
response = await updateResponse;
expect(response.status()).toBe(200);
publicDashboard = await response.json();
// Test that API access is now forbidden
apiResponse = await request.get(`/api/public/dashboards/${publicDashboard.accessToken}`);
expect(apiResponse.status()).toBe(403);
});
}
);
@@ -0,0 +1,175 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
scenes: true,
newDashboardSharingComponent: true,
},
});
const DASHBOARD_UID = 'ZqZnVvFZz';
test.describe(
'Share internally',
{
tag: ['@dashboards'],
},
() => {
test.beforeEach(async ({ page }) => {
// Clear localStorage before each test
await page.evaluate(() => {
window.localStorage.removeItem('grafana.dashboard.link.shareConfiguration');
});
});
test('Create a locked time range short link', async ({ page, gotoDashboardPage, selectors }) => {
// Navigate to dashboard with specific time range
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }),
});
// Open share internally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
const createResponse = page.waitForResponse(
(response) => response.url().includes('/api/short-urls') && response.request().method() === 'POST'
);
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally)
.click();
await expect(page).toHaveURL(/.*shareView=link.*/);
// Check that the required elements exist
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.copyUrlButton)
).toBeVisible();
// Check radio buttons
const radioButtons = dashboardPage.getByGrafanaSelector(selectors.components.RadioButton.container);
await expect(radioButtons).toHaveCount(3);
// Check localStorage is initially null
const initialConfig = await page.evaluate(() => {
return window.localStorage.getItem('grafana.dashboard.link.shareConfiguration');
});
expect(initialConfig).toBeNull();
// Wait for the short URL creation
const response = await createResponse;
expect(response.status()).toBe(200);
const responseBody = await response.json();
expect(responseBody.url).toContain('goto');
});
test('Create a relative time range short link', async ({ page, gotoDashboardPage, selectors }) => {
// Navigate to dashboard with specific time range
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }),
});
// Open share internally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally)
.click();
const updateResponse = page.waitForResponse(
(response) => response.url().includes('/api/short-urls') && response.request().method() === 'POST'
);
// Toggle the lock time range switch
const lockTimeRangeSwitch = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch
);
await expect(lockTimeRangeSwitch).toBeInViewport();
await expect(async () => {
await lockTimeRangeSwitch.uncheck({ force: true });
}).toPass();
await expect(async () => {
// Check localStorage configuration
const shareConfig = await page.evaluate(() => {
const config = window.localStorage.getItem('grafana.dashboard.link.shareConfiguration');
return config ? JSON.parse(config) : null;
});
expect(shareConfig).not.toBeNull();
expect(shareConfig.useAbsoluteTimeRange).toBe(false);
expect(shareConfig.useShortUrl).toBe(true);
expect(shareConfig.theme).toBe('current');
}).toPass();
// Wait for the API response
const response = await updateResponse;
expect(response.status()).toBe(200);
const responseBody = await response.json();
expect(responseBody.url).toContain('goto');
});
test('Share button gets configured link', async ({ page, gotoDashboardPage, selectors }) => {
// Navigate to dashboard with specific time range
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }),
});
// Open share internally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally)
.click();
// Check localStorage is initially null
const initialConfig = await page.evaluate(() => {
return window.localStorage.getItem('grafana.dashboard.link.shareConfiguration');
});
expect(initialConfig).toBeNull();
// Configure the sharing options
const lockTimeRangeSwitch = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch
);
await expect(lockTimeRangeSwitch).toBeInViewport();
await expect(async () => {
await lockTimeRangeSwitch.uncheck({ force: true });
}).toPass();
const shortenUrlSwitch = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch
);
await expect(shortenUrlSwitch).toBeInViewport();
await expect(async () => {
await shortenUrlSwitch.uncheck({ force: true });
}).toPass();
// Close the drawer
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click();
await expect(page).not.toHaveURL(/.*shareView=link.*/);
await expect(async () => {
// Check that localStorage has been updated with the configuration
const finalConfig = await page.evaluate(() => {
const config = window.localStorage.getItem('grafana.dashboard.link.shareConfiguration');
return config ? JSON.parse(config) : null;
});
expect(finalConfig).not.toBeNull();
expect(finalConfig.useAbsoluteTimeRange).toBe(false);
expect(finalConfig.useShortUrl).toBe(false);
expect(finalConfig.theme).toBe('current');
}).toPass();
});
}
);
@@ -0,0 +1,69 @@
import { test, expect } from '@grafana/plugin-e2e';
import { SnapshotCreateResponse } from '../../public/app/features/dashboard/services/SnapshotSrv';
test.use({
featureToggles: {
scenes: true,
newDashboardSharingComponent: true,
},
});
const DASHBOARD_UID = 'ZqZnVvFZz';
test.describe(
'Snapshots',
{
tag: ['@dashboards'],
},
() => {
test('Create a snapshot dashboard', async ({ page, gotoDashboardPage, selectors }) => {
// Opening a dashboard
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID });
const panelsToCheck = [
'Raw Data Graph',
'Last non-null',
'min',
'Max',
'The data from graph above with seriesToColumns transform',
];
// Open the sharing drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareSnapshot)
.click();
// Publish snapshot
const createSnapshotPromise = page.waitForResponse(
(response) => response.url().includes('/api/snapshots') && response.request().method() === 'POST'
);
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot)
.click();
const createResponse = await createSnapshotPromise;
expect(createResponse.status()).toBe(200);
const responseBody: SnapshotCreateResponse = await createResponse.json();
// Navigate to the snapshot URL
const snapshotUrl = getSnapshotUrl(responseBody.key);
await page.goto(snapshotUrl);
// Validate the dashboard controls are rendered
await expect(dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls)).toBeVisible();
// Validate the panels are rendered
for (const title of panelsToCheck) {
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title))).toBeVisible();
}
});
}
);
const getSnapshotUrl = (uid: string): string => {
return `/dashboard/snapshot/${uid}`;
};
@@ -0,0 +1,74 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_UID = 'HYaGDGIMk';
test.use({
timezoneId: 'Pacific/Easter',
});
test.describe(
'Dashboard templating',
{
tag: ['@dashboards'],
},
() => {
test('Verify variable interpolation works', async ({ page, gotoDashboardPage }) => {
// Open dashboard global variables and interpolation
await gotoDashboardPage({ uid: DASHBOARD_UID });
// Get the actual timezone from the browser context (should be Pacific/Easter due to test.use)
const timeZone = await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
const example = `Example: from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}`;
const expectedItems: string[] = [
'__dashboard = Templating - Global variables and interpolation',
'__dashboard.name = Templating - Global variables and interpolation',
'__dashboard.uid = HYaGDGIMk',
'__org.name = Main Org.',
'__org.id = 1',
'__user.id = 1',
'__user.login = admin',
'__user.email = admin@localhost',
`Server:raw = A'A"A,BB\\B,CCC`,
`Server:regex = (A'A"A|BB\\\\B|CCC)`,
`Server:lucene = ("A'A\\"A" OR "BB\\\\B" OR "CCC")`,
`Server:glob = {A'A"A,BB\\B,CCC}`,
`Server:pipe = A'A"A|BB\\B|CCC`,
`Server:distributed = A'A"A,Server=BB\\B,Server=CCC`,
`Server:csv = A'A"A,BB\\B,CCC`,
`Server:html = A&#39;A&quot;A, BB\\B, CCC`,
`Server:json = ["A'A\\"A","BB\\\\B","CCC"]`,
`Server:percentencode = %7BA%27A%22A%2CBB%5CB%2CCCC%7D`,
`Server:singlequote = 'A\\'A"A','BB\\B','CCC'`,
`Server:doublequote = "A'A\\"A","BB\\B","CCC"`,
`Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`,
`Server:date = NaN`,
`Server:text = All`,
`Server:queryparam = var-Server=$__all`,
`1 < 2`,
example,
];
// Get all list items from the markdown content
const listItems = page.locator('.markdown-html li');
// Verify we have the expected number of items
await expect(listItems).toHaveCount(26);
// Get all the text content from list items
const actualItems = await listItems.allTextContents();
// Compare each expected item with actual items
for (let i = 0; i < expectedItems.length; i++) {
expect(actualItems[i]).toBe(expectedItems[i]);
}
// Check link interpolation is working correctly
const exampleLink = page.locator(`a:has-text("${example}")`);
await expect(exampleLink).toHaveAttribute(
'href',
`https://example.com/?from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}`
);
});
}
);
@@ -0,0 +1,97 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_UID = '5SdHCasdf';
const USER = 'dashboard-timepicker-test';
const PASSWORD = 'dashboard-timepicker-test';
// Separate user to isolate changes from other tests
test.use({
user: {
user: USER,
password: PASSWORD,
},
storageState: {
cookies: [],
origins: [],
},
});
test.describe(
'Dashboard timepicker',
{
tag: ['@dashboards'],
},
() => {
test('Shows the correct calendar days with custom timezone set via preferences', async ({
createUser,
page,
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
await createUser();
// login manually for now
await page.getByTestId(selectors.pages.Login.username).fill(USER);
await page.getByTestId(selectors.pages.Login.password).fill(PASSWORD);
await page.getByTestId(selectors.pages.Login.submit).click();
await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible();
// Set user preferences for timezone
await page.goto('/profile');
await page.getByTestId(selectors.components.TimeZonePicker.containerV2).click();
await page.getByRole('option', { name: 'Asia/Tokyo' }).click();
await page.getByTestId(selectors.components.UserProfile.preferencesSaveButton).click();
// Open dashboard with time range from 8th to end of 10th.
// Will be Tokyo time because of above preference
await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({
timezone: 'Default',
}),
});
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField).fill('2022-06-08 00:00:00');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2022-06-10 23:59:59');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.calendar.openButton).first().click();
// Assert that the calendar shows 08 and 09 and 10 as selected days
const activeTiles = page.locator('.react-calendar__tile--active, .react-calendar__tile--hasActive');
await expect(activeTiles).toHaveCount(3);
});
test('Shows the correct calendar days with custom timezone set via time picker', async ({
createUser,
page,
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
await createUser();
// login manually for now
await page.getByTestId(selectors.pages.Login.username).fill(USER);
await page.getByTestId(selectors.pages.Login.password).fill(PASSWORD);
await page.getByTestId(selectors.pages.Login.submit).click();
await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible();
// Open dashboard with time range from 2022-06-08 00:00:00 to 2022-06-10 23:59:59 in Tokyo time
await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({
timezone: 'Asia/Tokyo',
}),
});
// Open dashboard with time range from 8th to end of 10th.
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField).fill('2022-06-08 00:00:00');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2022-06-10 23:59:59');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.calendar.openButton).first().click();
// Assert that the calendar shows 08 and 09 and 10 as selected days
const activeTiles = page.locator('.react-calendar__tile--active, .react-calendar__tile--hasActive');
await expect(activeTiles).toHaveCount(3);
});
}
);
@@ -0,0 +1,26 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Embedded dashboard',
{
tag: ['@dashboards'],
},
() => {
test('open test page', async ({ page, dashboardPage, selectors }) => {
await page.goto('/dashboards/embedding-test');
// Verify pie charts are rendered
const pieChartSlices = page.locator(
`[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]`
);
await expect(pieChartSlices).toHaveCount(5);
// Verify no url sync
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click();
await page.locator('label').filter({ hasText: 'Last 1 hour' }).click();
// Verify URL remains the same (no sync)
expect(page.url()).toMatch(/\/dashboards\/embedding-test$/);
});
}
);
@@ -0,0 +1,47 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
test.describe(
'Dashboards',
{
tag: ['@dashboards'],
},
() => {
test('should restore scroll position', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
// Verify first panel is visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1'))
).toBeVisible();
// Scroll to the bottom
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
// The last panel should be visible...
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #50'))
).toBeVisible();
// Then we open and close the panel editor
// Click on panel menu (it only shows on hover)
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.menu('Panel #50'))
.click({ force: true });
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')).click();
// Go back to dashboard
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
// The last panel should still be visible!
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #50'))
).toBeVisible();
});
}
);
@@ -0,0 +1,63 @@
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/TestDashboard.json';
test.describe(
'Import Dashboards Test',
{
tag: ['@dashboards'],
},
() => {
let dashboardUID: string;
test('Ensure you can import a number of json test dashboards from a specific test directory', async ({
page,
dashboardPage,
selectors,
}) => {
await page.goto('/dashboard/import');
// Fill in the dashboard JSON and name
const textarea = dashboardPage.getByGrafanaSelector(selectors.components.DashboardImportPage.textarea);
await textarea.fill(JSON.stringify(testDashboard));
// Submit the JSON
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardImportPage.submit).click();
// Fill in the dashboard name and submit
const nameField = dashboardPage.getByGrafanaSelector(selectors.components.ImportDashboardForm.name);
await expect(nameField).toBeVisible();
await nameField.click();
await nameField.clear();
await nameField.fill(testDashboard.title);
await dashboardPage.getByGrafanaSelector(selectors.components.ImportDashboardForm.submit).click();
// Wait for dashboard to load and extract UID from URL
await page.waitForURL('**/d/**');
const url = page.url();
const urlParts = url.split('/d/');
if (urlParts.length > 1) {
dashboardUID = urlParts[1].split('/')[0];
}
// Verify the dashboard title is present in the breadcrumbs
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb(testDashboard.title))
).toBeVisible();
// Verify that specific panels from the test dashboard are loaded
await expect(page.getByText('Gauge Example')).toBeVisible();
await expect(page.getByText('Stat')).toBeVisible();
await expect(page.getByText('Time series example')).toBeVisible();
});
test.afterEach(async ({ request }) => {
// Clean up the imported dashboard using API
if (dashboardUID) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
dashboardUID = '';
}
});
}
);
@@ -0,0 +1,300 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
test.describe(
'Variables - Load options from Url',
{
tag: ['@dashboards'],
},
() => {
test('default options should be correct', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
// Test first variable (A)
const firstVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A'))
.locator('input');
await expect(firstVariableInput).toBeVisible();
await firstVariableInput.click();
// Check dropdown has 10 options
const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(firstDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Test second variable (AA)
const secondVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA'))
.locator('input');
await expect(secondVariableInput).toBeVisible();
await secondVariableInput.click();
// Check dropdown has 10 options
const secondDropdownOptions = dashboardPage
.getByGrafanaSelector(selectors.components.Select.option)
.locator('..');
await expect(secondDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Test third variable ($__all)
const thirdVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all'))
.locator('input');
await expect(thirdVariableInput).toBeVisible();
await thirdVariableInput.click();
// Check dropdown has 10 options
const thirdDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(thirdDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAC'))
).toBeVisible();
});
test('options set in url should load correct options', async ({
page,
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({
orgId: '1',
'var-datacenter': 'B',
'var-server': 'BB',
'var-pod': 'BBB',
}),
});
// Test first variable (B) - should be selected from URL
const firstVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B'))
.locator('input');
await expect(firstVariableInput).toBeVisible();
await firstVariableInput.click();
// Check dropdown has 10 options
const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(firstDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Test second variable (BB) - should be selected from URL
const secondVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB'))
.locator('input');
await expect(secondVariableInput).toBeVisible();
await secondVariableInput.click();
// Check dropdown has 10 options
const secondDropdownOptions = dashboardPage
.getByGrafanaSelector(selectors.components.Select.option)
.locator('..');
await expect(secondDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Test third variable (BBB) - should be selected from URL
const thirdVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB'))
.locator('input');
await expect(thirdVariableInput).toBeVisible();
await thirdVariableInput.click();
// Check dropdown has 10 options
const thirdDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(thirdDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC'))
).toBeVisible();
});
test('options set in url that do not exist should load correct options', async ({
page,
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
// Handle uncaught exceptions that are expected
page.on('pageerror', (error) => {
if (error.message.includes("Couldn't find any field of type string in the results.")) {
// This error is expected and should not fail the test
return;
}
throw error;
});
await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({
orgId: '1',
'var-datacenter': 'X',
}),
});
// Test first variable (X) - invalid value from URL
const firstVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X'))
.locator('input');
await expect(firstVariableInput).toBeVisible();
await firstVariableInput.click();
// Check dropdown has 10 options
const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(firstDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible (should fall back to default options)
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click();
// Check that second variable shows $__all (should be 2 instances)
const allVariables = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')
);
await expect(allVariables).toHaveCount(2);
await expect(allVariables.first()).toBeVisible();
await expect(allVariables.last()).toBeVisible();
});
}
);
@@ -0,0 +1,65 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Variables - Constant',
{
tag: ['@dashboards'],
},
() => {
test('can add a new constant variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Constant" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
// Select "Constant" type from dropdown
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Constant');
await typeSelect.locator('input').press('Enter');
// Set variable name
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill('VariableUnderTest');
// Set constant value
const constantInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2
);
await constantInput.fill('pesto');
// Set label
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('Variable under test');
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click();
// Assert the variable was rendered in the markdown content
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: pesto');
// Assert the variable is not visible in the dashboard navigation (constant variables are hidden)
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test'))
).toBeHidden();
});
}
);
@@ -0,0 +1,121 @@
import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
async function fillInCustomVariable(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
name: string,
label: string,
value: string
) {
// Select "Custom" type from dropdown
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Custom');
await typeSelect.locator('input').press('Enter');
// Set variable name
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill(name);
// Set label
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill(label);
// Set custom values
const customValueInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
);
await customValueInput.fill(value);
await customValueInput.blur();
}
async function assertPreviewValues(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
expectedValues: string[]
) {
for (let i = 0; i < expectedValues.length; i++) {
const previewOption = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption)
.nth(i);
await expect(previewOption).toHaveText(expectedValues[i]);
}
}
test.describe(
'Variables - Custom',
{
tag: ['@dashboards'],
},
() => {
test('can add a custom template variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Custom" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
await fillInCustomVariable(dashboardPage, selectors, 'VariableUnderTest', 'Variable under test', 'one,two,three');
await assertPreviewValues(dashboardPage, selectors, ['one', 'two', 'three']);
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one'))
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'two' }).click();
// Assert it was rendered
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: two');
});
test('can add a custom template variable with labels', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Custom" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
await fillInCustomVariable(
dashboardPage,
selectors,
'VariableUnderTest',
'Variable under test',
'One : 1,Two : 2, Three : 3'
);
await assertPreviewValues(dashboardPage, selectors, ['One', 'Two', 'Three']);
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1'))
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'Two' }).click();
// Assert it was rendered (the value "2" should be displayed, not the label "Two")
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: 2');
});
}
);
@@ -0,0 +1,74 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Variables - Datasource',
{
tag: ['@dashboards'],
},
() => {
test('can add a new datasource variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Datasource" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Data source');
await typeSelect.locator('input').press('Enter');
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill('VariableUnderTest');
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('Variable under test');
// If this is failing, make sure there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus"
// Or update to match available gdev datasources for testing
const datasourceSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect
);
await datasourceSelect.locator('input').fill('Prometheus');
await datasourceSelect.locator('input').press('Enter');
const previewOptions = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption
);
await expect(previewOptions.first()).toContainText('gdev-prometheus');
await expect(previewOptions.last()).toContainText('gdev-slow-prometheus');
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click();
// Change the selected variable value
await dashboardPage
.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus')
)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.Select.option)
.filter({ hasText: 'gdev-slow-prometheus' })
.click();
// Assert it was rendered
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: gdev-slow-prometheus-uid');
await expect(page.locator('.markdown-html').nth(1)).toContainText('VariableUnderTestText: gdev-slow-prometheus');
});
}
);
@@ -0,0 +1,84 @@
import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
async function assertPreviewValues(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
expectedValues: string[]
) {
for (let i = 0; i < expectedValues.length; i++) {
const previewOption = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption)
.nth(i);
await expect(previewOption).toHaveText(expectedValues[i]);
}
}
test.describe(
'Variables - Interval',
{
tag: ['@dashboards'],
},
() => {
test('can add a new interval variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Interval" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Interval');
await typeSelect.locator('input').press('Enter');
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill('VariableUnderTest');
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('Variable under test');
// Set interval values
const intervalInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput
);
await intervalInput.fill('10s,10m,60m,90m,1h30m');
await intervalInput.blur();
await assertPreviewValues(dashboardPage, selectors, ['10s', '10m', '60m', '90m', '1h30m']);
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click();
// Verify the variable label and initial value, then change the selected value
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test')
);
// Find the value element (next sibling) and verify it shows the default value
const variableValue = variableLabel.locator('+ div');
await expect(variableValue).toHaveText('10s');
// Click to open the dropdown
await variableValue.click();
// Select '1h30m' from the dropdown
await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: '1h30m' }).click();
// Assert it was rendered
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: 1h30m');
});
}
);

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