Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -8,6 +8,7 @@ updates:
|
||||
directories:
|
||||
- "/"
|
||||
- "/apps/playlist"
|
||||
- "/apps/secret"
|
||||
- "/apps/investigations"
|
||||
- "/pkg/aggregator"
|
||||
- "/pkg/apimachinery"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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/*'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
version: v1
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
lint:
|
||||
use:
|
||||
- DEFAULT
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -0,0 +1,4 @@
|
||||
module: "github.com/grafana/grafana/apps/secret/kinds"
|
||||
language: {
|
||||
version: "v0.9.0"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package secret
|
||||
|
||||
manifest: {
|
||||
appName: "secret"
|
||||
groupOverride: "secret.grafana.app"
|
||||
kinds: [
|
||||
securevalue,
|
||||
keeper,
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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'A"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
Reference in New Issue
Block a user