diff --git a/.betterer.eslint.config.js b/.betterer.eslint.config.js index bbd5842a3c8..3b44630965e 100644 --- a/.betterer.eslint.config.js +++ b/.betterer.eslint.config.js @@ -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', diff --git a/.drone.yml b/.drone.yml index 9778f442148..bf621c24a1e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec932f79bf3..1c05b89a30f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -400,8 +400,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 +479,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 @@ -770,6 +773,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 +821,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 diff --git a/.github/actions/check-jobs/action.yml b/.github/actions/check-jobs/action.yml new file mode 100644 index 00000000000..b4551730b96 --- /dev/null +++ b/.github/actions/check-jobs/action.yml @@ -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" diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index ec74716f7e3..5e7d116d0c2 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -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" diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 06a644f5843..2c43ae43de4 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -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 diff --git a/.github/workflows/run-dashboard-search-e2e.yml b/.github/workflows/run-dashboard-search-e2e.yml index 8554f7549a6..6eebaf9320a 100644 --- a/.github/workflows/run-dashboard-search-e2e.yml +++ b/.github/workflows/run-dashboard-search-e2e.yml @@ -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 diff --git a/.github/workflows/storybook-verification-playwright.yml b/.github/workflows/storybook-verification-playwright.yml new file mode 100644 index 00000000000..e8391259f55 --- /dev/null +++ b/.github/workflows/storybook-verification-playwright.yml @@ -0,0 +1,45 @@ +name: Verify Storybook (Playwright) + +on: + pull_request: + paths: + - 'packages/grafana-ui/**' + - '!docs/**' + - '!*.md' + push: + branches: + - main + paths: + - 'packages/grafana-ui/**' + - '!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 diff --git a/.gitignore b/.gitignore index ccc42239a87..7457c8148b3 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/contribute/style-guides/e2e-plugins.md b/contribute/style-guides/e2e-plugins.md index e708281d675..8d6ffaca61a 100644 --- a/contribute/style-guides/e2e-plugins.md +++ b/contribute/style-guides/e2e-plugins.md @@ -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. diff --git a/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts b/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts new file mode 100644 index 00000000000..29cc753df75 --- /dev/null +++ b/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts @@ -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, + 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(); + } + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts new file mode 100644 index 00000000000..f42082587d8 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts @@ -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 + ); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts new file mode 100644 index 00000000000..80f94f855b2 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -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); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts new file mode 100644 index 00000000000..c2214121d85 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts new file mode 100644 index 00000000000..d0b4cb5fcaf --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts new file mode 100644 index 00000000000..01a3e65db22 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts @@ -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]}"`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts new file mode 100644 index 00000000000..14af22ab2c3 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts @@ -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}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts new file mode 100644 index 00000000000..e964414dcad --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts @@ -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]}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts new file mode 100644 index 00000000000..30373362528 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts @@ -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`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts new file mode 100644 index 00000000000..d483b7b8005 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts @@ -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\)/); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts new file mode 100644 index 00000000000..6c55988a309 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts @@ -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}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts new file mode 100644 index 00000000000..411d2122954 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts @@ -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'); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts new file mode 100644 index 00000000000..b376c6f6af2 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts @@ -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); +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts new file mode 100644 index 00000000000..2cdb847a03f --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -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 { + 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 { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first(); + const boundingBox = await panel.boundingBox(); + return boundingBox?.y || 0; +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts new file mode 100644 index 00000000000..5b5202f1455 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts @@ -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(); +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts new file mode 100644 index 00000000000..87b8fc6ff3b --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts @@ -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'); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts new file mode 100644 index 00000000000..1ef85b85261 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -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; +}; diff --git a/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts b/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts new file mode 100644 index 00000000000..742aa106336 --- /dev/null +++ b/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts @@ -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 }); +} diff --git a/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts new file mode 100644 index 00000000000..7f88e462deb --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts new file mode 100644 index 00000000000..98106773c2d --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts new file mode 100644 index 00000000000..d5a3f4ecac3 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts @@ -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.*/); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts new file mode 100644 index 00000000000..893afe13f09 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts new file mode 100644 index 00000000000..89a03671f5a --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts @@ -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)); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts new file mode 100644 index 00000000000..79ae94c5e96 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts @@ -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); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts new file mode 100644 index 00000000000..3263b4f3011 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts @@ -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}`; +}; diff --git a/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts new file mode 100644 index 00000000000..60b928c1301 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts new file mode 100644 index 00000000000..0d991c100e1 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -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); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts new file mode 100644 index 00000000000..d250bb73464 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts new file mode 100644 index 00000000000..a3390122e92 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts @@ -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}`; +}; diff --git a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts new file mode 100644 index 00000000000..b31fecf230f --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts @@ -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)}` + ); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts new file mode 100644 index 00000000000..ac505aa9cd4 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts @@ -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); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts new file mode 100644 index 00000000000..b6479d8007c --- /dev/null +++ b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts @@ -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$/); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/general-dashboards.spec.ts b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts new file mode 100644 index 00000000000..0a1978f3d69 --- /dev/null +++ b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/import-dashboard.spec.ts b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts new file mode 100644 index 00000000000..cd905ac56b8 --- /dev/null +++ b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts @@ -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 = ''; + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts new file mode 100644 index 00000000000..ce1293acc2c --- /dev/null +++ b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts new file mode 100644 index 00000000000..99fa52b3a5c --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts @@ -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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts new file mode 100644 index 00000000000..30a16a7dae0 --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts @@ -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'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts new file mode 100644 index 00000000000..a769f8438cb --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts @@ -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'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts new file mode 100644 index 00000000000..dbb670c739e --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts @@ -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'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts new file mode 100644 index 00000000000..d289142a097 --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts @@ -0,0 +1,251 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; +const DASHBOARD_NAME = 'Templating - Nested Template Variables'; + +test.describe( + 'Variables - Query - Add variable', + { + tag: ['@dashboards'], + }, + () => { + test('query variable should be default and default fields should be correct', 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(); + + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await expect(nameInput).toBeVisible(); + await expect(nameInput).toHaveAttribute('placeholder', 'Variable name'); + await expect(nameInput).toHaveValue('query0'); + + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await expect(typeSelect).toBeVisible(); + const singleValue = typeSelect.locator('div[class*="-singleValue"]'); + await expect(singleValue).toHaveText('Query'); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await expect(labelInput).toBeVisible(); + await expect(labelInput).toHaveAttribute('placeholder', 'Label name'); + await expect(labelInput).toHaveValue(''); + + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await expect(descriptionInput).toBeVisible(); + await expect(descriptionInput).toHaveAttribute('placeholder', 'Descriptive text'); + await expect(descriptionInput).toHaveValue(''); + + await expect(page.locator('label').filter({ hasText: 'Hide' })).toBeVisible(); + + // Check datasource selector + const datasourceSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect + ); + await expect(datasourceSelect).toBeVisible(); + await expect(datasourceSelect).toHaveAttribute('placeholder', 'gdev-testdata'); + + await expect(page.locator('label').filter({ hasText: 'Refresh' })).toBeVisible(); + await expect(page.locator('label').filter({ hasText: 'On dashboard load' })).toBeVisible(); + + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await expect(regexInput).toBeVisible(); + await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?.*)-(?.*)-.*/'); + await expect(regexInput).toHaveValue(''); + + const sortSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2 + ); + await expect(sortSelect).toBeVisible(); + const sortSingleValue = sortSelect.locator('div[class*="-singleValue"]'); + await expect(sortSingleValue).toHaveText('Disabled'); + + // Check Multi-value checkbox + const multiValueLabel = page.locator('label').filter({ hasText: 'Multi-value' }); + const multiValueCheckbox = multiValueLabel.locator('input[type="checkbox"]'); + await expect(multiValueCheckbox).toBeChecked({ + checked: false, + }); + + // Check Include All option checkbox + const includeAllLabel = page.locator('label').filter({ hasText: 'Include All option' }); + const includeAllCheckbox = includeAllLabel.locator('input[type="checkbox"]'); + await expect(includeAllCheckbox).toBeChecked({ + checked: false, + }); + + // Check preview and custom all input don't exist initially + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput + ) + ).toBeHidden(); + }); + + test('adding a single value query 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(); + + // Click the "New" button to add a variable + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('a label'); + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await descriptionInput.fill('a description'); + const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill('gdev-testdata'); + const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-testdata').click(); + const queryInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput + ); + await queryInput.fill('*'); + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await regexInput.fill('/.*C.*/'); + await regexInput.blur(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toBeVisible(); + + // Navigate back to dashboard + const submitButton = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton + ); + await submitButton.click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // Check variable appears in submenu + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('a label')) + ).toBeVisible(); + + const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItems).toHaveCount(4); + const fourthItem = submenuItems.nth(3); + const input = fourthItem.locator('input'); + await input.click(); + + // Check dropdown has 1 option containing 'C' + const options = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(options).toHaveCount(1); + await expect(options).toContainText('C'); + }); + + test('adding a multi value query 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(); + + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('a label'); + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await descriptionInput.fill('a description'); + const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill('gdev-testdata'); + const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-testdata').click(); + const queryInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput + ); + await queryInput.fill('*'); + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await regexInput.fill('/.*C.*/'); + + // Enable Multi-value + const multiValueLabel = page.locator('label').filter({ hasText: 'Multi-value' }); + const multiValueCheckbox = multiValueLabel.locator('input[type="checkbox"]'); + await multiValueCheckbox.click({ force: true }); + await expect(multiValueCheckbox).toBeChecked(); + + // Enable Include All option + const includeAllLabel = page.locator('label').filter({ hasText: 'Include All option' }); + const includeAllCheckbox = includeAllLabel.locator('input[type="checkbox"]'); + await includeAllCheckbox.click({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + const customAllInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput + ); + await expect(customAllInput).toHaveValue(''); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toHaveCount(2); + + // Navigate back to dashboard + const submitButton = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton + ); + await submitButton.click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // Check variable appears in submenu + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('a label')) + ).toBeVisible(); + + const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItems).toHaveCount(4); + + const fourthItem = submenuItems.nth(3); + const input = fourthItem.locator('input'); + await input.click(); + + // Check dropdown has 3 options (All + filtered results) + const options = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(options).toHaveCount(3); + + // Check toggle shows Selected (1) + const toggleAll = dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions); + await expect(toggleAll).toHaveText('Selected (1)'); + + // Check options contain 'All' and 'C' + await expect(options.filter({ hasText: 'All' })).toBeVisible(); + // need to use regex here else this will also match "Selected" + await expect(options.filter({ hasText: /^C$/ })).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts new file mode 100644 index 00000000000..4f5a856c5de --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Variables - Text box', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new text box 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 "text box" 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('Textbox'); + 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'); + const textBoxInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2 + ); + await textBoxInput.fill('cat-dog'); + + // Navigate back to the dashboard 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(); + const submenuItem = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + + const textInput = submenuItem.locator('input'); + await textInput.fill('dog-cat'); + await textInput.blur(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: dog-cat'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts new file mode 100644 index 00000000000..22aacc41c88 --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally'; + +test.describe( + 'Repeating a panel horizontally', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat a panel horizontally', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + // Verify all panels are initially visible + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + } + + // Change to only show panels 1 + 3 + const horizontalVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('horizontal')) + .locator('..') + .locator('input'); + await horizontalVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown panels + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-horizontal=1&var-horizontal=3` }); + + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + // Check correct panels are displayed with proper positioning + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts new file mode 100644 index 00000000000..0e6755c05e5 --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically'; + +test.describe( + 'Repeating a panel vertically', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat a panel vertically', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + // Verify all panels are initially visible + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + } + + // Change to only show panels 1 + 3 + const verticalVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('vertical')) + .locator('..') + .locator('input'); + await verticalVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown panels + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-vertical=1&var-vertical=3` }); + + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + // Check correct panels are displayed with proper positioning + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts new file mode 100644 index 00000000000..c326f55552d --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts @@ -0,0 +1,114 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row'; + +test.describe( + 'Repeating empty rows', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat empty rows vertically', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevTop = Number.NEGATIVE_INFINITY; + const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3']; + + for (const title of rowTitles) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3']; + + // Verify all rows are initially visible + for (const title of rowTitles) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + } + + // Change to only show rows 1 + 3 + const rowVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('row')) + .locator('..') + .locator('input'); + await rowVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown rows + let prevTop = Number.NEGATIVE_INFINITY; + const rowsShown = ['Row title 1', 'Row title 3']; + const rowsNotShown = ['Row title 2']; + + for (const title of rowsShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + + for (const title of rowsNotShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-row=1&var-row=3` }); + + let prevTop = Number.NEGATIVE_INFINITY; + const rowsShown = ['Row title 1', 'Row title 3']; + const rowsNotShown = ['Row title 2']; + + // Check correct rows are displayed with proper positioning + for (const title of rowsShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + + for (const title of rowsNotShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts new file mode 100644 index 00000000000..ccf6056d695 --- /dev/null +++ b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts @@ -0,0 +1,280 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; + +test.describe( + 'Variables - Set options from ui', + { + tag: ['@dashboards'], + }, + () => { + test('clicking a value that is not part of dependents options should change these to All', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'A', + 'var-server': 'AA', + 'var-pod': 'AAA', + }), + }); + + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')) + .locator('..') + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + ) + ).toBeVisible(); + + const serverVariableAllOptions = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + ); + await expect(serverVariableAllOptions).toHaveCount(2); + + const firstServerDropdown = serverVariableAllOptions.nth(0); + await expect(firstServerDropdown).toBeVisible(); + await firstServerDropdown.locator('input').click(); + + const selectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(selectOptions).toHaveCount(10); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + 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(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('pod')) + .locator('..') + .locator('input'); + await podVariable.click(); + + // length is 11 because of virtualized select options + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(podSelectOptions).toHaveCount(11); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAC')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAD')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAE')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAF')) + ).toBeVisible(); + }); + + test('adding a value that is not part of dependents options should add the new values dependant options', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'A', + 'var-server': 'AA', + 'var-pod': 'AAA', + }), + }); + + // Click on datacenter variable dropdown + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')) + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (2)' + ); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Verify datacenter shows "A,B" + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B') + ) + ).toBeVisible(); + + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')) + .locator('input'); + await expect(serverVariable).toBeVisible(); + await serverVariable.click(); + + const serverSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(serverSelectOptions).toHaveCount(11); + + 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(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AD')) + ).toBeVisible(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Click on pod variable dropdown + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA')) + .locator('input'); + await expect(podVariable).toBeVisible(); + await podVariable.click(); + + // Verify pod dropdown has 10 options + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(podSelectOptions).toHaveCount(10); + + 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('removing a value that is part of dependents options should remove the new values dependant options', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + // can't use queryParams here because it won't work with the multiple values for each variable + uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-datacenter=B&var-server=AA&var-server=BB&var-pod=AAA&var-pod=BBB`, + }); + + // Click on datacenter variable dropdown + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B')) + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + .click(); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + ) + ).toBeVisible(); + + // Click on server variable dropdown + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')) + .locator('input'); + await expect(serverVariable).toBeVisible(); + await serverVariable.click(); + + const serverSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(serverSelectOptions).toHaveCount(10); + + 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 + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Click on pod variable dropdown + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')) + .locator('input'); + await expect(podVariable).toBeVisible(); + await podVariable.click(); + + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(podSelectOptions).toHaveCount(10); + + 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(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts new file mode 100644 index 00000000000..a2ae416d84b --- /dev/null +++ b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.use({ + featureToggles: { + newDashboardSharingComponent: false, // Use legacy sharing component for this test + }, +}); + +test.describe( + 'Snapshots', + { + tag: ['@dashboards'], + }, + () => { + test('Create a snapshot dashboard', async ({ page, gotoDashboardPage, selectors }) => { + 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 modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select the snapshot tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Snapshot')).click(); + + // Publish snapshot + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.SnapshotScene.PublishSnapshot) + .click(); + + // Copy link button should be visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.SnapshotScene.CopyUrlButton) + ).toBeVisible(); + + // Get the snapshot URL from the input field + const urlInput = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.SnapshotScene.CopyUrlInput + ); + const snapshotUrl = await urlInput.inputValue(); + + // Extract the snapshot key from the URL and navigate to the snapshot + const snapshotKey = getSnapshotKey(snapshotUrl); + await page.goto(`/dashboard/snapshot/${snapshotKey}`); + + // 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 getSnapshotKey = (url: string): string => { + return url.split('/').pop() || ''; +}; diff --git a/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts new file mode 100644 index 00000000000..54bfdb771ba --- /dev/null +++ b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'yBCC3aKGk'; + +test.describe( + 'Templating dashboard links and variables', + { + tag: ['@dashboards'], + }, + () => { + test('Tests dashboard links and variables in links', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID }); + + const verifyLinks = async (variableValue: string) => { + const dashboardLinks = dashboardPage.getByGrafanaSelector(selectors.components.DashboardLinks.link); + + let linkCount = 0; + await expect + .poll(async () => { + linkCount = await dashboardLinks.count(); + return linkCount; + }) + .toBeGreaterThan(13); + + for (let i = 0; i < linkCount; i++) { + const href = await dashboardLinks.nth(i).getAttribute('href'); + expect(href).toContain(variableValue); + } + }; + + // Click on dashboard links dropdown + const dashboardLinksDropdown = dashboardPage.getByGrafanaSelector(selectors.components.DashboardLinks.dropDown); + await expect(dashboardLinksDropdown).toBeVisible(); + await dashboardLinksDropdown.click(); + + // Verify links contain default variable value + await verifyLinks('var-custom=$__all'); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Change variable value from $__all to p2 + const customVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')) + .locator('input'); + await expect(customVariable).toBeVisible(); + await customVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2')) + .click(); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Open dashboard links dropdown again + await expect(dashboardLinksDropdown).toBeVisible(); + await dashboardLinksDropdown.click(); + + // Verify all links now contain the p2 value + await verifyLinks('p2'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/textbox-variables.spec.ts b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts new file mode 100644 index 00000000000..20815446223 --- /dev/null +++ b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts @@ -0,0 +1,56 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'AejrN1AMz'; + +test.describe( + 'TextBox - load options scenarios', + { + tag: ['@dashboards'], + }, + () => { + test('default options should be correct', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await validateTextboxAndMarkup(page, dashboardPage, selectors, 'default value'); + }); + + test('loading variable from url should be correct', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + 'var-text': 'not default value', + }), + }); + + await validateTextboxAndMarkup(page, dashboardPage, selectors, 'not default value'); + }); + } +); + +// Helper function to validate textbox and markup +async function validateTextboxAndMarkup( + page: Page, + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + value: string +) { + const submenuItem = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItem).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('text')) + ).toBeVisible(); + + const textInput = submenuItem.locator('input'); + await expect(textInput).toBeVisible(); + await expect(textInput).toHaveValue(value); + + const textPanel = page.locator(selectors.components.Panels.Visualization.Text.container('')); + await expect(textPanel).toBeVisible(); + + const headerElement = textPanel.locator('h1'); + await expect(headerElement).toBeVisible(); + await expect(headerElement).toHaveText(`variable: ${value}`); +} diff --git a/e2e-playwright/dashboards-suite/utils/makeDashboard.ts b/e2e-playwright/dashboards-suite/utils/makeDashboard.ts new file mode 100644 index 00000000000..2ada48db7a9 --- /dev/null +++ b/e2e-playwright/dashboards-suite/utils/makeDashboard.ts @@ -0,0 +1,58 @@ +export function makeNewDashboardRequestBody(dashboardName: string, folderUid?: string) { + return { + dashboard: { + annotations: { + list: [ + { + builtIn: 1, + datasource: { type: 'grafana', uid: '-- Grafana --' }, + enable: true, + hide: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + type: 'dashboard', + }, + ], + }, + editable: true, + fiscalYearStartMonth: 0, + graphTooltip: 0, + links: [], + liveNow: false, + panels: [ + { + datasource: { type: 'testdata', uid: '89_jzlT4k' }, + gridPos: { h: 9, w: 12, x: 0, y: 0 }, + id: 2, + options: { + code: { + language: 'plaintext', + showLineNumbers: false, + showMiniMap: false, + }, + content: '***A nice little happy empty dashboard***', + mode: 'markdown', + }, + pluginVersion: '9.4.0-pre', + title: 'Nothing to see here', + type: 'text', + }, + ], + refresh: '', + revision: 1, + schemaVersion: 38, + tags: [], + templating: { list: [] }, + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + timezone: '', + title: dashboardName, + version: 0, + uid: '', + weekStart: '', + }, + message: '', + overwrite: false, + folderUid, + } as const; +} diff --git a/e2e-playwright/dashboards/DashboardLiveTest.json b/e2e-playwright/dashboards/DashboardLiveTest.json new file mode 100644 index 00000000000..7d98377df89 --- /dev/null +++ b/e2e-playwright/dashboards/DashboardLiveTest.json @@ -0,0 +1,107 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.3.0-pre", + "targets": [ + { + "channel": "plugin/testdata/random-20Hz-stream", + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "measurements", + "refId": "A" + } + ], + "title": "Live", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "New dashboard", + "version": 0, + "uid": "live-e2e-test", + "weekStart": "" +} diff --git a/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json b/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json new file mode 100644 index 00000000000..08b2e3fda11 --- /dev/null +++ b/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json @@ -0,0 +1,255 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "links": [ + { + "title": "", + "url": "/d/${__dashboard.uid}?var-instance=${__data.fields.test1}&${__url_time_range}" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "alias": "test1", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv" + } + ], + "title": "Data links without slug", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "counters_logins{geohash=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "9wvfgzurfzb", + "value": "9wvfgzurfzb" + }, + "name": "instance", + "options": [ + { + "selected": true, + "text": "9wvfgzurfzb", + "value": "9wvfgzurfzb" + }, + { + "selected": false, + "text": "9yy21uzzxypg", + "value": "9yy21uzzxypg" + }, + { + "selected": false, + "text": "dr199bpvpcru", + "value": "dr199bpvpcru" + }, + { + "selected": false, + "text": "dre33fzyxcrz", + "value": "dre33fzyxcrz" + }, + { + "selected": false, + "text": "gc6j7crvrcpf", + "value": "gc6j7crvrcpf" + }, + { + "selected": false, + "text": "u6g9zuxvxypv", + "value": "u6g9zuxvxypv" + } + ], + "query": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv", + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "Data Link without slug test", + "uid": "data-link-no-slug", + "version": 3 +} diff --git a/e2e-playwright/dashboards/PanelSandboxDashboard.json b/e2e-playwright/dashboards/PanelSandboxDashboard.json new file mode 100644 index 00000000000..50fb2ad7f57 --- /dev/null +++ b/e2e-playwright/dashboards/PanelSandboxDashboard.json @@ -0,0 +1,57 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "title": "Sandbox Panel test", + "type": "sandbox-test-panel" + } + ], + "refresh": "", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Sandbox Panel Test", + "uid": "c46b2460-16b7-42a5-82d1-b07fbf431950", + "version": 1, + "weekStart": "" +} diff --git a/e2e-playwright/dashboards/TestDashboard.json b/e2e-playwright/dashboards/TestDashboard.json new file mode 100644 index 00000000000..8f58d09c697 --- /dev/null +++ b/e2e-playwright/dashboards/TestDashboard.json @@ -0,0 +1,208 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "text": {} + }, + "pluginVersion": "8.3.0-pre", + "title": "Gauge Example", + "type": "gauge" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.3.0-pre", + "title": "Stat", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "title": "Time series example", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 31, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2021-09-01T04:00:00.000Z", + "to": "2021-09-15T04:00:00.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "E2E Test - Import Dashboard", + "uid": "kquZN5H7k", + "version": 4 +} diff --git a/e2e-playwright/dashboards/TestV2Dashboard.json b/e2e-playwright/dashboards/TestV2Dashboard.json new file mode 100644 index 00000000000..da89762c3a6 --- /dev/null +++ b/e2e-playwright/dashboards/TestV2Dashboard.json @@ -0,0 +1,424 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "admjzp8", + "namespace": "default", + "uid": "hrbekBWXeM7nC7GtouIbcngFyuqt8Xx7KlE4AnTwV7AX", + "resourceVersion": "1", + "generation": 1, + "creationTimestamp": "2025-05-16T09:41:56Z", + "labels": { + "grafana.app/deprecatedInternalID": "182" + }, + "annotations": { + "grafana.app/createdBy": "user:cejvsh18uudxcf", + "grafana.app/updatedBy": "user:cejvsh18uudxcf", + "grafana.app/updatedTimestamp": "2025-05-16T09:41:56Z", + "grafana.app/folder": "" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts" + } + } + ], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "height": 8, + "width": 12, + "x": 12, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 8 + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "New Test V2 Dashboard", + "variables": [] + }, + "status": {} +} diff --git a/e2e-playwright/fixtures/exemplars-query-response.json b/e2e-playwright/fixtures/exemplars-query-response.json new file mode 100644 index 00000000000..6c26a9fbf7d --- /dev/null +++ b/e2e-playwright/fixtures/exemplars-query-response.json @@ -0,0 +1,323 @@ +{ + "results": { + "A": { + "frames": [ + { + "schema": { + "name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))", + "refId": "A", + "meta": { "custom": { "resultType": "matrix" } }, + "fields": [ + { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, + { + "name": "Value", + "type": "number", + "typeInfo": { "frame": "float64" }, + "labels": {}, + "config": { + "displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))" + } + } + ] + }, + "data": { + "values": [ + [ + 1633619595000, 1633619610000, 1633619625000, 1633619640000, 1633619655000, 1633619670000, 1633619685000, + 1633619700000, 1633619715000, 1633619730000, 1633619745000, 1633619760000, 1633619775000, 1633619790000, + 1633619805000, 1633619820000, 1633619835000, 1633619850000, 1633619865000, 1633619880000, 1633619895000 + ], + [ + 0.07245212135073513, 0.07253198890830721, 0.07247862573797707, 0.07238248338231042, 0.07221687487740913, + 0.07223291298743946, 0.07225427016727755, 0.024531677091864545, 0.02317081920915543, + 0.07548902139580993, 0.0777721702857508, 0.07768649905047344, 0.07782257603228229, 0.07788810213200052, + 0.07791835055437593, 0.07798387201529966, 0.07790826751849372, 0.07794858648610933, 0.07778729925797964, + 0.07769657495236215, 0.077550401329267 + ] + ] + } + }, + { + "schema": { + "name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))", + "refId": "A", + "meta": { "custom": { "resultType": "vector" } }, + "fields": [ + { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, + { + "name": "Value", + "type": "number", + "typeInfo": { "frame": "float64" }, + "labels": {}, + "config": { + "displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))" + } + } + ] + }, + "data": { "values": [[1633619900000], [0.0775504013292671]] } + }, + { + "schema": { + "name": "exemplar", + "refId": "A", + "meta": { "custom": { "resultType": "exemplar" } }, + "fields": [ + { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, + { "name": "Value", "type": "number", "typeInfo": { "frame": "float64" } }, + { "name": "instance", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "__name__", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "job", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "status_code", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "method", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "traceID", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "route", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "ws", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "le", "type": "string", "typeInfo": { "frame": "string" } } + ] + }, + "data": { + "values": [ + [ + 1633619598000, 1633619622000, 1633619625000, 1633619646000, 1633619658000, 1633619682000, 1633619695000, + 1633619712000, 1633619712000, 1633619724000, 1633619717000, 1633619742000, 1633619757000, 1633619771000, + 1633619784000, 1633619801000, 1633619806000, 1633619833000, 1633619833000, 1633619845000, 1633619862000, + 1633619877000, 1633619889000 + ], + [ + 0.0146153, 0.0118506, 0.0473847, 0.026997, 0.0164318, 0.0113532, 0.0105197, 0.162789, 0.0556026, + 0.148856, 0.0433809, 0.0117758, 0.0114496, 0.0114099, 0.0421927, 0.0134148, 0.0152827, 0.6975967, + 0.0394788, 0.0137441, 0.0110939, 0.0104496, 0.0101284 + ], + [ + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "db:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80" + ], + [ + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket" + ], + [ + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/db", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app" + ], + [ + "302", + "200", + "200", + "200", + "200", + "200", + "200", + "500", + "200", + "302", + "208", + "200", + "200", + "200", + "200", + "200", + "302", + "200", + "200", + "200", + "200", + "200", + "200" + ], + [ + "POST", + "GET", + "GET", + "GET", + "GET", + "GET", + "GET", + "GET", + "GET", + "POST", + "POST", + "GET", + "GET", + "GET", + "GET", + "GET", + "POST", + "GET", + "GET", + "GET", + "GET", + "GET", + "GET" + ], + [ + "6a3cf561ef6c32a0", + "396bcdf29601a149", + "57c04ef608f11158", + "77c757dab83c665f", + "3d1069567e873f5e", + "b337949f6213efd", + "21b20cbe533cf099", + "2c10b3aa30fabd66", + "42ac6088a757636b", + "2f81158008cd4dcc", + "320b803ad7323b37", + "7f15fd82aeb8b361", + "11c79266da8a74cd", + "5a8571bdcc04c990", + "3de3f4f42ccb93ae", + "23343ac91cc0638", + "5cea3aad17ab11c8", + "5d334e2843d3405a", + "3cf6834596d4b6b6", + "1ab6cff012959723", + "2f78bc2c398b8b20", + "6d5862a70c3abd42", + "f5421be4054f501" + ], + [ + "post", + "root", + "root", + "root", + "root", + "root", + "root", + "root", + "root", + "post", + "post", + "root", + "root", + "root", + "root", + "root", + "post", + "metrics", + "root", + "root", + "root", + "root", + "root" + ], + [ + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false" + ], + [ + "0.025", + "0.025", + "0.05", + "0.05", + "0.025", + "0.025", + "0.025", + "0.25", + "0.1", + "0.25", + "0.05", + "0.025", + "0.025", + "0.025", + "0.05", + "0.025", + "0.025", + "1.0", + "0.05", + "0.025", + "0.025", + "0.025", + "0.025" + ] + ] + } + } + ] + } + } +} diff --git a/e2e-playwright/fixtures/long-trace-response.json b/e2e-playwright/fixtures/long-trace-response.json new file mode 100644 index 00000000000..80955535638 --- /dev/null +++ b/e2e-playwright/fixtures/long-trace-response.json @@ -0,0 +1,7592 @@ +{ + "data": [ + { + "traceID": "3fa414edcef6ad90", + "spans": [ + { + "traceID": "3fa414edcef6ad90", + "spanID": "1b26effbab24e95a", + "operationName": "FindTraceByID", + "references": [], + "startTime": 1605873894680581, + "duration": 1820, + "tags": [ + { "key": "component", "type": "string", "value": "gRPC" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0f5c1808567e4403", + "operationName": "FindTraceByID", + "references": [], + "startTime": 1605873894680587, + "duration": 1847, + "tags": [ + { "key": "component", "type": "string", "value": "gRPC" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "59f093577238d61e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683862, + "duration": 10204, + "tags": [], + "logs": [ + { "timestamp": 1605873894683872, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894694063, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1cc731490b1da4c5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683858, + "duration": 10257, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "602204dc8b8fbc6d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683201, + "duration": 11185, + "tags": [], + "logs": [ + { "timestamp": 1605873894683207, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894694385, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "586e5e4c0400de11", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683196, + "duration": 11200, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "779ac3811ce65e40", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683844, + "duration": 10983, + "tags": [ + { "key": "blockID", "type": "string", "value": "20a16df1-a312-4b1a-a2e2-33b55e9f3c8b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894694822, + "fields": [ + { "key": "bytes", "type": "int64", "value": 315664 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "24203526fe09b1e2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682997, + "duration": 12453, + "tags": [], + "logs": [ + { "timestamp": 1605873894683002, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894695448, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0afe9ad5f5b01be7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682993, + "duration": 12466, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "51413d67348a4624", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682986, + "duration": 13059, + "tags": [ + { "key": "blockID", "type": "string", "value": "08b90b09-c56e-4b4a-b95f-3f0409dc9ce9" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894695963, + "fields": [ + { "key": "bytes", "type": "int64", "value": 239824 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "60007a76ffde4644", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682866, + "duration": 13279, + "tags": [], + "logs": [ + { "timestamp": 1605873894682872, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894696144, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "09d7a8c1faef5a84", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682861, + "duration": 13291, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2755efbbfb1b537b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682846, + "duration": 14054, + "tags": [ + { "key": "blockID", "type": "string", "value": "f78b0397-d3ad-4514-9bf4-87b6ea7e920e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894696898, + "fields": [ + { "key": "bytes", "type": "int64", "value": 218440 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25223420e121413a", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683188, + "duration": 14278, + "tags": [ + { "key": "blockID", "type": "string", "value": "3ae22086-9266-481a-9725-c921471e4a94" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894697462, + "fields": [ + { "key": "bytes", "type": "int64", "value": 397880 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "17a3baf85848a727", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683030, + "duration": 14724, + "tags": [], + "logs": [ + { "timestamp": 1605873894683033, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894697752, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "46ebfa6c443776c4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683027, + "duration": 14734, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "19b1afe02cf639cf", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683883, + "duration": 14279, + "tags": [], + "logs": [ + { "timestamp": 1605873894683889, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894698160, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6e5a7dd55283f907", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683879, + "duration": 14289, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5085badf0c1dc842", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683657, + "duration": 14886, + "tags": [], + "logs": [ + { "timestamp": 1605873894683663, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894698542, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "71a0e94722b662ed", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683653, + "duration": 14897, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "57e69d8f17b39563", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683388, + "duration": 15548, + "tags": [], + "logs": [ + { "timestamp": 1605873894683394, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894698936, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6fe636103f47e1fc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683384, + "duration": 15558, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "52146a5c1b2c0030", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683284, + "duration": 15701, + "tags": [], + "logs": [ + { "timestamp": 1605873894683290, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894698984, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "160fb4c8329a2ea0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683280, + "duration": 15712, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1e283fe0dd8cc773", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683024, + "duration": 16029, + "tags": [ + { "key": "blockID", "type": "string", "value": "9e102b4e-115a-4bda-abd6-aa6221f9e4b7" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894699050, + "fields": [ + { "key": "bytes", "type": "int64", "value": 395808 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bae5c35dd7187ba", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683644, + "duration": 15612, + "tags": [ + { "key": "blockID", "type": "string", "value": "b2f5a951-19a0-473d-8830-e1120ab7bf25" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894699255, + "fields": [ + { "key": "bytes", "type": "int64", "value": 345992 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5af2c497b60703d9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683842, + "duration": 15628, + "tags": [], + "logs": [ + { "timestamp": 1605873894683848, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894699469, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a64d382dd0239a7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683837, + "duration": 15639, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "04652166eaec115c", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683378, + "duration": 16179, + "tags": [ + { "key": "blockID", "type": "string", "value": "30903640-5e8c-4cf6-9dc8-f84e0e2541c8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894699555, + "fields": [ + { "key": "bytes", "type": "int64", "value": 291056 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "650c7f5ec8cc53a5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683871, + "duration": 15807, + "tags": [ + { "key": "blockID", "type": "string", "value": "19b49abb-e17a-4632-a4b9-3ce95208e3cf" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894699675, + "fields": [ + { "key": "bytes", "type": "int64", "value": 424248 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1b30323ce39314b9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684553, + "duration": 15144, + "tags": [], + "logs": [ + { "timestamp": 1605873894684559, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894699696, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "288816ad36c9020c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684549, + "duration": 15154, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "26e83a54365218ad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683881, + "duration": 16602, + "tags": [], + "logs": [ + { "timestamp": 1605873894683888, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894700482, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5e6a2e62081720fd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683878, + "duration": 16613, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "63332243ceed106c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683893, + "duration": 16666, + "tags": [], + "logs": [ + { "timestamp": 1605873894683900, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894700557, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7dbbbda52a6d32ce", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683888, + "duration": 16678, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "195ed27075e44238", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683828, + "duration": 16766, + "tags": [ + { "key": "blockID", "type": "string", "value": "6c5d1290-2b4b-4f33-9798-63b6654e16b4" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894700591, + "fields": [ + { "key": "bytes", "type": "int64", "value": 367848 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "35e5a12a53c6088a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682748, + "duration": 17901, + "tags": [], + "logs": [ + { "timestamp": 1605873894682751, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894700647, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "690fcd8c8dc87ae8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683273, + "duration": 17376, + "tags": [ + { "key": "blockID", "type": "string", "value": "f1db0c64-befe-4790-af19-7b48e57a9558" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894700646, + "fields": [ + { "key": "bytes", "type": "int64", "value": 386448 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "113befce4abfecb2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682745, + "duration": 17911, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "277870fa55872b13", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683865, + "duration": 17440, + "tags": [ + { "key": "blockID", "type": "string", "value": "f05f1d13-0250-492a-abc8-bca24ccf3a15" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894701291, + "fields": [ + { "key": "bytes", "type": "int64", "value": 211672 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "022b6c95374f166d", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683879, + "duration": 17471, + "tags": [ + { "key": "blockID", "type": "string", "value": "0cba7eaf-2546-41ac-99d7-673ef23d6e98" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894701347, + "fields": [ + { "key": "bytes", "type": "int64", "value": 406456 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6cee3530fc730d34", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684631, + "duration": 16733, + "tags": [], + "logs": [ + { "timestamp": 1605873894684639, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894701363, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "674b435291a256c4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684627, + "duration": 16745, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1de85b574e5d906c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683373, + "duration": 18328, + "tags": [], + "logs": [ + { "timestamp": 1605873894683380, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894701701, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4c5ac8757f9888b7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683369, + "duration": 18338, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3e5ab83b57207c74", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683042, + "duration": 18823, + "tags": [], + "logs": [ + { "timestamp": 1605873894683045, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894701863, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7d9927e5c258d511", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682730, + "duration": 19136, + "tags": [ + { "key": "blockID", "type": "string", "value": "794e2adc-701e-4c2d-907a-66221b4455d3" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894701864, + "fields": [ + { "key": "bytes", "type": "int64", "value": 289928 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6c9178ed1e68f858", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683039, + "duration": 18834, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "445d4f3f2dc4d0ad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684214, + "duration": 17919, + "tags": [], + "logs": [ + { "timestamp": 1605873894684221, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702132, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "30dd998b2082f2b9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684210, + "duration": 17958, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2ff9bbb6c991a0ea", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683877, + "duration": 18301, + "tags": [], + "logs": [ + { "timestamp": 1605873894683883, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702177, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "310a2399bb07e8bd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683873, + "duration": 18311, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "19021bbbe6310785", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683360, + "duration": 18978, + "tags": [ + { "key": "blockID", "type": "string", "value": "d2212e62-5b1a-41e2-ae43-c0a596125f1b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894702335, + "fields": [ + { "key": "bytes", "type": "int64", "value": 199208 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "021f72c9979124b5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684621, + "duration": 17816, + "tags": [ + { "key": "blockID", "type": "string", "value": "06ebaf3b-4501-4cda-91fb-c48a9d33a99c" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894702434, + "fields": [ + { "key": "bytes", "type": "int64", "value": 384696 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "68a1e78424019eb9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683657, + "duration": 18900, + "tags": [], + "logs": [ + { "timestamp": 1605873894683663, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702556, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "244e73561d0c691d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683653, + "duration": 18910, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5c1d1b2d38dddcfb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683518, + "duration": 19222, + "tags": [], + "logs": [ + { "timestamp": 1605873894683526, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702739, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "364583eecf36b543", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683513, + "duration": 19232, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "22e42286de359dc4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683843, + "duration": 18969, + "tags": [], + "logs": [ + { "timestamp": 1605873894683849, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702812, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7b936283fac4d0ac", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683838, + "duration": 18981, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "660886869edd36cf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684202, + "duration": 18627, + "tags": [ + { "key": "blockID", "type": "string", "value": "f07137b8-7a0b-4199-b1a7-6b7d5b230723" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894702824, + "fields": [ + { "key": "bytes", "type": "int64", "value": 293936 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "57ed8902af3a60b5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683549, + "duration": 19426, + "tags": [], + "logs": [ + { "timestamp": 1605873894683554, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702972, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "64cadcdb4f18b2f7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683544, + "duration": 19437, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25f434fb5960aaef", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683933, + "duration": 19303, + "tags": [], + "logs": [ + { "timestamp": 1605873894683939, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894703235, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62afac560d435620", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683929, + "duration": 19314, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "58435ec74d79cc93", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683865, + "duration": 19469, + "tags": [ + { "key": "blockID", "type": "string", "value": "f2a53e6e-e261-4ec2-92bd-97c5a4c4b760" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894703331, + "fields": [ + { "key": "bytes", "type": "int64", "value": 390648 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "578849d0d44400b5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684004, + "duration": 19335, + "tags": [], + "logs": [ + { "timestamp": 1605873894684012, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894703337, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1f5faebfb90378ad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683999, + "duration": 19346, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "41f1eb48b61ef185", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683035, + "duration": 20463, + "tags": [ + { "key": "blockID", "type": "string", "value": "941a63d4-2739-4ba2-9a15-08256b5c9eae" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894703490, + "fields": [ + { "key": "bytes", "type": "int64", "value": 438128 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "71ee8c7b83046da0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683645, + "duration": 19895, + "tags": [ + { "key": "blockID", "type": "string", "value": "151c489c-a86a-49b7-9fa9-31d1714d59ee" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894703538, + "fields": [ + { "key": "bytes", "type": "int64", "value": 325344 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bf030a07aaceb80", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683218, + "duration": 20692, + "tags": [], + "logs": [ + { "timestamp": 1605873894683225, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894703909, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "54b34afd73af12d1", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683215, + "duration": 21011, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a7ba0261825c53c", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683505, + "duration": 20615, + "tags": [ + { "key": "blockID", "type": "string", "value": "db0fa030-4607-40e5-998b-47029aa3430e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704118, + "fields": [ + { "key": "bytes", "type": "int64", "value": 411272 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2a597269b23b1bcb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683426, + "duration": 20720, + "tags": [], + "logs": [ + { "timestamp": 1605873894683431, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894704146, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "66d886579510b6fd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683422, + "duration": 20841, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1ef8e63340342174", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683990, + "duration": 20334, + "tags": [ + { "key": "blockID", "type": "string", "value": "a10ec85d-9fd2-403e-abcd-6f4ec49b0396" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704322, + "fields": [ + { "key": "bytes", "type": "int64", "value": 442360 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "46c6de90778460b1", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683830, + "duration": 20675, + "tags": [ + { "key": "blockID", "type": "string", "value": "7e9e0142-15ff-461e-8e05-6c62d920603a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704502, + "fields": [ + { "key": "bytes", "type": "int64", "value": 465744 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "686f3e58fe28940f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696113, + "duration": 8480, + "tags": [], + "logs": [ + { "timestamp": 1605873894696126, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894704591, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5960c1f5750b1cde", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696104, + "duration": 8495, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6aa5ddd42d96f825", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683921, + "duration": 20886, + "tags": [ + { "key": "blockID", "type": "string", "value": "43e5ad4f-11d6-4f25-9925-652cb801fd58" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704804, + "fields": [ + { "key": "bytes", "type": "int64", "value": 405344 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "166377800e8e82a7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683214, + "duration": 21686, + "tags": [], + "logs": [ + { "timestamp": 1605873894683221, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894704899, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5e84f8676ef1efad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683209, + "duration": 21696, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "713c834576a0d9b0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683037, + "duration": 22197, + "tags": [], + "logs": [ + { "timestamp": 1605873894683045, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894705234, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "209c0e336c71e932", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683033, + "duration": 22209, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bd34d50efadb568", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683414, + "duration": 21894, + "tags": [ + { "key": "blockID", "type": "string", "value": "b432160f-347c-41ad-882e-f1786e4b42b1" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894705305, + "fields": [ + { "key": "bytes", "type": "int64", "value": 409104 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "58cee6c544e69e4f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683206, + "duration": 22173, + "tags": [ + { "key": "blockID", "type": "string", "value": "b12afd19-298a-443f-97ce-b5b2e5bc9d79" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894705375, + "fields": [ + { "key": "bytes", "type": "int64", "value": 376872 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4e48e93f70e06522", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696061, + "duration": 9466, + "tags": [ + { "key": "blockID", "type": "string", "value": "45701f45-c93a-4c35-9fed-9cce2c316a19" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894705524, + "fields": [ + { "key": "bytes", "type": "int64", "value": 453296 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2422bf6c2ed108c2", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683536, + "duration": 20571, + "tags": [ + { "key": "blockID", "type": "string", "value": "51945006-c165-40af-baea-769b3199bf46" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704104, + "fields": [ + { "key": "bytes", "type": "int64", "value": 342200 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42fac7c66e0ca970", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683200, + "duration": 22685, + "tags": [ + { "key": "blockID", "type": "string", "value": "8772aa40-3489-4b12-b685-9f708ae4de75" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894705882, + "fields": [ + { "key": "bytes", "type": "int64", "value": 407152 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2a86d93e70a1720c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684627, + "duration": 21313, + "tags": [], + "logs": [ + { "timestamp": 1605873894684633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894705939, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "72991150a8c3cf08", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684622, + "duration": 21322, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3ceac51ce73f994e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683627, + "duration": 22375, + "tags": [], + "logs": [ + { "timestamp": 1605873894683633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706001, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "704707012227a4f1", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683623, + "duration": 22386, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "26cf501f6dcbb968", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683959, + "duration": 22090, + "tags": [], + "logs": [ + { "timestamp": 1605873894683965, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706048, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7ebb1c9d8a55ac56", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683952, + "duration": 22104, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bd01ea1e13ac6fd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683733, + "duration": 22571, + "tags": [], + "logs": [ + { "timestamp": 1605873894683739, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706303, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4f94f7e28081e1af", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683728, + "duration": 22582, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "60fd2b3931676856", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684310, + "duration": 22119, + "tags": [], + "logs": [ + { "timestamp": 1605873894684317, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706428, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "432bc11447588912", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684305, + "duration": 22131, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1e1aa88072a7cefc", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683026, + "duration": 23483, + "tags": [ + { "key": "blockID", "type": "string", "value": "9064347a-7c49-48d8-b348-8d734f7fd542" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894706506, + "fields": [ + { "key": "bytes", "type": "int64", "value": 365672 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1fb49823a6f803bf", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682930, + "duration": 23695, + "tags": [], + "logs": [ + { "timestamp": 1605873894682935, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706622, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7db786f0da6d756d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682926, + "duration": 23705, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01cb21bacc3933da", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894697497, + "duration": 9150, + "tags": [], + "logs": [ + { "timestamp": 1605873894697507, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706646, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "260399c49430577a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894697488, + "duration": 9166, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5ba9d86263fc6da1", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684614, + "duration": 22250, + "tags": [ + { "key": "blockID", "type": "string", "value": "ca346cf4-8162-49e5-a0d0-0619d3813794" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894706862, + "fields": [ + { "key": "bytes", "type": "int64", "value": 416472 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "77f27a840cd8b75b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685061, + "duration": 21838, + "tags": [], + "logs": [ + { "timestamp": 1605873894685068, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706897, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4a48a86f95e117f9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685057, + "duration": 21850, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7d1f782957acfe32", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682777, + "duration": 24168, + "tags": [], + "logs": [ + { "timestamp": 1605873894682783, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706944, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "77f8165c15176536", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682773, + "duration": 24206, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7f20dbc684de78c8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683615, + "duration": 23703, + "tags": [ + { "key": "blockID", "type": "string", "value": "f518974f-2e1e-41c8-b70c-cd2088f5a081" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707316, + "fields": [ + { "key": "bytes", "type": "int64", "value": 278728 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "70a453eeff8ec687", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684828, + "duration": 22510, + "tags": [], + "logs": [ + { "timestamp": 1605873894684835, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894707337, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0c7d975a67c6d7bc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684823, + "duration": 22520, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7ccb153793c6afd9", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683720, + "duration": 23911, + "tags": [ + { "key": "blockID", "type": "string", "value": "6f72b73b-c5fe-4761-b91f-b92f447441fa" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707629, + "fields": [ + { "key": "bytes", "type": "int64", "value": 451984 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5bf10b9afef405a9", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894697476, + "duration": 10175, + "tags": [ + { "key": "blockID", "type": "string", "value": "61e0a11e-5e88-49c4-ad1d-81636670e642" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707648, + "fields": [ + { "key": "bytes", "type": "int64", "value": 296328 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1aae38562e2b6a1f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683945, + "duration": 23883, + "tags": [ + { "key": "blockID", "type": "string", "value": "7dda9580-666b-42f9-b8a1-1680a0de352f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707823, + "fields": [ + { "key": "bytes", "type": "int64", "value": 402936 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1dc5a0697b5d6161", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682764, + "duration": 25091, + "tags": [ + { "key": "blockID", "type": "string", "value": "bf101e70-4a86-4d88-890c-e976330ba857" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707853, + "fields": [ + { "key": "bytes", "type": "int64", "value": 385288 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3df0c4e2de834172", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684425, + "duration": 23557, + "tags": [], + "logs": [ + { "timestamp": 1605873894684432, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894707980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "50f5d53109a047da", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684421, + "duration": 23568, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "64e62db2206bdda3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682595, + "duration": 25553, + "tags": [], + "logs": [ + { "timestamp": 1605873894682603, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894708147, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "40f0742ab8be92ab", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682589, + "duration": 25564, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b89efb6b9fb16fc", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684815, + "duration": 23341, + "tags": [ + { "key": "blockID", "type": "string", "value": "84b0a7ea-895d-49f9-892c-11f689f0c13f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894708154, + "fields": [ + { "key": "bytes", "type": "int64", "value": 424816 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "33c05fda4c7d3921", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684414, + "duration": 23815, + "tags": [], + "logs": [ + { "timestamp": 1605873894684420, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894708228, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "742995638b3636e6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684409, + "duration": 23825, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1cf9294062a5780b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682920, + "duration": 25427, + "tags": [ + { "key": "blockID", "type": "string", "value": "e43ee3db-63c9-4d2b-a791-99a5a9203e4e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894708341, + "fields": [ + { "key": "bytes", "type": "int64", "value": 396312 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6852631d2c6d1586", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683843, + "duration": 24695, + "tags": [], + "logs": [ + { "timestamp": 1605873894683850, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894708538, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1691ee4e1f907b39", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683839, + "duration": 24706, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bcd55e85df0601a", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684400, + "duration": 24493, + "tags": [ + { "key": "blockID", "type": "string", "value": "211313f2-7284-43eb-b9dc-134b5b344524" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894708891, + "fields": [ + { "key": "bytes", "type": "int64", "value": 249032 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7757c670662153b5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682578, + "duration": 26605, + "tags": [ + { "key": "blockID", "type": "string", "value": "99a8b127-bef6-4718-997b-18e5cb6bee81" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894709180, + "fields": [ + { "key": "bytes", "type": "int64", "value": 443904 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "033e809d9deb02fb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683129, + "duration": 26149, + "tags": [], + "logs": [ + { "timestamp": 1605873894683139, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709277, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0701e7633d141024", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683124, + "duration": 26160, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5a1fcbfa2c2e077e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682990, + "duration": 26296, + "tags": [], + "logs": [ + { "timestamp": 1605873894682993, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709285, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2639318a16168a94", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682987, + "duration": 26304, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "573267e2aab9eb37", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683831, + "duration": 25627, + "tags": [ + { "key": "blockID", "type": "string", "value": "9a5df823-d980-4671-b33f-ef92e485232f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894709455, + "fields": [ + { "key": "bytes", "type": "int64", "value": 357872 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3705123c90491605", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683886, + "duration": 25575, + "tags": [], + "logs": [ + { "timestamp": 1605873894683890, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709460, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "46138581a74be710", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683883, + "duration": 25585, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "369cd4694f877602", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684325, + "duration": 25173, + "tags": [], + "logs": [ + { "timestamp": 1605873894684385, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709498, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7aab906468c79c5b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894703359, + "duration": 6145, + "tags": [], + "logs": [ + { "timestamp": 1605873894703375, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709503, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "57f0ffddbcc40049", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684321, + "duration": 25185, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0c27a77ad2f6bbb3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894703354, + "duration": 6155, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "27f360a42e423410", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696933, + "duration": 12666, + "tags": [], + "logs": [ + { "timestamp": 1605873894696942, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709598, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7e5086a8bb3eb3b3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696926, + "duration": 12678, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42f4a2e45bc6b552", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683602, + "duration": 26169, + "tags": [], + "logs": [ + { "timestamp": 1605873894683608, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "693c3e7a4e085ce6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683598, + "duration": 26180, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7645427b1d8ca012", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894694874, + "duration": 15023, + "tags": [], + "logs": [ + { "timestamp": 1605873894694896, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709897, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e6e130f1e7bf5ca", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894694866, + "duration": 15038, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2155087a44565c8a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700685, + "duration": 9446, + "tags": [], + "logs": [ + { "timestamp": 1605873894700698, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894710131, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "66ed873b2793ee77", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700678, + "duration": 9459, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "69654d80ac69ec92", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702020, + "duration": 8202, + "tags": [], + "logs": [ + { "timestamp": 1605873894702031, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894710221, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3a0447242878ba00", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701338, + "duration": 8890, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e73b563bfa4df76", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683106, + "duration": 27358, + "tags": [ + { "key": "blockID", "type": "string", "value": "b89a056f-d8cd-41e9-84ad-445e68d0a0d5" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894710462, + "fields": [ + { "key": "bytes", "type": "int64", "value": 337664 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e958ff5d95860cf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683591, + "duration": 27357, + "tags": [ + { "key": "blockID", "type": "string", "value": "4767ecb2-01d3-450b-b005-6b9219fdfd71" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894710945, + "fields": [ + { "key": "bytes", "type": "int64", "value": 421576 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4854f2803a2439d0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684313, + "duration": 26669, + "tags": [ + { "key": "blockID", "type": "string", "value": "ec9c982f-485f-47b2-be74-a7f203368ede" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894710972, + "fields": [ + { "key": "bytes", "type": "int64", "value": 409016 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62aa1124fbaafe29", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684107, + "duration": 26934, + "tags": [], + "logs": [ + { "timestamp": 1605873894684113, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894711040, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "67ee705c301e7e2a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684102, + "duration": 26945, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "417798c3fbab4244", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696911, + "duration": 14252, + "tags": [ + { "key": "blockID", "type": "string", "value": "6a346739-04b1-4e86-8f87-e182b01cf5cd" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894711160, + "fields": [ + { "key": "bytes", "type": "int64", "value": 407144 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "17faaf92fbea2ed9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683730, + "duration": 27707, + "tags": [], + "logs": [ + { "timestamp": 1605873894683736, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894711435, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "29d4e2aa59eae59e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683726, + "duration": 27719, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3b9a85f6cd6075b8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701327, + "duration": 10230, + "tags": [ + { "key": "blockID", "type": "string", "value": "ece056d3-aa27-464b-81a8-643b3ae208e4" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894711554, + "fields": [ + { "key": "bytes", "type": "int64", "value": 359960 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0da2897c85659567", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683377, + "duration": 28594, + "tags": [], + "logs": [ + { "timestamp": 1605873894683384, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894711971, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4407d391acba81fc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683373, + "duration": 28605, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1987773829521f8f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699105, + "duration": 12885, + "tags": [], + "logs": [ + { "timestamp": 1605873894699119, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894711989, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1c9553a6471269c6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699099, + "duration": 12896, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3dedf220c1f51d38", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704853, + "duration": 7356, + "tags": [], + "logs": [ + { "timestamp": 1605873894704879, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712209, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25820f0eebf05ab3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704846, + "duration": 7370, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7027388faf7e1bf1", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684937, + "duration": 27418, + "tags": [], + "logs": [ + { "timestamp": 1605873894684943, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712354, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "44c6d6c7e1afb67d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684933, + "duration": 27429, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3f654e75b41629f5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683718, + "duration": 28677, + "tags": [ + { "key": "blockID", "type": "string", "value": "0e5b1fbb-ab10-44b7-89a0-f8932ee26dcf" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894712392, + "fields": [ + { "key": "bytes", "type": "int64", "value": 369000 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4fa1d1a031112ab0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682996, + "duration": 29565, + "tags": [], + "logs": [ + { "timestamp": 1605873894683006, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712560, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e0985a0b4168ff2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682988, + "duration": 29577, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7a7bf32e81f4317e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682984, + "duration": 29753, + "tags": [ + { "key": "blockID", "type": "string", "value": "c56f4809-bc48-4f81-9656-a3bbb96ba87e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894712734, + "fields": [ + { "key": "bytes", "type": "int64", "value": 406296 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "66d4f363dfa46bdb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684041, + "duration": 28730, + "tags": [], + "logs": [ + { "timestamp": 1605873894684111, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "616b800031f78e5f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684037, + "duration": 28741, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5b0d3da4dac0a4ab", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683364, + "duration": 29595, + "tags": [ + { "key": "blockID", "type": "string", "value": "10e42379-1c35-419e-a26c-2630b9d2cdd2" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894712956, + "fields": [ + { "key": "bytes", "type": "int64", "value": 354744 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1571e420dca57b9f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683878, + "duration": 29122, + "tags": [ + { "key": "blockID", "type": "string", "value": "adb287c7-69e4-4ed8-8604-c3302c766db2" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894712996, + "fields": [ + { "key": "bytes", "type": "int64", "value": 300104 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3120fb610c52c9a6", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684415, + "duration": 28590, + "tags": [ + { "key": "blockID", "type": "string", "value": "faebcb3d-444a-4675-8e55-2f46dbcaa1d7" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713002, + "fields": [ + { "key": "bytes", "type": "int64", "value": 410672 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "46feff0edeabb674", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683135, + "duration": 29700, + "tags": [], + "logs": [ + { "timestamp": 1605873894683141, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712834, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "32df737f09cd2bf9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683131, + "duration": 29904, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "604de25c9811a395", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699064, + "duration": 14141, + "tags": [ + { "key": "blockID", "type": "string", "value": "7df8ef23-0902-4b4b-92aa-b6c1aeb3c9c2" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713203, + "fields": [ + { "key": "bytes", "type": "int64", "value": 436328 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7867c14538ff0c61", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684923, + "duration": 28333, + "tags": [ + { "key": "blockID", "type": "string", "value": "a94e5162-7e01-4bd6-b5c8-1bc3b50c67c6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713253, + "fields": [ + { "key": "bytes", "type": "int64", "value": 433064 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "04e793f4b075b20f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684557, + "duration": 28822, + "tags": [], + "logs": [ + { "timestamp": 1605873894684565, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713378, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3084a10a11a62355", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684553, + "duration": 28832, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0a0b86e5738d630b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685048, + "duration": 28355, + "tags": [ + { "key": "blockID", "type": "string", "value": "36ce4c95-0cb6-4803-bdfd-b316b3c0cc4c" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713400, + "fields": [ + { "key": "bytes", "type": "int64", "value": 293136 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62ea00c2c871a91e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704821, + "duration": 8702, + "tags": [ + { "key": "blockID", "type": "string", "value": "b9c0dc2b-ee12-4876-a517-2902c6fe655e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713521, + "fields": [ + { "key": "bytes", "type": "int64", "value": 415912 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0f54c2d4ac7df141", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683769, + "duration": 29774, + "tags": [], + "logs": [ + { "timestamp": 1605873894683775, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713542, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5e650633f1c4cb45", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683764, + "duration": 29784, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4cff4ebd296d36f0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684028, + "duration": 29524, + "tags": [ + { "key": "blockID", "type": "string", "value": "e6200492-f24a-40ef-946a-e89170d1ac54" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713549, + "fields": [ + { "key": "bytes", "type": "int64", "value": 447592 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0da82a874696fec5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684230, + "duration": 29351, + "tags": [], + "logs": [ + { "timestamp": 1605873894684236, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713581, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "20334815e0eb1b97", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684226, + "duration": 29362, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "20de4a897b30c066", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683315, + "duration": 30412, + "tags": [], + "logs": [ + { "timestamp": 1605873894683323, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713726, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6edcc31aa4c96617", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683309, + "duration": 30424, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3594272577366bc9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684491, + "duration": 29257, + "tags": [], + "logs": [ + { "timestamp": 1605873894684498, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713747, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01e9f897c4145c38", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684485, + "duration": 29270, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "53e0bef2bbb77bea", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684901, + "duration": 28911, + "tags": [], + "logs": [ + { "timestamp": 1605873894684908, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713812, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3df7804d8e682193", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684897, + "duration": 28919, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5664530667612f1f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682860, + "duration": 31015, + "tags": [], + "logs": [ + { "timestamp": 1605873894682871, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713875, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4b6340b15001f8c8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682855, + "duration": 31026, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b3d3f0643735e5f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894694842, + "duration": 19190, + "tags": [ + { "key": "blockID", "type": "string", "value": "f0b87e56-00a6-4270-8ce0-b47affb9113e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894714027, + "fields": [ + { "key": "bytes", "type": "int64", "value": 310320 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4a4b3e0d2f115bcf", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683987, + "duration": 30363, + "tags": [], + "logs": [ + { "timestamp": 1605873894683994, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714350, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1e0da3179b38449d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683983, + "duration": 30374, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "318fcd8e3bfc42c7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684209, + "duration": 30152, + "tags": [], + "logs": [ + { "timestamp": 1605873894684215, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714360, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "37e82ddc44e6bf60", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684205, + "duration": 30162, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0271272ae09aac5f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684542, + "duration": 29977, + "tags": [ + { "key": "blockID", "type": "string", "value": "bfbb9652-84f8-4145-8091-8197ea922ad3" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894714514, + "fields": [ + { "key": "bytes", "type": "int64", "value": 432848 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2d80feb23cbbb7cd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684508, + "duration": 30161, + "tags": [], + "logs": [ + { "timestamp": 1605873894684515, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714668, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01ad9e5d3837c5b6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684503, + "duration": 30172, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42698e68a26de8cf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683756, + "duration": 30942, + "tags": [ + { "key": "blockID", "type": "string", "value": "55f71d63-05b0-4c3a-b79f-a2563307bf40" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894714695, + "fields": [ + { "key": "bytes", "type": "int64", "value": 402624 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6ea302c343fec88f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683645, + "duration": 31247, + "tags": [], + "logs": [ + { "timestamp": 1605873894683652, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714891, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "279e17d93d4978da", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683641, + "duration": 31258, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "59b29ac1ab225873", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683001, + "duration": 31930, + "tags": [], + "logs": [ + { "timestamp": 1605873894683006, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714930, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4196b1f250632b3e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682998, + "duration": 31938, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "224b550ee6ad2bf2", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684889, + "duration": 30088, + "tags": [ + { "key": "blockID", "type": "string", "value": "07baec6a-187a-493b-b160-772936b5a3f0" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894714974, + "fields": [ + { "key": "bytes", "type": "int64", "value": 429360 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "368bcd97b5e9dde0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684678, + "duration": 30934, + "tags": [], + "logs": [ + { "timestamp": 1605873894684685, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894715611, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3d88bddf112b8ae2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684672, + "duration": 30946, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "718a103bd19501b2", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684196, + "duration": 31424, + "tags": [ + { "key": "blockID", "type": "string", "value": "a85669d8-148b-4d61-a359-8f97c036b880" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894715617, + "fields": [ + { "key": "bytes", "type": "int64", "value": 401280 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2b56997697dd91c0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684472, + "duration": 31151, + "tags": [ + { "key": "blockID", "type": "string", "value": "75911f2c-fc5e-4ef1-bcff-9abad2120f23" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894715621, + "fields": [ + { "key": "bytes", "type": "int64", "value": 397808 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1c049cad7edf280e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683630, + "duration": 32119, + "tags": [ + { "key": "blockID", "type": "string", "value": "fdcc5380-c15f-41c2-9a34-623d6cdd2d5a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894715733, + "fields": [ + { "key": "bytes", "type": "int64", "value": 397464 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6e3d16e8ed14d90c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702877, + "duration": 12975, + "tags": [], + "logs": [ + { "timestamp": 1605873894702894, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894715852, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7bd595782cdb70c3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699700, + "duration": 16154, + "tags": [], + "logs": [ + { "timestamp": 1605873894699708, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894715853, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "08a9d074d520a512", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702864, + "duration": 12993, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "083316368540b811", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699695, + "duration": 16164, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7f067cadc2b4569d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684535, + "duration": 31520, + "tags": [], + "logs": [ + { "timestamp": 1605873894684540, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716053, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5061bd596bc8a7e7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684531, + "duration": 31530, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4b9772650994e725", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682990, + "duration": 33209, + "tags": [ + { "key": "blockID", "type": "string", "value": "61022db6-4401-40b6-a3a2-1f4cd5ccb430" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716196, + "fields": [ + { "key": "bytes", "type": "int64", "value": 380504 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0e9c6b89215308ba", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683975, + "duration": 32340, + "tags": [ + { "key": "blockID", "type": "string", "value": "f17c848f-2f99-4215-a5e9-1f55d8e15c1e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716311, + "fields": [ + { "key": "bytes", "type": "int64", "value": 447736 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "030573bc0520e3c2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683061, + "duration": 33348, + "tags": [], + "logs": [ + { "timestamp": 1605873894683067, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716409, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0d2e16a8cf201e5a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683057, + "duration": 33357, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0361f359be22f9c8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702474, + "duration": 14135, + "tags": [], + "logs": [ + { "timestamp": 1605873894702483, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716607, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5310c5c355550cad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702463, + "duration": 14156, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "16870d24920c25b8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699686, + "duration": 17072, + "tags": [ + { "key": "blockID", "type": "string", "value": "320a9ecd-a9fd-4c88-8aeb-e8a312dcce0c" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716753, + "fields": [ + { "key": "bytes", "type": "int64", "value": 424544 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62090e9e1c22bb56", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707676, + "duration": 9095, + "tags": [], + "logs": [ + { "timestamp": 1605873894707691, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "02d91deb1ff0ea76", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707670, + "duration": 9107, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0cc47cc1eb5deb29", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702844, + "duration": 13991, + "tags": [ + { "key": "blockID", "type": "string", "value": "04e21143-53ef-4083-948e-3bbe502c2d44" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716832, + "fields": [ + { "key": "bytes", "type": "int64", "value": 401728 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1b27a749f4d1b557", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684297, + "duration": 32550, + "tags": [ + { "key": "blockID", "type": "string", "value": "d1ffbf86-0e11-4b6e-b9ae-8466e7c42a90" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716844, + "fields": [ + { "key": "bytes", "type": "int64", "value": 193992 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3c5f2282a3e7c658", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683345, + "duration": 33584, + "tags": [], + "logs": [ + { "timestamp": 1605873894683352, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716927, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6fc62f7a1ae1a6e9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683340, + "duration": 33596, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "524a9c941765266a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684584, + "duration": 32449, + "tags": [], + "logs": [ + { "timestamp": 1605873894684592, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894717030, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5bcaa6a4a1c06160", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684579, + "duration": 32460, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2d4e045a72c17ff2", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684465, + "duration": 32666, + "tags": [ + { "key": "blockID", "type": "string", "value": "4c9f58b7-b944-4692-9d5b-14270bd1b8d6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894717127, + "fields": [ + { "key": "bytes", "type": "int64", "value": 436224 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25548a46750dfecb", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684652, + "duration": 32684, + "tags": [ + { "key": "blockID", "type": "string", "value": "ffd8fb66-db97-4451-9a97-bfb6631b82a5" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894717332, + "fields": [ + { "key": "bytes", "type": "int64", "value": 434536 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5f4913a50dcd37c8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683050, + "duration": 34487, + "tags": [ + { "key": "blockID", "type": "string", "value": "0255db6b-061e-4ceb-9ae9-598588995be8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894717533, + "fields": [ + { "key": "bytes", "type": "int64", "value": 392520 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6750f7ac4a5b50e8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684648, + "duration": 32974, + "tags": [], + "logs": [ + { "timestamp": 1605873894684654, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894717621, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "41f4b72bd0a14291", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684644, + "duration": 32984, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "03fac2f4c91b31b6", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702448, + "duration": 15285, + "tags": [ + { "key": "blockID", "type": "string", "value": "f7da9248-f02e-44df-b243-c1f5f69e0f67" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894717730, + "fields": [ + { "key": "bytes", "type": "int64", "value": 366872 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4934a16eeec96b0d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684005, + "duration": 33827, + "tags": [], + "logs": [ + { "timestamp": 1605873894684010, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894717831, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0ebf8034f9944320", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684001, + "duration": 33836, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0cc1f6dfcc153616", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683672, + "duration": 34388, + "tags": [], + "logs": [ + { "timestamp": 1605873894683679, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894718059, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "501e4211325ef503", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683667, + "duration": 34399, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7bcf0390730028ef", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683332, + "duration": 34943, + "tags": [ + { "key": "blockID", "type": "string", "value": "cbeb7cd1-c8b1-4290-be74-4caf7b3f2d69" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894718272, + "fields": [ + { "key": "bytes", "type": "int64", "value": 432424 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1b30f12cd1728ebf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683995, + "duration": 34418, + "tags": [ + { "key": "blockID", "type": "string", "value": "2dd90b29-ffb5-4a27-bcd7-0950ca151c14" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894718411, + "fields": [ + { "key": "bytes", "type": "int64", "value": 210280 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "22a3f914c23d3456", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684570, + "duration": 34106, + "tags": [ + { "key": "blockID", "type": "string", "value": "4fca87b1-ceb1-4290-a3cb-c0a970a7c5a6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894718671, + "fields": [ + { "key": "bytes", "type": "int64", "value": 422528 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "169359b95c501fae", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683035, + "duration": 35830, + "tags": [], + "logs": [ + { "timestamp": 1605873894683041, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894718864, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "71541fab4a38308f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683031, + "duration": 35841, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3a7fc15a2fb60753", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707659, + "duration": 11315, + "tags": [ + { "key": "blockID", "type": "string", "value": "57b3be9d-2234-4b8b-a380-424f30717e5b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894718970, + "fields": [ + { "key": "bytes", "type": "int64", "value": 437088 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "10e57e001b6c6127", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683210, + "duration": 35772, + "tags": [], + "logs": [ + { "timestamp": 1605873894683220, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894718980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2c3fb8ad983d67fc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683206, + "duration": 35784, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3fca8d21c0827061", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684366, + "duration": 34674, + "tags": [], + "logs": [ + { "timestamp": 1605873894684372, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894719039, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25a226515c2f9150", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684362, + "duration": 34687, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5fb9111ac6a5d18d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683274, + "duration": 35965, + "tags": [], + "logs": [ + { "timestamp": 1605873894683286, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894719238, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6945097dfeae216a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683268, + "duration": 35979, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6dd256468dea419f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684628, + "duration": 34986, + "tags": [ + { "key": "blockID", "type": "string", "value": "0a5b8e26-05d5-4df2-97c1-a57ccb631b5e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894719610, + "fields": [ + { "key": "bytes", "type": "int64", "value": 368392 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "22c3bb99916b1cf9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684954, + "duration": 34724, + "tags": [], + "logs": [ + { "timestamp": 1605873894684961, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894719678, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7786d37aacb34302", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684949, + "duration": 34735, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0b4489a19011e658", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702395, + "duration": 17621, + "tags": [], + "logs": [ + { "timestamp": 1605873894702404, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720015, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5fe309dbb10a8aa0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702385, + "duration": 17639, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "28a88c33b44009c0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684502, + "duration": 35545, + "tags": [], + "logs": [ + { "timestamp": 1605873894684508, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6d20c9fb0d7d023a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683045, + "duration": 37003, + "tags": [], + "logs": [ + { "timestamp": 1605873894683057, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7aed634e79451eff", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684353, + "duration": 35695, + "tags": [ + { "key": "blockID", "type": "string", "value": "c4b4a484-2704-49e8-926b-e7bb7a13c520" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720046, + "fields": [ + { "key": "bytes", "type": "int64", "value": 394640 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2361a627270177b2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684498, + "duration": 35556, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6365c636dea9cf69", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683040, + "duration": 37014, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7fd1af0b8e4b13e5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704467, + "duration": 15587, + "tags": [], + "logs": [ + { "timestamp": 1605873894704477, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720054, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4d0b05c2fe988374", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704458, + "duration": 15600, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "45d91fa92cb81841", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683191, + "duration": 36908, + "tags": [ + { "key": "blockID", "type": "string", "value": "abec5c1e-02b5-4165-8b3d-2940d3adb991" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720066, + "fields": [ + { "key": "bytes", "type": "int64", "value": 352392 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4287af315802d4cd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704355, + "duration": 15863, + "tags": [], + "logs": [ + { "timestamp": 1605873894704369, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720218, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62b352c305041dcc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704348, + "duration": 15876, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6ec165f264482f57", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683346, + "duration": 37139, + "tags": [], + "logs": [ + { "timestamp": 1605873894683351, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720484, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7400527184eeef19", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683342, + "duration": 37152, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": ["invalid parent span IDs=4ff7c150586c7e6f; skipping clock skew adjustment"] + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1561e391ecd756d5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684939, + "duration": 35696, + "tags": [ + { "key": "blockID", "type": "string", "value": "7dbea947-c624-454c-a99a-b2aa0c96c19f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720631, + "fields": [ + { "key": "bytes", "type": "int64", "value": 328592 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "57f916fcf19f117f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682643, + "duration": 38011, + "tags": [], + "logs": [ + { "timestamp": 1605873894682653, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720650, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1d4458304925bc0f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682638, + "duration": 38028, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": ["invalid parent span IDs=3ff0fd3a1cdb9b5e; skipping clock skew adjustment"] + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6f251bfe2c45ae12", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894709481, + "duration": 11241, + "tags": [], + "logs": [ + { "timestamp": 1605873894709489, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720722, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5365877f5f1070a3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894709476, + "duration": 11253, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": ["invalid parent span IDs=5d1a0e533881c649; skipping clock skew adjustment"] + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5472390246aac5c4", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683265, + "duration": 37540, + "tags": [ + { "key": "blockID", "type": "string", "value": "31cd1597-b435-467c-8726-9fd43cb8f75a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720802, + "fields": [ + { "key": "bytes", "type": "int64", "value": 263520 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "38b14977915ca22c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683471, + "duration": 37385, + "tags": [], + "logs": [ + { "timestamp": 1605873894683477, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720855, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "08ecb88049158355", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683466, + "duration": 37394, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": ["invalid parent span IDs=241c721a4337e64b; skipping clock skew adjustment"] + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4cb042697154defa", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684490, + "duration": 36497, + "tags": [ + { "key": "blockID", "type": "string", "value": "37430ec1-eb84-4ad4-9bea-64b05bc05f0b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720984, + "fields": [ + { "key": "bytes", "type": "int64", "value": 329440 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01afdbfe975f8d6d", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704258, + "duration": 16738, + "tags": [ + { "key": "blockID", "type": "string", "value": "52136585-3c3c-418c-85bb-079c46f30ee8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720994, + "fields": [ + { "key": "bytes", "type": "int64", "value": 271408 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1c881037e38b18ad", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704334, + "duration": 16710, + "tags": [ + { "key": "blockID", "type": "string", "value": "293f4ca9-60cf-4dce-84f9-90d7a8903467" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721042, + "fields": [ + { "key": "bytes", "type": "int64", "value": 448208 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "727cf2a7b14f8891", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683209, + "duration": 37913, + "tags": [], + "logs": [ + { "timestamp": 1605873894683217, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894721121, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "72a3d0dd535ed714", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683196, + "duration": 37928, + "tags": [], + "logs": [ + { "timestamp": 1605873894683202, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894721124, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6e28aae41ed950a0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683204, + "duration": 37923, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1034cca4b87566b9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683192, + "duration": 37937, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5477c4334a555c1a", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702369, + "duration": 18817, + "tags": [ + { "key": "blockID", "type": "string", "value": "65c35f00-7bf4-4d7c-884e-57e1f4f386f1" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721184, + "fields": [ + { "key": "bytes", "type": "int64", "value": 343160 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "156254fce90fef6d", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683261, + "duration": 38071, + "tags": [ + { "key": "blockID", "type": "string", "value": "5f6da848-1f43-4327-b791-c8607c834469" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721329, + "fields": [ + { "key": "bytes", "type": "int64", "value": 425008 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "18174ee576735b69", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683380, + "duration": 38087, + "tags": [], + "logs": [ + { "timestamp": 1605873894683386, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894721466, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3c984a418432da06", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683376, + "duration": 38097, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2cb4a90ec9e7ed56", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684207, + "duration": 37304, + "tags": [ + { "key": "blockID", "type": "string", "value": "da15aeab-47f3-4150-a3a0-0b899f5728e0" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721505, + "fields": [ + { "key": "bytes", "type": "int64", "value": 435448 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "31a678641a0daa14", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683185, + "duration": 38722, + "tags": [ + { "key": "blockID", "type": "string", "value": "7f859498-9292-4be9-9902-9cc0cc94db7f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721902, + "fields": [ + { "key": "bytes", "type": "int64", "value": 382184 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "27ca437dde2b9612", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683192, + "duration": 38914, + "tags": [ + { "key": "blockID", "type": "string", "value": "723cdf42-e4bc-48dc-bda5-6173eb15dee4" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894722104, + "fields": [ + { "key": "bytes", "type": "int64", "value": 374272 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "777ba94dbf7e2679", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894703342, + "duration": 18950, + "tags": [ + { "key": "blockID", "type": "string", "value": "9ffb7568-b253-46bf-ae30-275a5370abdd" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894722288, + "fields": [ + { "key": "bytes", "type": "int64", "value": 296800 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "447a1de4607678e0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894716862, + "duration": 5567, + "tags": [], + "logs": [ + { "timestamp": 1605873894716881, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894722428, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4a1cb35fb165238f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894716854, + "duration": 5582, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "263d88ba7760646a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707662, + "duration": 14938, + "tags": [], + "logs": [ + { "timestamp": 1605873894707677, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894722599, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7cb0a9332c646221", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707654, + "duration": 14951, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "178dbf7349f30deb", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682835, + "duration": 39846, + "tags": [ + { "key": "blockID", "type": "string", "value": "ae4b5b30-d87d-459f-8bfe-d05f4f169ced" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894722679, + "fields": [ + { "key": "bytes", "type": "int64", "value": 384344 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "150994409f1cb25a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700618, + "duration": 22118, + "tags": [], + "logs": [ + { "timestamp": 1605873894700633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894722736, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "585e5d65d550b215", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700613, + "duration": 22129, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "27511615066e34db", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684102, + "duration": 38879, + "tags": [], + "logs": [ + { "timestamp": 1605873894684108, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894722980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "79b947d9ed7866a6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684097, + "duration": 38891, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6f2e6507fe12975c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894717161, + "duration": 5887, + "tags": [], + "logs": [ + { "timestamp": 1605873894717174, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "00cdeb6a7479c53f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894717155, + "duration": 5899, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "24bddd4ea3487e35", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683371, + "duration": 39804, + "tags": [ + { "key": "blockID", "type": "string", "value": "d7601ebc-c33c-492c-a1da-4aa053533084" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894723171, + "fields": [ + { "key": "bytes", "type": "int64", "value": 365144 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "53d7bccf2fc103c5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894716842, + "duration": 6390, + "tags": [ + { "key": "blockID", "type": "string", "value": "ca64f28a-77dc-4745-abdc-44054c1e5e40" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894723228, + "fields": [ + { "key": "bytes", "type": "int64", "value": 289352 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "552db884462abcc8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683762, + "duration": 39518, + "tags": [], + "logs": [ + { "timestamp": 1605873894683768, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723279, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "66bfd77009ceabee", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683756, + "duration": 39531, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "39ecc86ead7ef908", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684607, + "duration": 38759, + "tags": [], + "logs": [ + { "timestamp": 1605873894684613, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723365, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "23e63f9ee6638cc5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684602, + "duration": 38769, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3995f8a937161d18", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685603, + "duration": 37861, + "tags": [], + "logs": [ + { "timestamp": 1605873894685611, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723463, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7d8cbf547f13bab8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685598, + "duration": 37871, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "50ea9bcb501f096f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707639, + "duration": 15983, + "tags": [ + { "key": "blockID", "type": "string", "value": "7ba16a23-7c29-4c4e-a0a6-f5a35697f61e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894723607, + "fields": [ + { "key": "bytes", "type": "int64", "value": 283976 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "36174ad72177e7e0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683065, + "duration": 40713, + "tags": [], + "logs": [ + { "timestamp": 1605873894683103, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723777, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6eb33f7265438984", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683062, + "duration": 40722, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0d173415b42b54df", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684351, + "duration": 39618, + "tags": [], + "logs": [ + { "timestamp": 1605873894684357, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723968, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0353b10977450cf7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684346, + "duration": 39628, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7b094f4ebdce31c3", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894717141, + "duration": 6985, + "tags": [ + { "key": "blockID", "type": "string", "value": "f1a4a13f-5e5d-484e-8b53-1f4f8e1bad37" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894724124, + "fields": [ + { "key": "bytes", "type": "int64", "value": 448472 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4bf98f62683b0e8e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700602, + "duration": 23557, + "tags": [ + { "key": "blockID", "type": "string", "value": "2c8d9e08-28c9-43e0-a38e-48e205e70c0a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894724156, + "fields": [ + { "key": "bytes", "type": "int64", "value": 454976 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3d32ea43a7ace6a3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685122, + "duration": 39094, + "tags": [], + "logs": [ + { "timestamp": 1605873894685153, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724214, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42780d9e0c2beb80", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685114, + "duration": 39108, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6d4dfd6622f9d4e5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684594, + "duration": 39780, + "tags": [ + { "key": "blockID", "type": "string", "value": "6926ecb7-efff-41c5-ae95-85e0b8e75bed" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894724372, + "fields": [ + { "key": "bytes", "type": "int64", "value": 364000 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "462c7cc77e9bde26", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684089, + "duration": 40384, + "tags": [ + { "key": "blockID", "type": "string", "value": "eacc5319-5c66-4ef0-bdc7-61ebcd665770" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894724469, + "fields": [ + { "key": "bytes", "type": "int64", "value": 375312 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "64c5e8cb8a8c9c87", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684553, + "duration": 40087, + "tags": [], + "logs": [ + { "timestamp": 1605873894684559, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724639, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a2abf3fa1e44a02", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684548, + "duration": 40098, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5185d47ca37c94b2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684289, + "duration": 40372, + "tags": [], + "logs": [ + { "timestamp": 1605873894684296, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724661, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "63f3f66dc1b96cb5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684283, + "duration": 40383, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "09dfedf04619fd00", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683527, + "duration": 41169, + "tags": [], + "logs": [ + { "timestamp": 1605873894683532, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724695, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4fcf6b895ba07eb1", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683524, + "duration": 41178, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1d3e7eff78cb43af", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684135, + "duration": 40664, + "tags": [], + "logs": [ + { "timestamp": 1605873894684142, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724800, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "43860f9f193430f0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684131, + "duration": 40675, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7ea1f5a8b4dab8a7", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684338, + "duration": 40775, + "tags": [ + { "key": "blockID", "type": "string", "value": "2906f33b-d748-4827-8eb7-de90927a65dd" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725108, + "fields": [ + { "key": "bytes", "type": "int64", "value": 431856 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "512c60978bd2eac4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721019, + "duration": 4095, + "tags": [], + "logs": [ + { "timestamp": 1605873894721028, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725112, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7e897df0e96d32b5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721010, + "duration": 4112, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "787b23c8fa301dd7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894706900, + "duration": 18234, + "tags": [], + "logs": [ + { "timestamp": 1605873894706927, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725133, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3ffd9ecc1161334a", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683055, + "duration": 42085, + "tags": [ + { "key": "blockID", "type": "string", "value": "26f1bad8-cd58-4801-8785-d52b8d833a90" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725137, + "fields": [ + { "key": "bytes", "type": "int64", "value": 414056 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2f86e3ff470976d3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894706887, + "duration": 18254, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1a42c28bcd21acc8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685104, + "duration": 40144, + "tags": [ + { "key": "blockID", "type": "string", "value": "777d1eb8-cd33-44d5-8e34-2d253bd948a6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725246, + "fields": [ + { "key": "bytes", "type": "int64", "value": 407792 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "445f67ce7c86e918", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684540, + "duration": 40905, + "tags": [ + { "key": "blockID", "type": "string", "value": "d4b28adc-eb54-4e54-8639-40acbe82196a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725443, + "fields": [ + { "key": "bytes", "type": "int64", "value": 312232 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4dc9df94476d41ef", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713432, + "duration": 12062, + "tags": [], + "logs": [ + { "timestamp": 1605873894713448, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725366, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1f47248f05173a34", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713423, + "duration": 12078, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6ab47177b7f5e532", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684151, + "duration": 41357, + "tags": [], + "logs": [ + { "timestamp": 1605873894684157, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725507, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62088478a235ead5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684146, + "duration": 41368, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "021658e91c35b26e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683518, + "duration": 42121, + "tags": [ + { "key": "blockID", "type": "string", "value": "6deff928-b65a-437a-8d56-64d2397d9d1f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725636, + "fields": [ + { "key": "bytes", "type": "int64", "value": 238936 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "48401abd95ffa153", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894712421, + "duration": 13359, + "tags": [], + "logs": [ + { "timestamp": 1605873894712431, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725779, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a6bcedc4fc18a61", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894712416, + "duration": 13371, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "551f266c080ab0c6", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684122, + "duration": 41711, + "tags": [ + { "key": "blockID", "type": "string", "value": "5180765b-50b6-4de3-abab-ceea6086afb8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725830, + "fields": [ + { "key": "bytes", "type": "int64", "value": 393392 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "72970316c65770af", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894706872, + "duration": 18990, + "tags": [ + { "key": "blockID", "type": "string", "value": "541a6a31-d25a-4275-9688-228355c81085" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725860, + "fields": [ + { "key": "bytes", "type": "int64", "value": 280296 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "635a7471f4256c0b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684275, + "duration": 41695, + "tags": [ + { "key": "blockID", "type": "string", "value": "6bf57585-a03d-44e1-bd18-081b679d3e4a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725967, + "fields": [ + { "key": "bytes", "type": "int64", "value": 434432 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7607fc837fc26251", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683802, + "duration": 42223, + "tags": [], + "logs": [ + { "timestamp": 1605873894683808, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726025, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01269c3f9d50434a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683798, + "duration": 42233, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "70114fe92b16120e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721062, + "duration": 5033, + "tags": [], + "logs": [ + { "timestamp": 1605873894721068, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726093, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "37a69429cff69860", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721058, + "duration": 5043, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5262951e45efa67d", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894720997, + "duration": 5110, + "tags": [ + { "key": "blockID", "type": "string", "value": "5c09e7bd-2f9c-42d3-8d2f-863e93eaa939" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894726103, + "fields": [ + { "key": "bytes", "type": "int64", "value": 246840 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1d6a76dd2ca6c3e6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894711590, + "duration": 14746, + "tags": [], + "logs": [ + { "timestamp": 1605873894711600, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726335, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7c75cd286737359f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894711582, + "duration": 14759, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "78415d3812916d77", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707879, + "duration": 18497, + "tags": [], + "logs": [ + { "timestamp": 1605873894707887, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726375, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5f7ac8c4fd5c680a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707874, + "duration": 18513, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b4bc2ee6a63726e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713413, + "duration": 13261, + "tags": [ + { "key": "blockID", "type": "string", "value": "b654e510-386a-470a-98c4-fb9833d21728" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894726671, + "fields": [ + { "key": "bytes", "type": "int64", "value": 384056 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5f55f469fc2d6d29", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894712403, + "duration": 14367, + "tags": [ + { "key": "blockID", "type": "string", "value": "3fca3f89-a174-460d-9a87-92ed03555497" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894726767, + "fields": [ + { "key": "bytes", "type": "int64", "value": 334896 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "19235cb1f2dccb32", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894711567, + "duration": 15276, + "tags": [ + { "key": "blockID", "type": "string", "value": "de228107-4ff6-449e-9f1f-6ab34765ab68" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894726841, + "fields": [ + { "key": "bytes", "type": "int64", "value": 225832 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "29b186beff361524", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699284, + "duration": 27562, + "tags": [], + "logs": [ + { "timestamp": 1605873894699298, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726845, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0acce3e2af327bae", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699278, + "duration": 27578, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b343c544d82bb3b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721050, + "duration": 5994, + "tags": [ + { "key": "blockID", "type": "string", "value": "459be32f-b91d-491b-aa61-5389b239eed8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894727041, + "fields": [ + { "key": "bytes", "type": "int64", "value": 429792 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62e3ccbe325de3d1", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683746, + "duration": 43412, + "tags": [ + { "key": "blockID", "type": "string", "value": "a1b8740a-6430-4113-93f4-ad9c52e42d62" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894727155, + "fields": [ + { "key": "bytes", "type": "int64", "value": 366096 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "310178852fbf88e0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682731, + "duration": 44481, + "tags": [], + "logs": [ + { "timestamp": 1605873894682741, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894727211, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42bb5e9919ea40f4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682726, + "duration": 44493, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "150c8cfeedab6a38", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894708915, + "duration": 18374, + "tags": [], + "logs": [ + { "timestamp": 1605873894708922, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894727288, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1dda89d441503741", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894708910, + "duration": 18384, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6c1c11a742626433", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707863, + "duration": 19558, + "tags": [ + { "key": "blockID", "type": "string", "value": "ef68962f-224d-4b6c-9dcd-ef9be8607c72" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894727419, + "fields": [ + { "key": "bytes", "type": "int64", "value": 423912 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4393a9fa65c8ceae", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707853, + "duration": 19639, + "tags": [], + "logs": [ + { "timestamp": 1605873894707866, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894727491, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1392dcdbb07bc781", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707848, + "duration": 19650, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0804f1e82c8828e2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713028, + "duration": 14650, + "tags": [], + "logs": [ + { "timestamp": 1605873894713040, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894727677, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4e9e2ce15a6e596c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713023, + "duration": 14661, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0c763a5ef614a2f4", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685588, + "duration": 42409, + "tags": [ + { "key": "blockID", "type": "string", "value": "65fbe578-d535-4032-a12f-f249e7405363" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894727993, + "fields": [ + { "key": "bytes", "type": "int64", "value": 441088 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "31c1f2fd1bde2d49", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682863, + "duration": 45441, + "tags": [], + "logs": [ + { "timestamp": 1605873894682870, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728303, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2a694924a7a45244", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682710, + "duration": 45639, + "tags": [ + { "key": "blockID", "type": "string", "value": "06a1301e-5b48-425e-a506-a4350afe3d0d" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728346, + "fields": [ + { "key": "bytes", "type": "int64", "value": 418168 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "73f7696fdccac589", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682859, + "duration": 45516, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4b52acf382a86008", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684137, + "duration": 44241, + "tags": [ + { "key": "blockID", "type": "string", "value": "57df43b4-8552-49e3-a1cd-f442609deaf2" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728373, + "fields": [ + { "key": "bytes", "type": "int64", "value": 425104 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "18688fac379c4a9e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707836, + "duration": 20550, + "tags": [ + { "key": "blockID", "type": "string", "value": "c53e7db4-34cd-43d0-9383-de5e40936182" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728383, + "fields": [ + { "key": "bytes", "type": "int64", "value": 389928 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5fccf30d8fc010b8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894726139, + "duration": 2327, + "tags": [], + "logs": [ + { "timestamp": 1605873894726153, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728465, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1c2bf57fc386ac18", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894726130, + "duration": 2343, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7cfea87d3c8d06ca", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685457, + "duration": 43096, + "tags": [], + "logs": [ + { "timestamp": 1605873894685465, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728552, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "12a80c97c2a42f05", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685452, + "duration": 43107, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "08122694d5e37cb7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894723200, + "duration": 5455, + "tags": [], + "logs": [ + { "timestamp": 1605873894723209, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728654, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "313e6147867246d6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894723194, + "duration": 5466, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e4f73b3315eb612", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894715649, + "duration": 13049, + "tags": [], + "logs": [ + { "timestamp": 1605873894715658, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728698, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7e0457958022516b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894715644, + "duration": 13060, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "04db9d1320bc1720", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683309, + "duration": 45454, + "tags": [], + "logs": [ + { "timestamp": 1605873894683316, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728762, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4702ca2057b120a0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894726116, + "duration": 2652, + "tags": [ + { "key": "blockID", "type": "string", "value": "ec5b73d8-61f8-4210-8838-e6cfd253294b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728766, + "fields": [ + { "key": "bytes", "type": "int64", "value": 173264 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5e2c536cf48b42a8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683305, + "duration": 45464, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "69bd1c5d5184626b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894708900, + "duration": 19981, + "tags": [ + { "key": "blockID", "type": "string", "value": "de324468-b889-4f4c-af77-907353a719cd" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728878, + "fields": [ + { "key": "bytes", "type": "int64", "value": 307032 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6849c669006c2759", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894714727, + "duration": 14480, + "tags": [], + "logs": [ + { "timestamp": 1605873894714736, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894729206, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6836249c56ca87ae", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894714722, + "duration": 14490, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4614a3c3374430a7", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682850, + "duration": 46413, + "tags": [ + { "key": "blockID", "type": "string", "value": "ea416fc3-eedc-413e-9cc1-d8fd3548cfe6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729260, + "fields": [ + { "key": "bytes", "type": "int64", "value": 392160 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "33a5b0766e98269c", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685441, + "duration": 44269, + "tags": [ + { "key": "blockID", "type": "string", "value": "b50c3305-8d80-42fc-88a4-97a8604c7066" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729705, + "fields": [ + { "key": "bytes", "type": "int64", "value": 346072 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "38da2ce44e352b40", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894722577, + "duration": 7233, + "tags": [], + "logs": [ + { "timestamp": 1605873894722588, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894729809, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2a0fc54146d07c21", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894722569, + "duration": 7247, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "398ed8e3573cf781", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894715632, + "duration": 14249, + "tags": [ + { "key": "blockID", "type": "string", "value": "2d9f964a-aac5-42e1-b410-866ad1706d7a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729879, + "fields": [ + { "key": "bytes", "type": "int64", "value": 441600 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "47c93569ac9ecd04", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713013, + "duration": 16911, + "tags": [ + { "key": "blockID", "type": "string", "value": "96c1b791-af15-4554-a1bb-b0f200625856" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729919, + "fields": [ + { "key": "bytes", "type": "int64", "value": 446504 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3d61171be03f3db4", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894714709, + "duration": 15261, + "tags": [ + { "key": "blockID", "type": "string", "value": "ae957436-40cc-4d15-a3c3-26d10613aaf6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729967, + "fields": [ + { "key": "bytes", "type": "int64", "value": 287976 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3de17f2475734d79", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894723182, + "duration": 6796, + "tags": [ + { "key": "blockID", "type": "string", "value": "eb218dd8-99fa-4de7-87cd-8998b89e0778" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729976, + "fields": [ + { "key": "bytes", "type": "int64", "value": 403400 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "712c995480f17232", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713402, + "duration": 16771, + "tags": [], + "logs": [ + { "timestamp": 1605873894713412, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894730172, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "26950bc4bf84c34b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713392, + "duration": 16787, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bd3c2e5acbea9c4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701374, + "duration": 29019, + "tags": [], + "logs": [ + { "timestamp": 1605873894701383, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894730393, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2106e0853647ac08", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701369, + "duration": 29030, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6e59a327558a7329", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699265, + "duration": 31183, + "tags": [ + { "key": "blockID", "type": "string", "value": "d8b5fee2-e2b3-445c-8ea9-243519a5c104" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894730443, + "fields": [ + { "key": "bytes", "type": "int64", "value": 400224 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "037acce8385b1858", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721017, + "duration": 9605, + "tags": [], + "logs": [ + { "timestamp": 1605873894721028, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894730621, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3547a2504168d8c7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721012, + "duration": 9617, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0dc60016513590c8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894722299, + "duration": 8583, + "tags": [ + { "key": "blockID", "type": "string", "value": "69120461-fbd8-4ca0-a5fb-957d745a22a8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894730879, + "fields": [ + { "key": "bytes", "type": "int64", "value": 370472 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5d18bf1b78bd4e75", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684396, + "duration": 46590, + "tags": [], + "logs": [ + { "timestamp": 1605873894684402, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894730985, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3bdc5547104db28c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684392, + "duration": 46600, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "682e6d017ce71dad", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713268, + "duration": 17796, + "tags": [ + { "key": "blockID", "type": "string", "value": "84644a06-da08-4c3a-936f-70a634d8052d" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894731057, + "fields": [ + { "key": "bytes", "type": "int64", "value": 353808 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3bbbdf937ccb2ccb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721935, + "duration": 9258, + "tags": [], + "logs": [ + { "timestamp": 1605873894721944, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731193, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7c40bff5b749a532", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721928, + "duration": 9272, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "31f86900e7598e55", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701358, + "duration": 29885, + "tags": [ + { "key": "blockID", "type": "string", "value": "186f31e1-409b-4c9c-95b5-abc662389d3b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894731240, + "fields": [ + { "key": "bytes", "type": "int64", "value": 434496 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7471b3c5a188e8da", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894729995, + "duration": 1327, + "tags": [], + "logs": [ + { "timestamp": 1605873894730003, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731321, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "33cb5e55876f584c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894729989, + "duration": 1338, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "58b7d0b41550e88f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894729978, + "duration": 1396, + "tags": [ + { "key": "blockID", "type": "string", "value": "e0d8f1ac-a48f-4dea-868f-183e1a124fb5" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894731373, + "fields": [ + { "key": "bytes", "type": "int64", "value": 16488 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "70405f4198f01d16", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721544, + "duration": 9893, + "tags": [], + "logs": [ + { "timestamp": 1605873894721555, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731436, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "56f0f0d7120ea5a5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721540, + "duration": 9903, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "69bb9bf8c37d9faf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683022, + "duration": 48646, + "tags": [ + { "key": "blockID", "type": "string", "value": "cf7747f5-68f9-490b-840d-9975c057c7e6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894731663, + "fields": [ + { "key": "bytes", "type": "int64", "value": 425640 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4ef61721dfd7f61b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894720840, + "duration": 10841, + "tags": [], + "logs": [ + { "timestamp": 1605873894720869, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731680, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "70e8aa6d13e56007", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894720827, + "duration": 10860, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "297ff96c736c18f4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894705333, + "duration": 26573, + "tags": [], + "logs": [ + { "timestamp": 1605873894705342, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731904, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a80209f067be4f5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894705328, + "duration": 26585, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5b3db530e83db855", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894731272, + "duration": 796, + "tags": [], + "logs": [ + { "timestamp": 1605873894731284, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894732067, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b2458a9486a4298", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894731265, + "duration": 886, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3139145bb422702e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894731251, + "duration": 941, + "tags": [ + { "key": "blockID", "type": "string", "value": "f9fd03ba-91a8-476b-b809-d2b11bfa790d" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894732190, + "fields": [ + { "key": "bytes", "type": "int64", "value": 8680 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + } + ], + "processes": { + "p1": { + "serviceName": "s1" + } + }, + "warnings": null + } + ], + "total": 0, + "limit": 0, + "offset": 0, + "errors": null +} diff --git a/e2e-playwright/fixtures/tempo-response.json b/e2e-playwright/fixtures/tempo-response.json new file mode 100644 index 00000000000..30136a226a5 --- /dev/null +++ b/e2e-playwright/fixtures/tempo-response.json @@ -0,0 +1,1181 @@ +{ + "results": { + "A": { + "frames": [ + { + "schema": { + "name": "Trace", + "refId": "A", + "meta": { + "preferredVisualisationType": "trace" + }, + "fields": [ + { + "name": "traceID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "spanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "parentSpanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "operationName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceTags", + "type": "other", + "typeInfo": { + "frame": "other" + } + }, + { + "name": "startTime", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "duration", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "logs", + "type": "other", + "typeInfo": { + "frame": "other" + } + }, + { + "name": "tags", + "type": "other", + "typeInfo": { + "frame": "other" + } + } + ] + }, + "data": { + "values": [ + [ + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599" + ], + [ + "4829080550953998599", + "2017565205134657253", + "8342878680821632593", + "1224460974534844837", + "275963539390240345", + "8319197348330196958", + "6652870949263121722", + "607757742690372462", + "4351457163250103362", + "6171925700166476032", + "7554371986156978275" + ], + [ + null, + "4829080550953998599", + "2017565205134657253", + "8342878680821632593", + "1224460974534844837", + "275963539390240345", + "4829080550953998599", + "6652870949263121722", + "607757742690372462", + "4351457163250103362", + "6171925700166476032" + ], + [ + "HTTP Client", + "HTTP POST", + "HTTP POST - post", + "HTTP Client", + "HTTP POST", + "HTTP POST - post", + "HTTP GET", + "HTTP GET - root", + "HTTP Client", + "HTTP GET", + "HTTP GET - root" + ], + ["lb", "lb", "app", "app", "app", "db", "lb", "app", "app", "app", "db"], + [ + [ + { + "value": "lb", + "key": "service.name" + }, + { + "value": "tns/loadgen", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "f55ab7e317a6", + "key": "host.name" + }, + { + "value": "172.24.0.8", + "key": "ip" + }, + { + "value": "36042b5ca9f81d60", + "key": "client-uuid" + } + ], + [ + { + "value": "lb", + "key": "service.name" + }, + { + "value": "tns/loadgen", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "f55ab7e317a6", + "key": "host.name" + }, + { + "value": "172.24.0.8", + "key": "ip" + }, + { + "value": "36042b5ca9f81d60", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "db", + "key": "service.name" + }, + { + "value": "tns/db", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "aae464791221", + "key": "host.name" + }, + { + "value": "172.24.0.2", + "key": "ip" + }, + { + "value": "18b1a6b5278dd643", + "key": "client-uuid" + } + ], + [ + { + "value": "lb", + "key": "service.name" + }, + { + "value": "tns/loadgen", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "f55ab7e317a6", + "key": "host.name" + }, + { + "value": "172.24.0.8", + "key": "ip" + }, + { + "value": "36042b5ca9f81d60", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "db", + "key": "service.name" + }, + { + "value": "tns/db", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "aae464791221", + "key": "host.name" + }, + { + "value": "172.24.0.2", + "key": "ip" + }, + { + "value": "18b1a6b5278dd643", + "key": "client-uuid" + } + ] + ], + [ + 1620761543730.354, 1620761543730.3628, 1620761543732.044, 1620761543732.163, 1620761543732.191, + 1620761543733.925, 1620761543735.194, 1620761543738.197, 1620761543738.394, 1620761543738.398, + 1620761543744.6929 + ], + [17.826, 4.824, 2.588, 2.403, 2.401, 0.142, 13.266, 11.281, 8.593, 10.87, 0.136], + [ + null, + [ + { + "timestamp": 1620761543730.414, + "fields": [ + { + "value": "GetConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543730.475, + "fields": [ + { + "value": "DNSStart", + "key": "event" + }, + { + "value": "app", + "key": "host" + } + ] + }, + { + "timestamp": 1620761543731.1738, + "fields": [ + { + "value": "DNSDone", + "key": "event" + }, + { + "value": "172.24.0.7", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543731.184, + "fields": [ + { + "value": "ConnectStart", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.7: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543731.6228, + "fields": [ + { + "value": "ConnectDone", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.7: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543731.678, + "fields": [ + { + "value": "GotConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543731.737, + "fields": [ + { + "value": "WroteHeaders", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543731.742, + "fields": [ + { + "value": "WroteRequest", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543735.001, + "fields": [ + { + "value": "GotFirstResponseByte", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543735.186, + "fields": [ + { + "value": "ClosedBody", + "key": "event" + } + ] + } + ], + null, + null, + [ + { + "timestamp": 1620761543732.209, + "fields": [ + { + "value": "GetConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543732.236, + "fields": [ + { + "value": "DNSStart", + "key": "event" + }, + { + "value": "db", + "key": "host" + } + ] + }, + { + "timestamp": 1620761543733.196, + "fields": [ + { + "value": "DNSDone", + "key": "event" + }, + { + "value": "172.24.0.2", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543733.203, + "fields": [ + { + "value": "ConnectStart", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.2: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543733.459, + "fields": [ + { + "value": "ConnectDone", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.2: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543733.549, + "fields": [ + { + "value": "GotConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543733.613, + "fields": [ + { + "value": "WroteHeaders", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543733.618, + "fields": [ + { + "value": "WroteRequest", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543734.241, + "fields": [ + { + "value": "GotFirstResponseByte", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543734.592, + "fields": [ + { + "value": "ClosedBody", + "key": "event" + } + ] + } + ], + null, + [ + { + "timestamp": 1620761543735.23, + "fields": [ + { + "value": "GetConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543735.277, + "fields": [ + { + "value": "DNSStart", + "key": "event" + }, + { + "value": "app", + "key": "host" + } + ] + }, + { + "timestamp": 1620761543736.9658, + "fields": [ + { + "value": "DNSDone", + "key": "event" + }, + { + "value": "172.24.0.7", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543736.9758, + "fields": [ + { + "value": "ConnectStart", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.7: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543737.705, + "fields": [ + { + "value": "ConnectDone", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.7: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543737.773, + "fields": [ + { + "value": "GotConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543737.8098, + "fields": [ + { + "value": "WroteHeaders", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543737.811, + "fields": [ + { + "value": "WroteRequest", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543747.939, + "fields": [ + { + "value": "GotFirstResponseByte", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543748.46, + "fields": [ + { + "value": "ClosedBody", + "key": "event" + } + ] + } + ], + null, + null, + [ + { + "timestamp": 1620761543738.446, + "fields": [ + { + "value": "GetConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543738.502, + "fields": [ + { + "value": "DNSStart", + "key": "event" + }, + { + "value": "db", + "key": "host" + } + ] + }, + { + "timestamp": 1620761543743.861, + "fields": [ + { + "value": "DNSDone", + "key": "event" + }, + { + "value": "172.24.0.2", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543743.918, + "fields": [ + { + "value": "ConnectStart", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.2: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543744.228, + "fields": [ + { + "value": "ConnectDone", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.2: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543744.345, + "fields": [ + { + "value": "GotConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543744.414, + "fields": [ + { + "value": "WroteHeaders", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543744.416, + "fields": [ + { + "value": "WroteRequest", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543746.825, + "fields": [ + { + "value": "GotFirstResponseByte", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543749.2668, + "fields": [ + { + "value": "ClosedBody", + "key": "event" + } + ] + } + ], + null + ], + [ + [ + { + "value": "const", + "key": "sampler.type" + }, + { + "value": true, + "key": "sampler.param" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 302, + "key": "http.status_code" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "POST", + "key": "http.method" + }, + { + "value": "app: 80", + "key": "http.url" + }, + { + "value": false, + "key": "net/http.reused" + }, + { + "value": false, + "key": "net/http.was_idle" + }, + { + "value": "client", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 302, + "key": "http.status_code" + }, + { + "value": "POST", + "key": "http.method" + }, + { + "value": "/post", + "key": "http.url" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "server", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 204, + "key": "http.status_code" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "POST", + "key": "http.method" + }, + { + "value": "db: 80", + "key": "http.url" + }, + { + "value": false, + "key": "net/http.reused" + }, + { + "value": false, + "key": "net/http.was_idle" + }, + { + "value": "client", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 204, + "key": "http.status_code" + }, + { + "value": "POST", + "key": "http.method" + }, + { + "value": "/post", + "key": "http.url" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "server", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 200, + "key": "http.status_code" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "GET", + "key": "http.method" + }, + { + "value": "app: 80", + "key": "http.url" + }, + { + "value": false, + "key": "net/http.reused" + }, + { + "value": false, + "key": "net/http.was_idle" + }, + { + "value": "client", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 200, + "key": "http.status_code" + }, + { + "value": "GET", + "key": "http.method" + }, + { + "value": "/", + "key": "http.url" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "server", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 200, + "key": "http.status_code" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "GET", + "key": "http.method" + }, + { + "value": "db: 80", + "key": "http.url" + }, + { + "value": false, + "key": "net/http.reused" + }, + { + "value": false, + "key": "net/http.was_idle" + }, + { + "value": "client", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 200, + "key": "http.status_code" + }, + { + "value": "GET", + "key": "http.method" + }, + { + "value": "/", + "key": "http.url" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "server", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ] + ] + ] + } + } + ] + } + } +} diff --git a/e2e-playwright/panels-suite/dashlist.spec.ts b/e2e-playwright/panels-suite/dashlist.spec.ts new file mode 100644 index 00000000000..db4a8cccc9d --- /dev/null +++ b/e2e-playwright/panels-suite/dashlist.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'a6801696-cc53-4196-b1f9-2403e3909185/panel-tests-dashlist-variables'; + +test.describe( + 'Panels test: DashList panel', + { + tag: '@panels', + }, + () => { + // this is to prevent the fix for https://github.com/grafana/grafana/issues/76800 from regressing + test('should pass current variable values correctly when `Include current template variable values` is set', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + // check the initial value of the urls contain the variable value correctly + const panel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Include time range and variables enabled') + ); + await expect(panel).toBeVisible(); + + const links = panel.locator('a'); + const linkCount = await links.count(); + for (let i = 0; i < linkCount; i++) { + const href = await links.nth(i).getAttribute('href'); + expect(href).toContain('var-server=A'); + } + + // update variable to b + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('server')) + .locator('..') + .locator('input'); + await serverVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + // blur the dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // check the urls are updated with the new variable value + await expect(panel).toBeVisible(); + const updatedLinks = panel.locator('a'); + const updatedLinkCount = await updatedLinks.count(); + for (let i = 0; i < updatedLinkCount; i++) { + const href = await updatedLinks.nth(i).getAttribute('href'); + expect(href).toContain('var-server=B'); + } + }); + } +); diff --git a/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts new file mode 100644 index 00000000000..4cdb7afb081 --- /dev/null +++ b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; + +const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; + +test.beforeAll(async ({ request }) => { + // Import the dashboard + await request.post('/api/dashboards/import', { + data: { + dashboard: panelSandboxDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); +}); + +test.describe( + 'Panels test: Panel sandbox', + { + tag: ['@panels'], + }, + () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + await page.reload(); + }); + + test('Add iframes to body', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button adds iframes to the body + await page.locator('[data-testid="button-create-iframes"]').click(); + + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; + + for (const id of iframeIds) { + await expect(page.locator(`#${id}`)).toBeVisible(); + } + }); + + test('Reaches out of panel div', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button reaches out of the panel div and modifies the element dataset + await page.locator('[data-testid="button-reach-out"]').click(); + await expect(page.locator('[data-sandbox-test="true"]')).toBeVisible(); + }); + + test('Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); + await expect(input).toBeEnabled(); + await expect(input).toHaveValue(''); + + await input.fill('x'); + await expect(input).toHaveValue('x'); + await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeVisible(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + await page.reload(); + }); + + test('Does not add iframes to body', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button adds 3 iframes to the body + await page.locator('[data-testid="button-create-iframes"]').click(); + + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; + + for (const id of iframeIds) { + await expect(page.locator(`#${id}`)).toBeHidden(); + } + }); + + test('Does not reaches out of panel div', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button reaches out of the panel div and modifies the element dataset + await page.locator('[data-testid="button-reach-out"]').click(); + await expect(page.locator('[data-sandbox-test="true"]')).toBeHidden(); + }); + + test('Does not Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); + await expect(input).toBeEnabled(); + + await input.fill('x'); + await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeHidden(); + }); + + test('Can access specific window global variables', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + await page.locator('[data-testid="button-test-globals"]').click(); + await expect(page.locator('[data-sandbox-global="Prism"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="jQuery"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="location"]')).toBeVisible(); + }); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-layer-types.spec.ts b/e2e-playwright/panels-suite/geomap-layer-types.spec.ts new file mode 100644 index 00000000000..ba0d80d002e --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-layer-types.spec.ts @@ -0,0 +1,137 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +const MAP_LAYERS_TYPE = 'Map layers Layer type'; +const MAP_LAYERS_DATA = 'Map layers Data'; +const MAP_LAYERS_GEOJSON = 'Map layers GeoJSON URL'; + +test.describe( + 'Panels test: Geomap layer types', + { + tag: ['@panels'], + }, + () => { + test('Tests changing the layer type', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toBeVisible(); + const field = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE) + ); + await expect(field).toBeVisible(); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('markers'); + + // Heatmap + const input = field.locator('input'); + await input.fill('Heatmap'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('heatmap'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + + // GeoJSON + await input.fill('GeoJSON'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('geojson'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON)) + ).toBeVisible(); + + // Open Street Map + await input.fill('Open Street Map'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('osm-standard'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON)) + ).toBeHidden(); + + // CARTO basemap + await input.fill('CARTO basemap'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('carto'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme')) + ).toBeVisible(); + + // ArcGIS MapServer + await input.fill('ArcGIS MapServer'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('esri-xyz'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance') + ) + ).toBeVisible(); + + // XYZ Tile layer + await input.fill('XYZ Tile layer'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('xyz'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution') + ) + ).toBeVisible(); + + // Night / Day (Alpha) + await input.fill('Night / Day'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('dayNight'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Night region color') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Display sun') + ) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Route (Alpha) + await input.fill('Route'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('route'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Location Mode') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Style')) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-map-controls.spec.ts b/e2e-playwright/panels-suite/geomap-map-controls.spec.ts new file mode 100644 index 00000000000..de415dc8301 --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-map-controls.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +test.describe( + 'Panels test: Geomap layer controls options', + { + tag: ['@panels'], + }, + () => { + test('Tests map controls options', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + // Wait until the query editor has been loaded by ensuring that the QueryEditor select contains the text 'flight_info_by_state.csv' + await expect( + page.locator(selectors.components.Select.singleValue('')).getByText('flight_info_by_state.csv') + ).toBeVisible(); + + const mapControlsGroup = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('Map controls') + ); + await expect(mapControlsGroup).toBeVisible(); + + // Show zoom field + const showZoomField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showZoomField); + await expect(showZoomField).toBeVisible(); + const zoomCheckbox = showZoomField.locator('input[type="checkbox"]'); + await zoomCheckbox.check({ force: true }); + await expect(zoomCheckbox).toBeChecked(); + + // Show attribution + const showAttributionField = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.showAttributionField + ); + await expect(showAttributionField).toBeVisible(); + const attributionCheckbox = showAttributionField.locator('input[type="checkbox"]'); + await attributionCheckbox.check({ force: true }); + await expect(attributionCheckbox).toBeChecked(); + + // Show scale + const showScaleField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showScaleField); + await expect(showScaleField).toBeVisible(); + const scaleCheckbox = showScaleField.locator('input[type="checkbox"]'); + await scaleCheckbox.check({ force: true }); + await expect(scaleCheckbox).toBeChecked(); + + // Show measure tool + const showMeasureField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showMeasureField); + await expect(showMeasureField).toBeVisible(); + const measureCheckbox = showMeasureField.locator('input[type="checkbox"]'); + await measureCheckbox.check({ force: true }); + await expect(measureCheckbox).toBeChecked(); + + // Show debug + const showDebugField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showDebugField); + await expect(showDebugField).toBeVisible(); + const debugCheckbox = showDebugField.locator('input[type="checkbox"]'); + await debugCheckbox.check({ force: true }); + await expect(debugCheckbox).toBeChecked(); + + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content); + await expect(panelContent).toBeVisible(); + + // Verify zoom + await expect(panelContent.locator('.ol-zoom')).toBeVisible(); + + // Verify attribution + await expect(panelContent.locator('.ol-attribution')).toBeVisible(); + + // Verify scale + await expect(panelContent.locator('.ol-scale-line')).toBeVisible(); + + // Verify measure tool + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.measureButton)).toBeVisible(); + + // Verify debug tool + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DebugOverlay.wrapper)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts b/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts new file mode 100644 index 00000000000..10df0887a0f --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts @@ -0,0 +1,194 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +test.describe( + 'Panels test: Geomap spatial operations', + { + tag: ['@panels'], + }, + () => { + test('Tests location auto option', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Auto'); + await locationInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Point')).toBeVisible(); + }); + + test('Tests location coords option', async ({ gotoDashboardPage, dashboardPage, selectors }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Coords'); + await locationInput.press('Enter'); + + const latitudeField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel + ); + await expect(latitudeField).toBeVisible(); + const latitudeInput = latitudeField.locator('input'); + await latitudeInput.fill('Lat'); + await latitudeInput.press('Enter'); + + const longitudeField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel + ); + await expect(longitudeField).toBeVisible(); + const longitudeInput = longitudeField.locator('input'); + await longitudeInput.fill('Lng'); + await longitudeInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Point')).toBeVisible(); + }); + + test('Tests geoshash field column appears in table view', async ({ + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Geohash'); + await locationInput.press('Enter'); + + const geohashField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.geohash.geohashFieldLabel + ); + await expect(geohashField).toBeVisible(); + const geohashFieldInput = geohashField.locator('input'); + await geohashFieldInput.fill('State'); + await geohashFieldInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('State 1')).toBeVisible(); + }); + + test('Tests location lookup option', async ({ gotoDashboardPage, dashboardPage, selectors }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Lookup'); + await locationInput.press('Enter'); + + const lookupField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel + ); + await expect(lookupField).toBeVisible(); + const lookupFieldInput = lookupField.locator('input'); + await lookupFieldInput.fill('State'); + await lookupFieldInput.press('Enter'); + + const gazetteerField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel + ); + await expect(gazetteerField).toBeVisible(); + const gazetteerFieldInput = gazetteerField.locator('input'); + await gazetteerFieldInput.fill('USA States'); + await gazetteerFieldInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Geometry')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/panelEdit_base.spec.ts b/e2e-playwright/panels-suite/panelEdit_base.spec.ts new file mode 100644 index 00000000000..66584d0387b --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_base.spec.ts @@ -0,0 +1,109 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PANEL_UNDER_TEST = 'Lines 500 data points'; + +test.describe( + 'Panels test: Panel edit base', + { + tag: ['@panels'], + }, + () => { + test('Tests various Panel edit scenarios', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'TkZXxlNG3' }); + + const panelTitle = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await expect(panelTitle).toBeVisible(); + + // Check that the panel is visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.loadingBar(''))).toHaveCount(0); + await expect(panelTitle.locator('[data-testid="uplot-main-div"]').first()).toBeVisible(); + + // Open panel menu and click edit + await panelTitle.hover(); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')).click(); + + // New panel editor opens when navigating from Panel menu + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Queries tab is rendered and open by default + const dataPane = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.DataPane.content); + await expect(dataPane).toBeVisible(); + + // Check that Queries tab is visible and active + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries'))).toBeVisible(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Queries1'); // there's already a query so therefore Query + 1 + + // Check query content is visible and other tabs are not + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.AlertTab.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelAlertTabContent.content)).toBeHidden(); + + // Can change to Transform tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Transformations0'); // no transforms so Transform + 0 + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton) + ).toBeVisible(); + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.AlertTab.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelAlertTabContent.content)).toBeHidden(); + + // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Alert')).click(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Alert0'); // no alert so Alert + 0 + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeHidden(); + + // Go back to Queries tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries')).click(); + + // Check that Time series is chosen + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Time series' + ); + + // Check that table view works + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.loadingBar(''))).toHaveCount(0); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('A-series')).toBeVisible(); + + // Change to Text panel + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Text')).click(); + // Check current visualization shows Text + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Text' + ); + + // Data pane should not be rendered + await expect(dataPane).toBeHidden(); + + // Change to Table panel + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Table')).click(); + // Check current visualization shows Table + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Table' + ); + + // Data pane should be rendered + await expect(dataPane).toBeVisible(); + + // Field & Overrides tabs (need to switch to React based vis, i.e. Table) + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Column width') + ) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/panelEdit_queries.spec.ts b/e2e-playwright/panels-suite/panelEdit_queries.spec.ts new file mode 100644 index 00000000000..e60a7cc32b3 --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_queries.spec.ts @@ -0,0 +1,118 @@ +import { Locator, Page } from 'playwright-core'; + +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +test.describe( + 'Panels test: Queries', + { + tag: ['@panels'], + }, + () => { + test('Tests various Panel edit queries scenarios', async ({ selectors, gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: '5SdHCadmz', + queryParams: new URLSearchParams({ editPanel: '3' }), + }); + + // New panel editor opens when navigating from Panel menu + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Queries tab is rendered and open by default + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.DataPane.content)).toBeVisible(); + + // We expect row with refId A to exist and be visible + const initialRows = dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows); + await expect(initialRows).toHaveCount(1); + + // Add query button should be visible and clicking on it should create a new row + await dashboardPage.getByGrafanaSelector(selectors.components.QueryTab.addQuery).click(); + + // We expect row with refId A and B to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2); + + // Remove refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Remove query')) + .first() + .click(); + + // We expect row with refId B to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(1); + + // Duplicate refId B + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Duplicate query')) + .first() + .click(); + + // We expect row with refId B and A to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2); + + // Change to CSV Metric Values scenario for A + const scenarioSelect = dashboardPage + .getByGrafanaSelector(selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer) + .first(); + await scenarioSelect.locator('input[id*="test-data-scenario-select-"]').first().click(); + await page.getByText('CSV Metric Values').first().click(); + + // Verify both queries are present in inspector + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + const resultIds = new Set([ + keyTexts[length - 2], // last 2 + keyTexts[length - 1], // last 2 + ]); + + expect(resultIds.has('A:')).toBe(true); + expect(resultIds.has('B:')).toBe(true); + }); + + // Hide response for row with refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Hide response')) + .nth(1) + .click(); + + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + expect(keyTexts[length - 1]).toBe('B:'); + }); + + // Show response for row with refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Hide response')) + .nth(1) + .click(); + + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + const resultIds = new Set([ + keyTexts[length - 2], // last 2 + keyTexts[length - 1], // last 2 + ]); + + expect(resultIds.has('A:')).toBe(true); + expect(resultIds.has('B:')).toBe(true); + }); + }); + } +); + +const expectInspectorResultAndClose = async ( + page: Page, + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + expectCallback: (keys: Locator) => Promise +) => { + await dashboardPage.getByGrafanaSelector(selectors.components.QueryTab.queryInspectorButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.refreshButton).click(); + + const keys = page.locator(selectors.components.PanelInspector.Query.jsonObjectKeys('')); + await expect(keys.first()).toBeVisible(); + await expectCallback(keys); + + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click(); +}; diff --git a/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts b/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts new file mode 100644 index 00000000000..ed38aaedcbb --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Panels test: Transformations', + { + tag: ['@panels'], + }, + () => { + test('Tests transformations editor', async ({ selectors, gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage({ + uid: 'TkZXxlNG3', + queryParams: new URLSearchParams({ editPanel: '47' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Reduce')).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.Reduce.calculationsLabel) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Transforms.Reduce.modeLabel)).toBeVisible(); + }); + + test('Tests case where transformations can be disabled and not clear out panel data', async ({ + selectors, + gotoDashboardPage, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: 'TkZXxlNG3', + queryParams: new URLSearchParams({ editPanel: '47' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Reduce')).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.disableTransformationButton).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts b/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts new file mode 100644 index 00000000000..b88dbbaa196 --- /dev/null +++ b/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'grafana-azure-monitor-datasource' }); + + await expect(await page.getByText('Type: Azure Monitor', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Authentication', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts b/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts new file mode 100644 index 00000000000..2932175da33 --- /dev/null +++ b/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'stackdriver' }); + + await expect(await page.getByText('Type: Google Cloud Monitoring', { exact: true })).toBeVisible(); + await expect(await page.getByText('Google JWT File', { exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts b/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts new file mode 100644 index 00000000000..c0f181560f0 --- /dev/null +++ b/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'cloudwatch' }); + + await expect(await page.getByText('Type: CloudWatch', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection Details', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts b/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts new file mode 100644 index 00000000000..41badfb29e3 --- /dev/null +++ b/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'elasticsearch' }); + + await expect(await page.getByText('Type: Elasticsearch', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts b/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts new file mode 100644 index 00000000000..a1c1a1f6fbd --- /dev/null +++ b/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'graphite' }); + + await expect(await page.getByText('Type: Graphite', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts b/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts new file mode 100644 index 00000000000..923106ebf0e --- /dev/null +++ b/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'influxdb' }); + + await expect(await page.getByText('Type: InfluxDB', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts b/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts new file mode 100644 index 00000000000..17373dcdce8 --- /dev/null +++ b/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'jaeger' }); + + await expect(await page.getByText('Type: Jaeger', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/loki/loki.spec.ts b/e2e-playwright/plugin-e2e/loki/loki.spec.ts similarity index 53% rename from e2e/plugin-e2e/loki/loki.spec.ts rename to e2e-playwright/plugin-e2e/loki/loki.spec.ts index 25c459140cc..4e7af47b071 100644 --- a/e2e/plugin-e2e/loki/loki.spec.ts +++ b/e2e-playwright/plugin-e2e/loki/loki.spec.ts @@ -3,6 +3,6 @@ import { test, expect } from '@grafana/plugin-e2e'; test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { await createDataSourceConfigPage({ type: 'loki' }); - await expect(await page.getByText('Type: Loki', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + await expect(page.getByText('Type: Loki', { exact: true })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); }); diff --git a/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts b/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts new file mode 100644 index 00000000000..7590562c548 --- /dev/null +++ b/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'mssql' }); + + await expect(await page.getByText('Type: Microsoft SQL Server', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/mysql/mocks/mysql.mocks.ts b/e2e-playwright/plugin-e2e/mysql/mocks/mysql.mocks.ts similarity index 100% rename from e2e/plugin-e2e/mysql/mocks/mysql.mocks.ts rename to e2e-playwright/plugin-e2e/mysql/mocks/mysql.mocks.ts diff --git a/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts b/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts new file mode 100644 index 00000000000..9bd0f85b7de --- /dev/null +++ b/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { tableNameWithSpecialCharacter } from './mocks/mysql.mocks'; +import { mockDataSourceRequest } from './utils'; + +test.beforeEach(mockDataSourceRequest); + +test( + 'code editor autocomplete should handle table name escaping/quoting', + { + tag: '@plugins', + }, + async ({ explorePage, selectors, page }) => { + await page.getByLabel('Code').check(); + + const editor = explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox'); + await editor.fill('S'); + await page.getByLabel('SELECT FROM ').locator('a').click(); + await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); + await page.keyboard.press('Enter'); + + await expect(editor).toHaveValue(`SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``); + + for (let i = 0; i < tableNameWithSpecialCharacter.length + 2; i++) { + await page.keyboard.press('Backspace'); + } + + await page.keyboard.press('Control+I'); + await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/mysql/utils.ts b/e2e-playwright/plugin-e2e/mysql/utils.ts similarity index 100% rename from e2e/plugin-e2e/mysql/utils.ts rename to e2e-playwright/plugin-e2e/mysql/utils.ts diff --git a/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts b/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts new file mode 100644 index 00000000000..9cf6c580927 --- /dev/null +++ b/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts @@ -0,0 +1,176 @@ +import { selectors } from '@grafana/e2e-selectors'; +import { test, expect } from '@grafana/plugin-e2e'; + +import { normalTableName } from './mocks/mysql.mocks'; +import { mockDataSourceRequest } from './utils'; + +test.beforeEach(mockDataSourceRequest); + +test.describe( + 'mysql', + { + tag: '@plugins', + }, + () => { + test('visual query builder should handle macros', async ({ explorePage, page }) => { + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); + await page.getByText(normalTableName, { exact: true }).click(); + + // Open Data operations + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).click(); + const select = page.getByLabel('Select options menu'); + await select.locator(page.getByText('$__timeGroupAlias')).click(); + + // Open column selector + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) + .click(); + await select.locator(page.getByText('createdAt')).click(); + + // Open Interval selector + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Interval')) + .click(); + await select.locator(page.getByText('$__interval')).click(); + + await page.getByRole('button', { name: 'Add column' }).click(); + + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).nth(1).click(); + await select.locator(page.getByText('AVG')).click(); + + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) + .nth(1) + .click(); + await select.locator(page.getByText('bigint')).click(); + + // Validate the query + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue( + `SELECT\n $__timeGroupAlias(createdAt, $__interval),\n AVG(\`bigint\`)\nFROM\n DataMaker.normalTable\nLIMIT\n 50` + ); + }); + + test('visual query builder should handle time filter macro', async ({ explorePage, page }) => { + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); + await page.getByText(normalTableName, { exact: true }).click(); + + // Open column selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectColumn).click(); + const select = page.getByLabel('Select options menu'); + await select.locator(page.getByText('createdAt')).click(); + + // Toggle where row + await page.getByLabel('Filter').last().click(); + + // Click add filter button + await page.getByRole('button', { name: 'Add filter' }).click(); + await page.getByRole('button', { name: 'Add filter' }).click(); // For some reason we need to click twice + + // Open field selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterField).click(); + await select.locator(page.getByText('createdAt')).click(); + + // Open operator selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); + await select.locator(page.getByText('Macros')).click(); + + // Open macros value selector + await explorePage.getByGrafanaSelector('Macros value selector').click(); + await select.locator(page.getByText('timeFilter', { exact: true })).click(); + + // Validate that the timeFilter macro was added + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue( + `SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n $__timeFilter(createdAt)\nLIMIT\n 50` + ); + + // Validate that the timeFilter macro was removed when changed to equals operator + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); + await select.locator(page.getByText('==')).click(); + + await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).click(); + await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).blur(); + + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).not.toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n createdAt = NULL\nLIMIT\n 50`); + }); + + test('visual query builder should not crash when filter is set to select_any_in', async ({ explorePage, page }) => { + const queryParams = new URLSearchParams(); + queryParams.set('schemaVersion', '1'); + queryParams.set('orgId', '1'); + const panes = { + mmm: { + datasource: 'P4FDCC188E688367F', + queries: [ + { + refId: 'A', + datasource: { + type: 'mysql', + uid: 'P4FDCC188E688367F', + }, + format: 'table', + rawSql: "SELECT * FROM DataMaker.normalTable WHERE name IN ('a') LIMIT 50 ", + editorMode: 'builder', + sql: { + columns: [ + { + type: 'function', + parameters: [ + { + type: 'functionParameter', + name: '*', + }, + ], + }, + ], + groupBy: [ + { + type: 'groupBy', + property: { + type: 'string', + }, + }, + ], + limit: 50, + whereJsonTree: { + id: 'baa99aa9-0123-4456-b89a-b195d1dcfc6a', + type: 'group', + children1: [ + { + type: 'rule', + id: 'bb9a8bba-89ab-4cde-b012-3195d1dd2c91', + properties: { + fieldSrc: 'field', + field: 'name', + operator: 'select_any_in', + value: ['a'], + valueSrc: ['value'], + valueType: ['text'], + }, + }, + ], + }, + whereString: "name IN ('a')", + }, + dataset: 'DataMaker', + table: 'normalTable', + }, + ], + }, + }; + queryParams.set('panes', JSON.stringify(panes)); + + await explorePage.goto({ queryParams }); + + // Validate the query + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue(`SELECT\n *\nFROM\n DataMaker.normalTable\nWHERE\n name IN ('a')\nLIMIT\n 50`); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts b/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts new file mode 100644 index 00000000000..1bab8050be5 --- /dev/null +++ b/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'opentsdb' }); + + await expect(await page.getByText('Type: OpenTSDB', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/README.md b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/README.md similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/README.md rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/README.md diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts new file mode 100644 index 00000000000..676de366071 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts @@ -0,0 +1,31 @@ +import * as e2e from '@grafana/e2e-selectors'; +import { expect, test } from '@grafana/plugin-e2e'; + +// let's disable the feature toggles for now, otherwise the getAlertRuleQueryRow fails and I don't see any other way to get the query row +test.use({ featureToggles: { alertingQueryAndExpressionsStepMode: false, alertingNotificationsStepMode: false } }); + +test.describe('plugin-e2e-api-tests admin', { tag: ['@plugins'] }, () => { + test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({ + page, + alertRuleEditPage, + }) => { + await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule'); + + //add working query + const queryA = alertRuleEditPage.getAlertRuleQueryRow('A'); + await queryA.datasource.set('gdev-prometheus'); + await queryA.locator.getByLabel('Code').click(); + await page.waitForFunction(() => window.monaco); + await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); + await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))'); + + //add broken query + const newQuery = await alertRuleEditPage.clickAddQueryRow(); + await newQuery.datasource.set('gdev-prometheus'); + await newQuery.locator.getByLabel('Code').click(); + await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); + await page.keyboard.insertText('topk(5,'); + + await expect(alertRuleEditPage.evaluate()).not.toBeOK(); + }); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts similarity index 62% rename from e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts index 1a11fdd6a56..8ea363adab1 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts @@ -41,13 +41,15 @@ const scenarios: Scenario[] = [ }, ]; -for (const scenario of scenarios) { - test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => { - annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status); - await annotationEditPage.datasource.set('gdev-testdata'); - await page.getByLabel('Scenario').last().fill('CSV Content'); - await page.keyboard.press('Tab'); - await annotationEditPage.runQuery(); - await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text }); - }); -} +test.describe('plugin-e2e-api-tests admin', { tag: ['@plugins'] }, () => { + for (const scenario of scenarios) { + test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => { + annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status); + await annotationEditPage.datasource.set('gdev-testdata'); + await page.getByLabel('Scenario').last().fill('CSV Content'); + await page.keyboard.press('Tab'); + await annotationEditPage.runQuery(); + await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text }); + }); + } +}); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts new file mode 100644 index 00000000000..6a1264d9c72 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('add panel in already existing dashboard', async ({ gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.addPanel(); + await expect(page.url()).toContain('editPanel'); + }); + + test('add panel in new dashboard', async ({ dashboardPage, page }) => { + const panelEditPage = await dashboardPage.addPanel(); + await expect(panelEditPage.panel.locator).toBeVisible(); + await expect(page.url()).toContain('editPanel'); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts new file mode 100644 index 00000000000..3d6f2e924a4 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('test createDataSourceConfigPage fixture, saveAndTest and toBeOK matcher', () => { + test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => { + const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); + await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); + await expect( + configPage.saveAndTest(), + formatExpectError('Expected save data source config to fail when Prometheus server is not running') + ).not.toBeOK(); + }); + + test('valid credentials should return a 200 status code', async ({ createDataSourceConfigPage, page }) => { + const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); + configPage.mockHealthCheckResponse({ status: 200 }); + await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); + await expect( + configPage.saveAndTest(), + formatExpectError('Expected data source config to be successfully saved') + ).toBeOK(); + }); + }); + + test.describe('test data source with frontend only health check', () => { + test('valid credentials should display a success alert on the page', async ({ + createDataSourceConfigPage, + page, + }) => { + const configPage = await createDataSourceConfigPage({ type: 'zipkin' }); + configPage.mockHealthCheckResponse({ message: 'Data source is working', status: 'OK' }, 200); + await page.getByPlaceholder('http://localhost:9411').fill('http://localhost:9411'); + await expect(configPage.saveAndTest()).toBeOK(); + await expect( + configPage, + formatExpectError('Expected data source config to display success alert after save') + ).toHaveAlert('success', { hasNotText: 'Datasource updated' }); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts new file mode 100644 index 00000000000..c97f2481fca --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('query data response should be OK when query is valid', async ({ explorePage }) => { + await explorePage.datasource.set('gdev-testdata'); + await expect( + explorePage.runQuery(), + formatExpectError('Expected Explore query to execute successfully') + ).toBeOK(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts new file mode 100644 index 00000000000..40771c4a7da --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const TRUTHY_CUSTOM_TOGGLE = 'custom_toggle1'; +const FALSY_CUSTOM_TOGGLE = 'custom_toggle2'; + +// override the feature toggles defined in playwright.config.ts only for tests in this file +test.use({ + featureToggles: { + [TRUTHY_CUSTOM_TOGGLE]: true, + [FALSY_CUSTOM_TOGGLE]: false, + }, +}); + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('should set and check feature toggles correctly', async ({ isFeatureToggleEnabled }) => { + expect(await isFeatureToggleEnabled(TRUTHY_CUSTOM_TOGGLE)).toBeTruthy(); + expect(await isFeatureToggleEnabled(FALSY_CUSTOM_TOGGLE)).toBeFalsy(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts new file mode 100644 index 00000000000..177bc908af7 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts @@ -0,0 +1,95 @@ +import * as e2e from '@grafana/e2e-selectors'; +import { expect, test } from '@grafana/plugin-e2e'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('Loki editor', () => { + test('Autocomplete features should work as expected.', async ({ page }) => { + // Go to loki datasource in explore + await page.goto( + '/explore?schemaVersion=1&panes=%7B%22iap%22:%7B%22datasource%22:%22gdev-loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22gdev-loki%22%7D,%22editorMode%22:%22builder%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1' + ); + + const queryEditor = page.getByTestId(e2e.selectors.components.QueryField.container); + const queryEditorRows = page.getByTestId('query-editor-rows'); + + async function assertQueryEditorEmpty() { + const queryEditorEmptyText = /^Enter to Rename.+/; + await expect(queryEditor).toHaveText(queryEditorEmptyText); + } + + async function clearInput() { + // Clear focused input + // Monaco appears to need some time to init keybindings after a change, adding this timeout to prevent flake + await page.waitForTimeout(100); + await page.keyboard.press('ControlOrMeta+A'); + await page.keyboard.press('Backspace'); + } + + // assert that the query builder is shown by default + await expect(page.getByText('Label filters')).toHaveCount(1); + + // switch to code editor + await page.getByLabel('Code').click(); + + await page.waitForFunction(() => window.monaco); + await expect(queryEditor).toHaveCount(1); + await assertQueryEditorEmpty(); + + // assert editor automatically adds close paren + await queryEditor.click(); + await page.keyboard.type('time('); + await expect(queryEditor).toContainText('time()'); + + // removes closing brace when opening brace is removed + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('avg_over_time('); + await expect(queryEditor).toContainText('avg_over_time()'); + await page.keyboard.press('Backspace'); + await expect(queryEditor).not.toContainText('avg_over_time()'); + await expect(queryEditor).toContainText('avg_over_time'); + + // keeps closing brace when opening brace is removed and inner values exist + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('time(test'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('Backspace'); + await expect(queryEditor).toContainText('timetest'); + + // overrides an automatically inserted paren + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('time()'); + await expect(queryEditor).toContainText('time()'); + + // does not override manually inserted braces + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('))'); + await expect(queryEditor).toContainText('))'); + + // Should execute the query when enter with shift is pressed + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.press('Shift+Enter'); + await expect(page.getByTestId('explore-no-data')).toHaveCount(1); + + // Suggestions plugin + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('av'); + await expect(queryEditorRows.getByLabel(/avg, docs:/)).toHaveCount(1); + await expect(queryEditorRows.getByLabel(/avg_over_time, docs:/)).toHaveCount(1); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts new file mode 100644 index 00000000000..0133a3e3712 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts @@ -0,0 +1,105 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulDataQuery } from '../mocks/queries'; + +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('panel edit page', () => { + test('table panel data assertions with provisioned dashboard', async ({ gotoPanelEditPage }) => { + const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + + test('table panel data assertions', async ({ panelEditPage }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization('Table'); + await panelEditPage.refreshPanel(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['col1', 'col2']); + await expect( + panelEditPage.panel.data, + formatExpectError('Could not locate headers in table panel') + ).toContainText(['val1', 'val2', 'val3', 'val4']); + }); + + test('timeseries panel - table view assertions', async ({ panelEditPage }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization('Time series'); + await panelEditPage.refreshPanel(); + await panelEditPage.toggleTableView(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['col1', 'col2']); + await expect( + panelEditPage.panel.data, + formatExpectError('Could not locate data elements in table panel') + ).toContainText(['val1', 'val2', 'val3', 'val4']); + }); + }); + + test.describe('dashboard page', () => { + test('getting panel by title', async ({ gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.goto(); + const panel = await dashboardPage.getPanelByTitle('Colored background'); + await expect(panel.fieldNames).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + + test('getting panel by id', async ({ gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.goto(); + const panel = await dashboardPage.getPanelByTitle('Colored background'); + await expect( + panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + }); + + test.describe('explore page', () => { + test('table panel', async ({ explorePage }) => { + const url = + 'left=%7B"datasource":"grafana","queries":%5B%7B"queryType":"randomWalk","refId":"A","datasource":%7B"type":"datasource","uid":"grafana"%7D%7D%5D,"range":%7B"from":"1547161200000","to":"1576364400000"%7D%7D&orgId=1'; + await explorePage.goto({ + queryParams: new URLSearchParams(url), + }); + await expect( + explorePage.timeSeriesPanel.locator, + formatExpectError('Could not locate time series panel in explore page') + ).toBeVisible(); + await expect( + explorePage.tablePanel.locator, + formatExpectError('Could not locate table panel in explore page') + ).toBeVisible(); + await expect(explorePage.tablePanel.fieldNames).toContainText(['time', 'A-series']); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts new file mode 100644 index 00000000000..4772a9717e8 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -0,0 +1,214 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulDataQuery } from '../mocks/queries'; +import { scenarios } from '../mocks/resources'; + +const PANEL_TITLE = 'Table panel E2E test'; +const TABLE_VIZ_NAME = 'Table'; +const TIME_SERIES_VIZ_NAME = 'Time series'; +const STANDARD_OTIONS_CATEGORY = 'Standard options'; +const DISPLAY_NAME_LABEL = 'Display name'; +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('query editor query data', () => { + test('query data response should be OK when query is valid', async ({ panelEditPage }) => { + await panelEditPage.datasource.set('gdev-testdata'); + await expect( + panelEditPage.refreshPanel(), + formatExpectError('Expected panel query to execute successfully') + ).toBeOK(); + }); + + test('query data response should not be OK and panel error should be displayed when query is invalid', async ({ + panelEditPage, + }) => { + await panelEditPage.datasource.set('gdev-testdata'); + const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Labels').fill('invalid-label-format'); + await expect(panelEditPage.refreshPanel(), formatExpectError('Expected panel query to fail')).not.toBeOK(); + await expect( + panelEditPage.panel.getErrorIcon(), + formatExpectError('Expected panel error to be displayed after query execution') + ).toBeVisible(); + }); + }); + + test.describe('query editor with mocked responses', () => { + test('and resource `scenarios` is mocked', async ({ selectors, dashboardPage }) => { + await dashboardPage.mockResourceResponse('scenarios', scenarios); + const panelEditPage = await dashboardPage.addPanel(); + await panelEditPage.datasource.set('gdev-testdata'); + const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Scenario').last().click(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Select.option), + formatExpectError('Expected certain select options to be displayed after clicking on the select input') + ).toHaveText(scenarios.map((s) => s.name)); + }); + + test('mocked query data response', async ({ panelEditPage, selectors }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await panelEditPage.refreshPanel(); + await expect( + panelEditPage.panel.getErrorIcon(), + formatExpectError('Did not expect panel error to be displayed after query execution') + ).toBeHidden(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body), + formatExpectError('Expected certain select options to be displayed after clicking on the select input') + ).toHaveText('val1val2val3val4'); + }); + }); + + test.describe('edit panel plugin settings', () => { + test('change viz to table panel, set panel title and collapse section', async ({ + panelEditPage, + selectors, + page, + }) => { + await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker), + formatExpectError('Expected panel visualization to be set to table') + ).toHaveText(TABLE_VIZ_NAME); + await panelEditPage.setPanelTitle(PANEL_TITLE); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_TITLE)), + formatExpectError('Expected panel title to be updated') + ).toBeVisible(); + await panelEditPage.collapseSection(STANDARD_OTIONS_CATEGORY); + await expect( + page.getByText(DISPLAY_NAME_LABEL), + formatExpectError('Expected section to be collapsed') + ).toBeVisible(); + }); + + test('Select time zone in timezone picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = await panelEditPage.getCustomOptions('Axis'); + const timeZonePicker = axisOptions.getSelect('Time zone'); + + await timeZonePicker.selectOption('Europe/Stockholm'); + await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); + }); + + test('select unit in unit picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const standardOptions = panelEditPage.getStandardOptions(); + const unitPicker = standardOptions.getUnitPicker('Unit'); + + await unitPicker.selectOption('Misc > Pixels'); + + await expect(unitPicker).toHaveSelected('Pixels'); + }); + + test('enter value in number input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const lineWith = axisOptions.getNumberInput('Soft min'); + + await lineWith.fill('10'); + + await expect(lineWith).toHaveValue('10'); + }); + + test('enter value in slider', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const graphOptions = panelEditPage.getCustomOptions('Graph styles'); + const lineWidth = graphOptions.getSliderInput('Line width'); + + await lineWidth.fill('10'); + + await expect(lineWidth).toHaveValue('10'); + }); + + test('select value in single value select', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const standardOptions = panelEditPage.getStandardOptions(); + const colorSchemeSelect = standardOptions.getSelect('Color scheme'); + + await colorSchemeSelect.selectOption('Classic palette'); + await expect(colorSchemeSelect).toHaveSelected('Classic palette'); + }); + + test('clear input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const panelOptions = panelEditPage.getPanelOptions(); + const title = panelOptions.getTextInput('Title'); + + await expect(title).toHaveValue('New panel'); + await title.clear(); + await expect(title).toHaveValue(''); + }); + + test('enter value in input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const panelOptions = panelEditPage.getPanelOptions(); + const description = panelOptions.getTextInput('Description'); + + await expect(description).toHaveValue(''); + await description.fill('This is a panel'); + await expect(description).toHaveValue('This is a panel'); + }); + + test('unchecking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const showBorder = axisOptions.getSwitch('Show border'); + + await expect(showBorder).toBeChecked({ checked: false }); + await showBorder.check(); + await expect(showBorder).toBeChecked(); + + await showBorder.uncheck(); + await expect(showBorder).toBeChecked({ checked: false }); + }); + + test('checking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const showBorder = axisOptions.getSwitch('Show border'); + + await expect(showBorder).toBeChecked({ checked: false }); + await showBorder.check(); + await expect(showBorder).toBeChecked(); + }); + + test('re-selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const placement = axisOptions.getRadioGroup('Placement'); + + await placement.check('Right'); + await expect(placement).toHaveChecked('Right'); + + await placement.check('Auto'); + await expect(placement).toHaveChecked('Auto'); + }); + + test('selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const placement = axisOptions.getRadioGroup('Placement'); + + await placement.check('Right'); + await expect(placement).toHaveChecked('Right'); + }); + }); + + test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => { + const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); + await panelEditPage.backToDashboard(); + await expect(page.url()).not.toContain('editPanel'); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts new file mode 100644 index 00000000000..c6025ca85fc --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { prometheusLabels } from '../mocks/resources'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('variable query with mocked response', async ({ variableEditPage, page }) => { + variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); + variableEditPage.mockResourceResponse('suggestions*', prometheusLabels); + await variableEditPage.datasource.set('gdev-prometheus'); + await variableEditPage.getByGrafanaSelector('Query type').fill('Label names'); + await page.keyboard.press('Tab'); + await variableEditPage.runQuery(); + await expect( + variableEditPage, + formatExpectError('Expected variable edit page to display certain label names after query execution') + ).toDisplayPreviews(prometheusLabels.data); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts new file mode 100644 index 00000000000..3deaf6341f7 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +test.describe( + 'plugin-e2e-api-test viewer', + { + tag: ['@plugins'], + }, + () => { + test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { + await page.goto('/'); + const homePageURL = new URL(page.url()); + await page.goto('/datasources', { waitUntil: 'networkidle' }); + const redirectedPageURL = new URL(page.url()); + expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname); + }); + + test('current user should have viewer role', async ({ page, request }) => { + await page.goto('/'); + const response = await request.get('/api/user/orgs'); + await expect(response).toBeOK(); + await expect(await response.json()).toContainEqual(expect.objectContaining({ role: 'Viewer' })); + }); + } +); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/errors.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/errors.ts diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts diff --git a/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts b/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts new file mode 100644 index 00000000000..f898c39fd97 --- /dev/null +++ b/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'grafana-postgresql-datasource' }); + + await expect(await page.getByText('Type: PostgreSQL', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts b/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts new file mode 100644 index 00000000000..c2ec230b481 --- /dev/null +++ b/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'zipkin' }); + + await expect(await page.getByText('Type: Zipkin', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/scenarios/login.spec.ts b/e2e-playwright/scenarios/login.spec.ts new file mode 100644 index 00000000000..afef3d867d1 --- /dev/null +++ b/e2e-playwright/scenarios/login.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Scenario test: Can login successfully', + { + tag: ['@scenarios'], + }, + async ({ selectors, page }) => { + await page.goto(selectors.pages.Login.url); + + await page.getByTestId(selectors.pages.Login.username).fill('admin'); + await page.getByTestId(selectors.pages.Login.password).fill('admin'); + await page.getByTestId(selectors.pages.Login.submit).click(); + + await page.getByTestId(selectors.pages.Login.skip).click(); + + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + } +); diff --git a/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts b/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts new file mode 100644 index 00000000000..a6bea288915 --- /dev/null +++ b/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts @@ -0,0 +1,54 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Smoke tests', + { + tag: ['@smoke'], + }, + () => { + test('Login, create test data source, create dashboard and panel scenario', async ({ + createDataSourceConfigPage, + gotoDashboardPage, + selectors, + page, + }) => { + const dataSourceConfigPage = await createDataSourceConfigPage({ + name: `e2e-${uuidv4()}`, + type: 'grafana-testdata-datasource', + }); + const { datasource } = dataSourceConfigPage; + await dataSourceConfigPage.saveAndTest({ + path: `/api/datasources/uid/${datasource.uid}?accesscontrol=true`, + }); + + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Select CSV Metric Values scenario + const scenarioSelect = dashboardPage.getByGrafanaSelector( + selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer + ); + await expect(scenarioSelect).toBeVisible(); + await scenarioSelect.locator('input[id*="test-data-scenario-select-"]').click(); + await page.getByText('CSV Metric Values').click(); + + // Verify the graph renders by checking legend + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + + // Verify panel is added to dashboard + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts b/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts new file mode 100644 index 00000000000..61e17bec7c6 --- /dev/null +++ b/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@grafana/plugin-e2e'; +import { GrafanaBootConfig } from '@grafana/runtime'; + +test.describe( + 'Panels smokescreen', + { + tag: ['@smoke'], + }, + () => { + test('Tests each panel type in the panel edit view to ensure no crash', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + // this test can absolutely take longer than the default 30s timeout + test.setTimeout(60000); + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Get panel types from window object + const panelTypes = await page.evaluate(() => { + // @grafana/plugin-e2e doesn't export the full bootdata config + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const win = window as typeof window & { grafanaBootData: GrafanaBootConfig['bootData'] }; + return win.grafanaBootData?.settings?.panels ?? {}; + }); + + // Loop through every panel type and ensure no crash + for (const [_, panel] of Object.entries(panelTypes)) { + // Skip hidden and deprecated panels + if (!panel.hideFromList && panel.state !== 'deprecated') { + // Open visualization picker + const vizPicker = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker); + await vizPicker.click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(panel.name)).click(); + + // Verify panel type is selected + await expect(vizPicker).toHaveText(panel.name); + + // Ensure no unexpected error occurred + await expect(page.getByText('An unexpected error happened')).toBeHidden(); + } + } + }); + } +); diff --git a/e2e/plugin-e2e/start-and-run-suite b/e2e-playwright/start-and-run-suite similarity index 100% rename from e2e/plugin-e2e/start-and-run-suite rename to e2e-playwright/start-and-run-suite diff --git a/e2e-playwright/storybook/verify.spec.ts b/e2e-playwright/storybook/verify.spec.ts new file mode 100644 index 00000000000..1e24b83532d --- /dev/null +++ b/e2e-playwright/storybook/verify.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@grafana/plugin-e2e'; +// very basic test to verify that the button story loads correctly +// this is only intended to catch some basic build errors with storybook +test.describe( + 'Verify storybook', + { + tag: ['@storybook'], + }, + () => { + test('Loads the button story correctly', async ({ page }) => { + await page.goto('?path=/story/buttons-button--basic'); + const iframe = page.locator('#storybook-preview-iframe'); + await expect(iframe).toBeVisible(); + const iframeBody = iframe.contentFrame(); + await expect(iframeBody.getByText('Example button')).toBeVisible(); + }); + } +); diff --git a/e2e/test-plugins/README.md b/e2e-playwright/test-plugins/README.md similarity index 100% rename from e2e/test-plugins/README.md rename to e2e-playwright/test-plugins/README.md diff --git a/e2e/test-plugins/frontend-sandbox-app-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-app-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-app-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-app-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-app-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-app-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/plugin.json diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/plugin.json diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/.gitignore b/e2e-playwright/test-plugins/grafana-extensionstest-app/.gitignore similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/.gitignore rename to e2e-playwright/test-plugins/grafana-extensionstest-app/.gitignore diff --git a/e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md b/e2e-playwright/test-plugins/grafana-extensionstest-app/CHANGELOG.md similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md rename to e2e-playwright/test-plugins/grafana-extensionstest-app/CHANGELOG.md diff --git a/e2e/test-plugins/grafana-extensionstest-app/README.md b/e2e-playwright/test-plugins/grafana-extensionstest-app/README.md similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/README.md rename to e2e-playwright/test-plugins/grafana-extensionstest-app/README.md diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/constants.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/constants.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/package.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/package.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/testIds.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/testIds.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/testIds.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/testIds.ts diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts new file mode 100644 index 00000000000..9f3d3f0bf35 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@grafana/plugin-e2e'; +import { testIds } from '../testIds'; +import pluginJson from '../plugin.json'; + +test.describe( + 'grafana-extensionstest-app', + { + tag: ['@plugins'], + }, + () => { + test('should display component exposed by another app', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/exposed-components`); + await expect(page.getByTestId(testIds.appB.exposedComponent)).toHaveText('Hello World!'); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts new file mode 100644 index 00000000000..cb2039f6480 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import pluginJson from '../plugin.json'; +import { testIds } from '../testIds'; + +test.describe( + 'grafana-extensionstest-app', + { + tag: ['@plugins'], + }, + () => { + test('should render component with usePluginComponents hook', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-components`); + await expect( + page.getByTestId(testIds.addedComponentsPage.container).getByTestId(testIds.appB.reusableAddedComponent) + ).toHaveText('Hello World!'); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts new file mode 100644 index 00000000000..852fe00fa85 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import pluginJson from '../plugin.json'; +import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json'; +import { testIds } from '../testIds'; + +test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => { + test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); + + test('should extend main app with link extension from app B', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Open from B').click(); + await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); + }); + + test('should extend main app with basic link extension from app A', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Basic link').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); + + test('should not display any extensions when extension point is not declared in plugin json when in development mode', async ({ + page, + }) => { + await page.goto(`/a/${testApp3pluginJson.id}`); + const container = await page.getByTestId(testIds.appC.section1); + await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible(); + }); + + const panelTitle = 'Link with defaults'; + const extensionTitle = 'Open from time series...'; + + const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; + const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; + + test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({ + gotoDashboardPage, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible(); + }); + + test('should add link extension (onclick) with defaults to time series panel', async ({ + gotoDashboardPage, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); + }); + + test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { + const panelTitle = 'Link with new name'; + const extensionTitle = 'Open from piechart'; + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); + }); +}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/utils.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/tests/utils.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/tsconfig.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/tsconfig.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/tsconfig.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/tsconfig.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts diff --git a/e2e/test-plugins/grafana-test-datasource/CHANGELOG.md b/e2e-playwright/test-plugins/grafana-test-datasource/CHANGELOG.md similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/CHANGELOG.md rename to e2e-playwright/test-plugins/grafana-test-datasource/CHANGELOG.md diff --git a/e2e/test-plugins/grafana-test-datasource/README.md b/e2e-playwright/test-plugins/grafana-test-datasource/README.md similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/README.md rename to e2e-playwright/test-plugins/grafana-test-datasource/README.md diff --git a/e2e/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx b/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx rename to e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx diff --git a/e2e/test-plugins/grafana-test-datasource/components/QueryEditor.tsx b/e2e-playwright/test-plugins/grafana-test-datasource/components/QueryEditor.tsx similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/components/QueryEditor.tsx rename to e2e-playwright/test-plugins/grafana-test-datasource/components/QueryEditor.tsx diff --git a/e2e/test-plugins/grafana-test-datasource/datasource.ts b/e2e-playwright/test-plugins/grafana-test-datasource/datasource.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/datasource.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/datasource.ts diff --git a/e2e/test-plugins/grafana-test-datasource/img/logo.svg b/e2e-playwright/test-plugins/grafana-test-datasource/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/img/logo.svg rename to e2e-playwright/test-plugins/grafana-test-datasource/img/logo.svg diff --git a/e2e/test-plugins/grafana-test-datasource/module.ts b/e2e-playwright/test-plugins/grafana-test-datasource/module.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/module.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/module.ts diff --git a/e2e/test-plugins/grafana-test-datasource/package.json b/e2e-playwright/test-plugins/grafana-test-datasource/package.json similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/package.json rename to e2e-playwright/test-plugins/grafana-test-datasource/package.json diff --git a/e2e/test-plugins/grafana-test-datasource/plugin.json b/e2e-playwright/test-plugins/grafana-test-datasource/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/plugin.json rename to e2e-playwright/test-plugins/grafana-test-datasource/plugin.json diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts b/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts new file mode 100644 index 00000000000..ed38635cd86 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts @@ -0,0 +1,47 @@ +import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; + +// The following tests verify that label and input field association is working correctly. +// If these tests break, e2e tests in external plugins will break too. + +test.describe( + 'grafana-test-datasource', + { + tag: ['@plugins'], + }, + () => { + test.describe('config editor ', () => { + let configPage: DataSourceConfigPage; + test.beforeEach(async ({ createDataSourceConfigPage }) => { + configPage = await createDataSourceConfigPage({ type: 'grafana-e2etest-datasource' }); + }); + + test('text input field', async ({ page }) => { + const field = page.getByRole('textbox', { name: 'API key' }); + await expect(field).toBeEmpty(); + await field.fill('test text'); + await expect(field).toHaveValue('test text'); + }); + + test('switch field', async ({ page }) => { + const field = page.getByLabel('Switch Enabled'); + await expect(field).not.toBeChecked(); + await field.check(); + await expect(field).toBeChecked(); + }); + + test('checkbox field', async ({ page }) => { + const field = page.getByRole('checkbox', { name: 'Checkbox Enabled' }); + await expect(field).not.toBeChecked(); + await field.check({ force: true }); + await expect(field).toBeChecked(); + }); + + test('select field', async ({ page, selectors }) => { + const field = page.getByRole('combobox', { name: 'Auth type' }); + await field.click(); + const option = selectors.components.Select.option; + await expect(configPage.getByGrafanaSelector(option)).toHaveText(['keys', 'credentials']); + }); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts b/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts new file mode 100644 index 00000000000..5c1e636da7e --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'grafana-test-datasource', + { + tag: ['@plugins'], + }, + () => { + test('should render variable editor', async ({ variableEditPage, page }) => { + await variableEditPage.datasource.set('gdev-e2etestdatasource'); + await expect(page.getByRole('textbox', { name: 'Query Text' })).toBeVisible(); + }); + + test('create new, successful variable query', async ({ variableEditPage, page }) => { + await variableEditPage.datasource.set('gdev-e2etestdatasource'); + await page.getByRole('textbox', { name: 'Query Text' }).fill('variableQuery'); + await variableEditPage.runQuery(); + await expect(variableEditPage).toDisplayPreviews(['A', 'B']); + }); + } +); diff --git a/e2e/test-plugins/grafana-test-datasource/tsconfig.json b/e2e-playwright/test-plugins/grafana-test-datasource/tsconfig.json similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/tsconfig.json rename to e2e-playwright/test-plugins/grafana-test-datasource/tsconfig.json diff --git a/e2e/test-plugins/grafana-test-datasource/types.ts b/e2e-playwright/test-plugins/grafana-test-datasource/types.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/types.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/types.ts diff --git a/e2e/test-plugins/grafana-test-datasource/variables.ts b/e2e-playwright/test-plugins/grafana-test-datasource/variables.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/variables.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/variables.ts diff --git a/e2e/test-plugins/grafana-test-datasource/webpack.config.ts b/e2e-playwright/test-plugins/grafana-test-datasource/webpack.config.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/webpack.config.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/webpack.config.ts diff --git a/e2e-playwright/utils/dashboard-helpers.ts b/e2e-playwright/utils/dashboard-helpers.ts new file mode 100644 index 00000000000..ff73bfe919f --- /dev/null +++ b/e2e-playwright/utils/dashboard-helpers.ts @@ -0,0 +1,35 @@ +import { Page, expect } from '@playwright/test'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * Add a new dashboard + */ +export async function addDashboard(page: Page, title?: string): Promise { + const dashboardTitle = title || `e2e-${uuidv4()}`; + + // Navigate to add dashboard page + await page.goto('/dashboard/new'); + + // Save the dashboard + const saveButton = page.getByTestId('data-testid Save dashboard button'); + await saveButton.click(); + + // Enter dashboard title + const titleInput = page.getByLabel('Save dashboard title field'); + await titleInput.clear(); + await titleInput.fill(dashboardTitle); + + // Click save + const saveAsButton = page.getByTestId('data-testid Save dashboard drawer button'); + await saveAsButton.click(); + + // Wait for success notification + await expect(page.getByText('Dashboard saved')).toBeVisible(); + + // Get the dashboard UID from the URL + const url = page.url(); + const uidMatch = url.match(/\/d\/([^\/\?]+)/); + const uid = uidMatch ? uidMatch[1] : ''; + + return uid; +} diff --git a/e2e-playwright/utils/prometheus-helpers.ts b/e2e-playwright/utils/prometheus-helpers.ts new file mode 100644 index 00000000000..229fee2bf33 --- /dev/null +++ b/e2e-playwright/utils/prometheus-helpers.ts @@ -0,0 +1,52 @@ +import { Page } from '@playwright/test'; + +export async function getResources(page: Page): Promise { + // Mock the Prometheus API responses + await page.route(/__name__/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: ['metric1', 'metric2'], + }), + }); + }); + + await page.route(/metadata/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: { + metric1: [ + { + type: 'counter', + help: 'metric1 help', + unit: '', + }, + ], + metric2: [ + { + type: 'counter', + help: 'metric2 help', + unit: '', + }, + ], + }, + }), + }); + }); + + await page.route(/labels/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: ['__name__', 'action', 'active', 'backend'], + }), + }); + }); +} diff --git a/e2e-playwright/various-suite/bar-gauge.spec.ts b/e2e-playwright/various-suite/bar-gauge.spec.ts new file mode 100644 index 00000000000..ecd07b7eb9b --- /dev/null +++ b/e2e-playwright/various-suite/bar-gauge.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Various Suite test: Bar Gauge Panel', + { + tag: ['@various'], + }, + async ({ gotoDashboardPage, selectors, page }) => { + await gotoDashboardPage({ uid: 'O6f11TZWk' }); + + const barGaugeElement = page + .locator('[data-viz-panel-key="panel-6"]') + .getByTestId(selectors.components.Panels.Visualization.BarGauge.valueV2) + .first(); + + await expect(barGaugeElement).toBeVisible(); + await expect(barGaugeElement).toHaveCSS('color', 'rgb(242, 73, 92)'); + await expect(barGaugeElement).toContainText('100'); + } +); diff --git a/e2e-playwright/various-suite/bookmarks.spec.ts b/e2e-playwright/various-suite/bookmarks.spec.ts new file mode 100644 index 00000000000..9c67884ea7e --- /dev/null +++ b/e2e-playwright/various-suite/bookmarks.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Pin nav items', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + // Set localStorage to dock the navigation + await page.evaluate(() => { + localStorage.setItem('grafana.navigation.docked', 'true'); + }); + }); + + test('should pin the selected menu item and add it as a Bookmarks menu item child', async ({ page, selectors }) => { + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + + const navList = navMenu.locator('ul[aria-label="Navigation"]'); + await expect(navList).toBeVisible(); + + // Check if the Bookmark section is visible + const bookmarksItem = navList.locator('li').nth(1); + await expect(bookmarksItem).toBeVisible(); + await expect(bookmarksItem).toContainText('Bookmarks'); + + // Check if the Administration section is visible + const adminItem = navList.locator('li').last(); + await expect(adminItem).toBeVisible(); + await expect(adminItem).toContainText('Administration'); + + // Click the "Add to Bookmarks" button in the Administration section + const addToBookmarksButton = adminItem.getByLabel('Add to Bookmarks'); + await addToBookmarksButton.click({ force: true }); + + // Check if the Administration menu item is visible in the Bookmarks section + const expandBookmarksButton = bookmarksItem.locator('button[aria-label="Expand section: Bookmarks"]'); + await expect(expandBookmarksButton).toBeVisible(); + await expandBookmarksButton.click({ force: true }); + + const administrationLink = bookmarksItem.locator('a').filter({ hasText: 'Administration' }); + await expect(administrationLink).toBeVisible(); + }); + + test('should unpin the item and remove it from the Bookmarks section', async ({ page, selectors }) => { + // Set Administration as a pinned item + await page.evaluate(() => { + localStorage.setItem( + 'grafana.user.preferences', + JSON.stringify({ + navbar: { bookmarkUrls: ['/admin'] }, + }) + ); + }); + + // Visit the home page + await page.goto('/'); + + // Reload the page to apply the docked navigation + await page.reload(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + + const navList = navMenu.locator('ul[aria-label="Navigation"]'); + await expect(navList).toBeVisible(); + + // Check if the Bookmark section is visible + const bookmarksItem = navList.locator('li').nth(1); + await expect(bookmarksItem).toBeVisible(); + await expect(bookmarksItem).toContainText('Bookmarks'); + + // Expand the Bookmarks section + const expandBookmarksButton = bookmarksItem.getByLabel('Expand section: Bookmarks'); + await expandBookmarksButton.click({ force: true }); + + // Check that Administration is visible in bookmarks + const administrationLink = bookmarksItem.locator('a').filter({ hasText: 'Administration' }); + await expect(administrationLink).toBeVisible(); + + // Click the "Remove from Bookmarks" button + const removeFromBookmarksButton = bookmarksItem.getByLabel('Remove from Bookmarks'); + await removeFromBookmarksButton.click({ force: true }); + + // Check that Administration is no longer in bookmarks + await expect(bookmarksItem.locator('a')).toHaveCount(1); + await expect(bookmarksItem.locator('a').filter({ hasText: 'Administration' })).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/exemplars.spec.ts b/e2e-playwright/various-suite/exemplars.spec.ts new file mode 100644 index 00000000000..1e2f77aa04d --- /dev/null +++ b/e2e-playwright/various-suite/exemplars.spec.ts @@ -0,0 +1,150 @@ +import { Page } from '@playwright/test'; + +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Exemplars', + { + tag: ['@various'], + }, + () => { + const dataSourceName = 'PromExemplar'; + + async function addDataSource(page, selectors, createDataSourceConfigPage) { + // Navigate to add data source page + await createDataSourceConfigPage({ type: 'prometheus', name: dataSourceName }); + + // Add exemplars configuration + const exemplarsAddButton = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.exemplarsAddButton + ); + await exemplarsAddButton.click(); + + const internalLinkSwitch = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.internalLinkSwitch + ); + await internalLinkSwitch.check({ force: true }); + + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.click(); + await page.keyboard.type('http://prom-url:9090'); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await dataSourcePicker.click(); + + const tempoOption = page.getByText('gdev-tempo'); + await tempoOption.scrollIntoViewIfNeeded(); + await expect(tempoOption).toBeVisible(); + await tempoOption.click(); + + // Save the data source + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + } + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + await addDataSource(page, selectors, createDataSourceConfigPage); + await page.goto('/'); + }); + + test('should be able to navigate to configured data source', async ({ page, selectors }) => { + // Mock API responses + await page.route(/api\/ds\/query/, async (route) => { + const postData = route.request().postDataJSON(); + const datasourceType = postData.queries[0].datasource.type; + + if (datasourceType === 'prometheus') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(require('../fixtures/exemplars-query-response.json')), + }); + } else if (datasourceType === 'tempo') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(require('../fixtures/tempo-response.json')), + }); + } else { + await route.fulfill({ status: 200, body: '{}' }); + } + }); + + // Navigate to explore + await page.goto('/explore'); + + // Select data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(dataSourceName); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + + // Switch to code editor + const codeRadioButton = page.getByTestId(selectors.components.RadioButton.container).filter({ hasText: 'Code' }); + await codeRadioButton.click(); + + // Wait for lazy loading Monaco + await waitForMonacoToLoad(page); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.clear(); + await fromField.fill('2021-07-10 17:10:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.clear(); + await toField.fill('2021-07-10 17:30:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + // Type query + const queryField = page.getByTestId(selectors.components.QueryField.container); + await expect(queryField).toBeVisible(); + await queryField.click(); + await page.keyboard.type('exemplar-query_bucket'); + await page.keyboard.press('Shift+Enter'); + + await page.waitForTimeout(1000); + + // Click zoom to data + const zoomToDataButton = page.getByTestId('time-series-zoom-to-data'); + await zoomToDataButton.click(); + + // Hover over exemplar marker and click + const exemplarMarker = page.getByTestId(selectors.components.DataSource.Prometheus.exemplarMarker).first(); + await exemplarMarker.hover(); + + const queryWithTempoLink = page.getByText('Query with gdev-tempo'); + await queryWithTempoLink.click(); + + // Verify trace viewer has span bars + const spanBars = page.getByTestId(selectors.components.TraceViewer.spanBar); + await expect(spanBars).toHaveCount(11); + }); + } +); + +export async function waitForMonacoToLoad(page: Page) { + // Wait for spinner to disappear + const spinner = page.getByTestId('Spinner'); + await expect(spinner).toBeHidden(); + + // Wait for Monaco to be available in window + await page.waitForFunction(() => { + return window.monaco !== undefined; + }); + + // Wait for Monaco editor textarea to exist + const monacoTextarea = page.locator('.monaco-editor').first(); + await expect(monacoTextarea).toBeVisible(); +} diff --git a/e2e-playwright/various-suite/explore.spec.ts b/e2e-playwright/various-suite/explore.spec.ts new file mode 100644 index 00000000000..afbc9d74c32 --- /dev/null +++ b/e2e-playwright/various-suite/explore.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Explore', + { + tag: ['@various'], + }, + () => { + test('Basic path through Explore.', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const exploreContainer = dashboardPage.getByGrafanaSelector(selectors.pages.Explore.General.container); + await expect(exploreContainer).toHaveCount(1); + + const refreshButton = dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2); + await expect(refreshButton).toHaveCount(1); + + const scenarioSelectContainer = dashboardPage.getByGrafanaSelector( + selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer + ); + await expect(scenarioSelectContainer).toBeVisible(); + await scenarioSelectContainer.locator('input[id*="test-data-scenario-select-"]').click(); + + const csvMetricValues = page.getByText('CSV Metric Values'); + await expect(csvMetricValues).toBeVisible(); + await csvMetricValues.click(); + }); + } +); diff --git a/e2e-playwright/various-suite/filter-annotations.spec.ts b/e2e-playwright/various-suite/filter-annotations.spec.ts new file mode 100644 index 00000000000..7e51d607864 --- /dev/null +++ b/e2e-playwright/various-suite/filter-annotations.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Annotations filtering', + { + tag: ['@various'], + }, + () => { + const DASHBOARD_ID = 'ed155665'; + + test('Tests switching filter type updates the UI accordingly', async ({ page, selectors, gotoDashboardPage }) => { + // Navigate to dashboard + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_ID }); + + // Enter edit mode + const editButton = dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + // Open settings + const settingsButton = dashboardPage.getByGrafanaSelector( + selectors.components.NavToolbar.editDashboard.settingsButton + ); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + // Navigate to annotations tab + const annotationsTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Annotations')); + await annotationsTab.click(); + + // Click "New query" + const newQueryButton = page.getByText('New query'); + await newQueryButton.click(); + + // Set annotation name + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Annotations.Settings.name + ); + await nameInput.fill('Red - Panel two'); + + // Test filter type switching + const showInLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel + ); + await expect(showInLabel).toBeVisible(); + + // Test "All panels" option + const annotationsTypeInput = dashboardPage + .getByGrafanaSelector(selectors.components.Annotations.annotationsTypeInput) + .locator('input'); + await annotationsTypeInput.fill('All panels'); + await annotationsTypeInput.press('Enter'); + + const annotationsChoosePanelInput = dashboardPage.getByGrafanaSelector( + selectors.components.Annotations.annotationsChoosePanelInput + ); + await expect(annotationsChoosePanelInput).toBeHidden(); + + // Test "All panels except" option + await annotationsTypeInput.fill('All panels except'); + await annotationsTypeInput.press('Enter'); + await expect(annotationsChoosePanelInput).toBeVisible(); + + // Test "Selected panels" option + await annotationsTypeInput.fill('Selected panels'); + await annotationsTypeInput.press('Enter'); + await expect(annotationsChoosePanelInput).toBeVisible(); + + // Select "Panel two" + await annotationsTypeInput.fill('Panel two'); + await annotationsTypeInput.press('Enter'); + + // Click outside to close dropdown + await page.click('body', { position: { x: 0, y: 0 } }); + + // Go back to dashboard + const backToDashboardButton = dashboardPage.getByGrafanaSelector( + selectors.components.NavToolbar.editDashboard.backToDashboardButton + ); + await expect(backToDashboardButton).toBeVisible(); + await backToDashboardButton.click(); + + // Test annotation controls + const dashboardControls = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls); + await expect(dashboardControls).toBeVisible(); + + // Test "Red - Panel two" annotation toggle + const redPanelTwoLabel = page.getByLabel('Red - Panel two'); + await expect(redPanelTwoLabel).toBeVisible(); + + const redPanelTwoToggleParent = page.getByLabel('Red - Panel two').locator('..'); + const redPanelTwoCheckbox = redPanelTwoToggleParent.locator('input'); + + // Check initial state + await expect(redPanelTwoCheckbox).toBeChecked(); + + // Uncheck and verify + await redPanelTwoCheckbox.uncheck({ force: true }); + await expect(redPanelTwoCheckbox).toBeChecked({ checked: false }); + + // Check again and verify + await redPanelTwoCheckbox.check({ force: true }); + await expect(redPanelTwoCheckbox).toBeChecked(); + + // Test "Red, only panel 1" annotation toggle + const redOnlyPanelOneLabel = page.getByLabel('Red, only panel 1'); + await expect(redOnlyPanelOneLabel).toBeVisible(); + + const redOnlyPanelOneToggleParent = page.getByLabel('Red, only panel 1').locator('..'); + const redOnlyPanelOneCheckbox = redOnlyPanelOneToggleParent.locator('input'); + await expect(redOnlyPanelOneCheckbox).toBeChecked(); + + // Verify annotations in panel + const panelOne = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel one')); + await expect(panelOne).toBeVisible(); + + const annotationMarkers = panelOne.getByTestId(selectors.pages.Dashboard.Annotations.marker); + await expect(annotationMarkers).toHaveCount(4); + }); + } +); diff --git a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts new file mode 100644 index 00000000000..dfa19753b15 --- /dev/null +++ b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const APP_ID = 'sandbox-app-test'; + +test.describe( + 'Datasource sandbox', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + await page.request.post(`/api/plugins/${APP_ID}/settings`, { + data: { + enabled: true, + }, + }); + }); + + test.describe('App Page', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Loads the app page without the sandbox div wrapper', async ({ page }) => { + await page.goto(`/a/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeHidden(); + + const appPage = page.getByTestId('sandbox-app-test-page-one'); + await expect(appPage).toBeVisible(); + }); + + test('Loads the app configuration without the sandbox div wrapper', async ({ page }) => { + await page.goto(`/plugins/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeHidden(); + + const configPage = page.getByTestId('sandbox-app-test-config-page'); + await expect(configPage).toBeVisible(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Loads the app page with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/a/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); + + const appPage = page.getByTestId('sandbox-app-test-page-one'); + await expect(appPage).toBeVisible(); + }); + + test('Loads the app configuration with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/plugins/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); + + const configPage = page.getByTestId('sandbox-app-test-config-page'); + await expect(configPage).toBeVisible(); + }); + }); + }); + } +); diff --git a/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts new file mode 100644 index 00000000000..df0f32aeba0 --- /dev/null +++ b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts @@ -0,0 +1,203 @@ +import { random } from 'lodash'; + +import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; + +const DATASOURCE_ID = 'sandbox-test-datasource'; +let DATASOURCE_CONNECTION_ID = ''; +const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance'; + +// Originally skipped due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts +// TODO: fix and remove skip +test.describe.skip( + 'Datasource sandbox', + { + tag: ['@various', '@wip'], + }, + () => { + let configPage: DataSourceConfigPage; + test.beforeEach(async ({ createDataSourceConfigPage }) => { + // Add the datasource + configPage = await createDataSourceConfigPage({ + type: 'sandbox', + name: DATASOURCE_TYPED_NAME, + }); + }); + + test.describe('Config Editor', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Should not render a sandbox wrapper around the datasource config editor', async ({ page }) => { + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + await page.waitForTimeout(300); // wait to prevent false positives because playwright checks too fast + + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeHidden(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Should render a sandbox wrapper around the datasource config editor', async ({ page, selectors }) => { + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); + }); + + test('Should store values in jsonData and secureJsonData correctly', async ({ page }) => { + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + + const valueToStore = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-config-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToStore); + await expect(queryInput).toHaveValue(valueToStore); + + const saveButton = page.getByTestId('data-testid Data source settings page Save and test button'); + await saveButton.click(); + + const alert = page.locator('[data-testid="data-testid Alert"]'); + await expect(alert).toBeVisible(); + await expect(alert).toContainText('Sandbox Success'); + + // validate the value was stored + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + await expect(queryInput).not.toBeDisabled(); + await expect(queryInput).toHaveValue(valueToStore); + }); + }); + }); + + test.describe('Explore Page', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Should not wrap the query editor in a sandbox wrapper', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + await page.waitForTimeout(300); // wait to prevent false positives because playwright checks too fast + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeHidden(); + }); + + test('Should accept values when typed', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const valueToType = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToType); + await expect(queryInput).toHaveValue(valueToType); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Should wrap the query editor in a sandbox wrapper', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); + }); + + test('Should accept values when typed', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const valueToType = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToType); + await expect(queryInput).toHaveValue(valueToType); + + // typing the query editor should reflect in the url + await expect(page).toHaveURL(new RegExp(valueToType)); + }); + }); + }); + + test.afterEach(async ({ page }) => { + // Clear cookies after each test + await page.context().clearCookies(); + }); + } +); diff --git a/e2e-playwright/various-suite/gauge.spec.ts b/e2e-playwright/various-suite/gauge.spec.ts new file mode 100644 index 00000000000..3e2d6023c95 --- /dev/null +++ b/e2e-playwright/various-suite/gauge.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +// this test requires a larger viewport so all gauge panels load properly +test.use({ + viewport: { width: 1280, height: 1080 }, +}); + +test.describe( + 'Gauge Panel', + { + tag: ['@various'], + }, + () => { + test('Gauge rendering e2e tests', async ({ gotoDashboardPage, selectors, page }) => { + // open Panel Tests - Gauge + const dashboardPage = await gotoDashboardPage({ uid: '_5rDmaQiz' }); + + // check that gauges are rendered + const gaugeElements = page.locator('.flot-base'); + await expect(gaugeElements).toHaveCount(16); + + // check that no panel errors exist + const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error')); + await expect(errorInfo).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/graph-auto-migrate.spec.ts b/e2e-playwright/various-suite/graph-auto-migrate.spec.ts new file mode 100644 index 00000000000..8b1234a8160 --- /dev/null +++ b/e2e-playwright/various-suite/graph-auto-migrate.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'XMjIZPmik'; +const DASHBOARD_NAME = 'Panel Tests - Graph Time Regions'; +const UPLOT_MAIN_DIV_SELECTOR = 'uplot-main-div'; +const ANNOTATION_MARKER_SELECTOR = 'data-testid annotation-marker'; + +test.describe( + 'Auto-migrate graph panel', + { + tag: ['@various'], + }, + () => { + test('Graph panel is auto-migrated', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden(); + + await gotoDashboardPage({ uid: DASHBOARD_ID }); + + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeVisible(); + }); + + test('Annotation markers exist for time regions', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_ID }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden(); + + await gotoDashboardPage({ uid: DASHBOARD_ID }); + + // Check Business Hours panel + const businessHoursPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Business Hours') + ); + await expect(businessHoursPanel).toBeVisible(); + const businessHoursMarker = businessHoursPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(businessHoursMarker).toBeVisible(); + + // Check Sunday's 20-23 panel + const sundayPanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title("Sunday's 20-23")); + await expect(sundayPanel).toBeVisible(); + const sundayMarker = sundayPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(sundayMarker).toBeVisible(); + + // Check Each day of week panel + const eachDayPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Each day of week') + ); + await expect(eachDayPanel).toBeVisible(); + const eachDayMarker = eachDayPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(eachDayMarker).toBeVisible(); + + // Scroll to bottom + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + // Check 05:00 panel + const timePanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('05:00')); + await expect(timePanel).toBeVisible(); + const timeMarker = timePanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(timeMarker).toBeVisible(); + + // Check From 22:00 to 00:30 panel + const midnightPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('From 22:00 to 00:30 (crossing midnight)') + ); + await expect(midnightPanel).toBeVisible(); + const midnightMarker = midnightPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(midnightMarker).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/inspect-drawer.spec.ts b/e2e-playwright/various-suite/inspect-drawer.spec.ts new file mode 100644 index 00000000000..60f0dd42b06 --- /dev/null +++ b/e2e-playwright/various-suite/inspect-drawer.spec.ts @@ -0,0 +1,173 @@ +import { Page } from 'playwright-core'; + +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const PANEL_UNDER_TEST = 'Value reducers 1'; + +test.describe( + 'Inspect drawer tests', + { + tag: ['@various'], + }, + () => { + test('Tests various Inspect Drawer scenarios', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'wfTJJL5Wz' }); + + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await panel.scrollIntoViewIfNeeded(); + await expect(panel).toBeVisible(); + + // Open panel menu + const panelMenu = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)); + await panelMenu.click({ force: true }); + + // Hover over Inspect menu item to show submenu + const inspectMenuItem = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.menuItems('Inspect') + ); + await inspectMenuItem.hover(); + + // Click on Data submenu item + const dataMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Data')); + await dataMenuItem.click(); + + await expectDrawerTabsAndContent(dashboardPage, selectors, page); + + await expectDrawerClose(dashboardPage, selectors); + + await expectSubMenuScenario(dashboardPage, selectors, page, 'Data'); + await expectSubMenuScenario(dashboardPage, selectors, page, 'Query'); + await expectSubMenuScenario(dashboardPage, selectors, page, 'Panel JSON', 'JSON'); + + // Test edit panel scenario + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)) + .click({ force: true }); + const editMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')); + await editMenuItem.click(); + + const queryInspectorButton = dashboardPage.getByGrafanaSelector( + selectors.components.QueryTab.queryInspectorButton + ); + await expect(queryInspectorButton).toBeVisible(); + await queryInspectorButton.click(); + + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeVisible(); + + const queryTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Query')); + await expect(queryTab).toBeVisible(); + + // Query should be the active tab + const activeTab = page.locator('a[class*="-activeTabStyle"]'); + await expect(activeTab).toHaveText('Query'); + + const queryContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content); + await expect(queryContent).toBeVisible(); + }); + } +); + +const expectDrawerTabsAndContent = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, page: Page) => { + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeVisible(); + + const dataTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Data')); + await expect(dataTab).toBeVisible(); + + // Data should be the active tab + const activeTab = page.locator(selectors.components.Tab.active('')); + await expect(activeTab).toHaveText('Data'); + + const dataContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content); + await expect(dataContent).toBeVisible(); + + const statsContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content); + await expect(statsContent).toBeHidden(); + + const jsonContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content); + await expect(jsonContent).toBeHidden(); + + const queryContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content); + await expect(queryContent).toBeHidden(); + + // Test Stats tab + const statsTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Stats')); + await expect(statsTab).toBeVisible(); + await statsTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeHidden(); + + // Test JSON tab + const jsonTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('JSON')); + await expect(jsonTab).toBeVisible(); + await jsonTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeHidden(); + + // Test Query tab + const queryTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Query')); + await expect(queryTab).toBeVisible(); + await queryTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeHidden(); +}; + +const expectDrawerClose = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + // Close using close button + const closeButton = dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close); + await closeButton.click(); + + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeHidden(); +}; + +const expectSubMenuScenario = async ( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + page: Page, + subMenu: string, + tabTitle?: string +) => { + tabTitle = tabTitle ?? subMenu; + + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await panel.scrollIntoViewIfNeeded(); + await expect(panel).toBeVisible(); + + // Open panel menu + const panelMenu = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)); + await panelMenu.click({ force: true }); + + // Hover over Inspect menu item to show submenu + const inspectMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Inspect')); + await inspectMenuItem.hover(); + + // Click on submenu item + const subMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems(subMenu)); + await subMenuItem.click(); + + // Tab should be visible and active + const tab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabTitle)); + await expect(tab).toBeVisible(); + + const activeTab = page.locator(selectors.components.Tab.active('')); + await expect(activeTab).toHaveText(tabTitle); + + await expectDrawerClose(dashboardPage, selectors); +}; diff --git a/e2e-playwright/various-suite/keybinds.spec.ts b/e2e-playwright/various-suite/keybinds.spec.ts new file mode 100644 index 00000000000..4ff37889838 --- /dev/null +++ b/e2e-playwright/various-suite/keybinds.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Keyboard shortcuts', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page, selectors }) => { + await page.goto('/'); + + // Wait for the page to load + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Latest from the blog')); + await expect(panelTitle).toBeVisible(); + }); + + test('sequence shortcuts should work', async ({ page, selectors }) => { + // Navigate to explore with 'ge' shortcut + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Navigate to profile with 'gp' shortcut + await page.keyboard.type('gp'); + const preferencesSaveButton = page.getByTestId(selectors.components.UserProfile.preferencesSaveButton); + await expect(preferencesSaveButton).toBeVisible(); + + // Navigate back to home with 'gh' shortcut + await page.keyboard.type('gh'); + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Latest from the blog')); + await expect(panelTitle).toBeVisible(); + }); + + test('ctrl+z should zoom out the time range', async ({ page, selectors }) => { + // Navigate to explore + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.fill('2024-06-05 10:05:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.fill('2024-06-05 10:06:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + await page.keyboard.press('Control+z'); + + const expectedRange = 'Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30'; + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + }); + + test('time range shortcuts should work', async ({ page, selectors }) => { + // Navigate to explore + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.fill('2024-06-05 10:05:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.fill('2024-06-05 10:06:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + let expectedRange = 'Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00'; + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + + // Use time range shortcut to move back + await page.keyboard.press('t'); + await page.keyboard.press('ArrowLeft'); + + expectedRange = 'Time range selected: 2024-06-05 10:04:00 to 2024-06-05 10:05:00'; // 1 min back + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + }); + } +); diff --git a/e2e-playwright/various-suite/loki-query-builder.spec.ts b/e2e-playwright/various-suite/loki-query-builder.spec.ts new file mode 100644 index 00000000000..47e3c35299f --- /dev/null +++ b/e2e-playwright/various-suite/loki-query-builder.spec.ts @@ -0,0 +1,112 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const MISSING_LABEL_FILTER_ERROR_MESSAGE = 'Select at least 1 label filter (label and value)'; +const dataSourceName = 'LokiBuilder'; +const finalQuery = 'rate({instance=~"instance1|instance2"} | logfmt | __error__=`` [$__auto]'; + +test.describe( + 'Loki query builder', + { + tag: ['@various'], + }, + () => { + test('should be able to use all modes', async ({ createDataSource, page, dashboardPage, selectors }) => { + await createDataSource({ type: 'loki', name: dataSourceName }); + // Mock API responses + await page.route(/labels\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance', 'job', 'source'] }), + }); + }); + + await page.route(/series?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: [{ instance: 'instance1' }] }), + }); + }); + + await page.route(/values/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance1', 'instance2'] }), + }); + }); + + await page.route(/index\/stats/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ streams: 2, chunks: 2660, bytes: 2721792, entries: 14408 }), + }); + }); + + // Go to Explore and choose Loki data source + await page.goto('/explore'); + await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click(); + await page.getByRole('button', { name: dataSourceName }).click(); + + // Start in builder mode, click and choose query pattern + await page.getByTestId(selectors.components.QueryBuilder.queryPatterns).click(); + await page.getByRole('button', { name: 'Log query starters' }).click(); + await page.getByRole('button', { name: 'Use this query' }).first().click(); + await expect(page.getByText('No pipeline errors')).toBeVisible(); + await expect(page.getByText('{} | logfmt | __error__=``')).toBeVisible(); + + // Add operation + await page.getByRole('button', { name: 'Operations', exact: true }).click(); + await page.getByText('Range functions').click(); + await page.getByText('Rate', { exact: true }).click(); + await expect(page.getByText('rate({} | logfmt | __error__=`` [$__auto]')).toBeVisible(); + + // Check for expected error + await expect(page.getByText(MISSING_LABEL_FILTER_ERROR_MESSAGE)).toBeVisible(); + + // Add labels to remove error + await dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.labelSelect).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.inputSelect).fill('instance'); + await page.keyboard.press('Enter'); + + const matchOperatorSelect = dashboardPage.getByGrafanaSelector( + selectors.components.QueryBuilder.matchOperatorSelect + ); + await expect(matchOperatorSelect).toBeVisible(); + await matchOperatorSelect.click({ force: true }); + + const matchOperatorInput = matchOperatorSelect.locator('div').locator('input'); + await matchOperatorInput.fill('=~'); + await page.keyboard.press('Enter'); + + const valueSelect = dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + await valueSelect.click(); + + const valueInput = valueSelect.locator('div').locator('input'); + await valueInput.fill('instance1'); + await page.keyboard.press('Enter'); + await valueInput.fill('instance2'); + await page.keyboard.press('Enter'); + + await expect(page.getByText(MISSING_LABEL_FILTER_ERROR_MESSAGE)).toBeHidden(); + await expect(page.getByText(finalQuery)).toBeVisible(); + + // Change to code editor + await page.getByRole('radio', { name: 'Code' }).click(); + + // We need to test this manually because the final query is split into separate DOM elements + await expect(page.getByText('rate')).toBeVisible(); + await expect(page.getByText('instance1|instance2')).toBeVisible(); + await expect(page.getByText('logfmt')).toBeVisible(); + await expect(page.getByText('__error__')).toBeVisible(); + await expect(page.getByText('$__auto')).toBeVisible(); + + // Checks the explain mode toggle + await page.getByText('Explain').click(); + await expect(page.getByText('Fetch all log lines matching label filters.')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts b/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts new file mode 100644 index 00000000000..8495d43c07c --- /dev/null +++ b/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts @@ -0,0 +1,204 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const dataSourceName = 'LokiEditor'; + +const lokiQueryResult = { + status: 'success', + results: { + A: { + status: 200, + frames: [ + { + schema: { + refId: 'A', + meta: { + typeVersion: [0, 0], + custom: { + frameType: 'LabeledTimeValues', + }, + stats: [ + { + displayName: 'Summary: bytes processed per second', + unit: 'Bps', + value: 223921, + }, + { + displayName: 'Summary: total bytes processed', + unit: 'decbytes', + value: 4156, + }, + { + displayName: 'Summary: exec time', + unit: 's', + value: 0.01856, + }, + ], + executedQueryString: 'Expr: {targetLabelName="targetLabelValue"}', + }, + fields: [ + { + name: 'labels', + type: 'other', + typeInfo: { + frame: 'json.RawMessage', + }, + }, + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + }, + { + name: 'Line', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + { + name: 'tsNs', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + { + name: 'id', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + ], + }, + data: { + values: [ + [ + { + targetLabelName: 'targetLabelValue', + instance: 'server\\1', + job: '"grafana/data"', + nonIndexed: 'value', + place: 'moon', + re: 'one.two$three^four', + source: 'data', + }, + ], + [1700077283237], + [ + '{"_entry":"log text with ANSI \\u001b[31mpart of the text\\u001b[0m [149702545]","counter":"22292","float":"NaN","wave":-0.5877852522916832,"label":"val3","level":"info"}', + ], + ['1700077283237000000'], + ['1700077283237000000_9b025d35'], + ], + }, + }, + ], + }, + }, +}; + +test.use({ + featureToggles: { + logsExploreTableVisualisation: true, + }, +}); + +test.describe( + 'Loki Query Editor', + { + tag: ['@various'], + }, + () => { + test('Should be able to add explore table to dashboard', async ({ + createDataSource, + page, + dashboardPage, + selectors, + }) => { + await createDataSource({ type: 'loki', name: dataSourceName }); + // Mock API responses + await page.route(/labels\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance', 'job', 'source'] }), + }); + }); + + await page.route(/series\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: [{ instance: 'instance1' }] }), + }); + }); + + await page.route(/\/api\/ds\/query\?ds_type=loki/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(lokiQueryResult), + }); + }); + + // Go to Explore and choose Loki data source + await page.goto('/explore'); + await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click(); + await page.getByRole('button', { name: dataSourceName }).click(); + + await page.getByRole('radio', { name: 'Code' }).click(); + + // Write a simple query + const queryField = page.getByTestId(selectors.components.QueryField.container).locator('textarea'); + await queryField.fill('query{instance="instance1"}'); + + // Submit the query with Shift+Enter + await queryField.press('Shift+Enter'); + + // Assert the no-data message is not visible + await expect(page.locator('[data-testid="explore-no-data"]')).toBeHidden(); + + // Click on the table toggle + await page.getByRole('radio', { name: 'Table' }).click({ force: true }); + + // One row with two cells initially + const cells = page.locator('[role="cell"]'); + await expect(cells).toHaveCount(2); + + // Find and click on the targetLabelName label + await page.getByText('targetLabelName', { exact: true }).click(); + + // Now we should have a row with 3 columns + await expect(cells).toHaveCount(3); + // And a value of "targetLabelValue" + await expect(cells.getByText('targetLabelValue')).toBeVisible(); + + await page.getByLabel('Add', { exact: true }).click(); + + await page.getByLabel('Add to dashboard').click(); + + const addPanelToDashboardButton = page.getByText('Add panel to dashboard'); + await expect(addPanelToDashboardButton).toBeVisible(); + + await page.getByText('Open dashboard').click(); + + // Check the panel is visible + const panel = page.locator('[data-viz-panel-key="panel-1"]'); + await expect(panel).toBeVisible(); + + // Check the table cells in the panel + const panelCells = panel.locator('[role="table"] [role="cell"]'); + // Should have 3 columns + await expect(panelCells).toHaveCount(3); + + // Cells contain strings found in log line + await expect(page.getByText('"wave":-0.5877852522916832')).toBeVisible(); + + // Column has correct value of "targetLabelValue" + await expect(panel.locator('[role="table"] [role="cell"]').filter({ hasText: 'targetLabelValue' })).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/migrate-to-cloud.spec.ts b/e2e-playwright/various-suite/migrate-to-cloud.spec.ts new file mode 100644 index 00000000000..d2712554ea1 --- /dev/null +++ b/e2e-playwright/various-suite/migrate-to-cloud.spec.ts @@ -0,0 +1,393 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Migrate to Cloud (On-prem)', + { + tag: ['@various', '@wip'], + }, + () => { + test.describe('with mocked calls to the API backend', () => { + test.afterEach(async ({ page }) => { + const disconnectButton = page.getByTestId('migrate-to-cloud-summary-disconnect-button'); + await expect(disconnectButton).toBeVisible(); + await disconnectButton.click(); + }); + + const SESSION_UID = 'fehq6hqd246iox'; + const SNAPSHOT_UID1 = 'cehq6vdjqbqbkx'; + + const SNAPSHOT_RESULTS = [ + { name: 'FolderA', type: 'FOLDER', refId: 'ref-id-folder-a', parentName: 'General' }, + { name: 'FolderB', type: 'FOLDER', refId: 'ref-id-folder-b', parentName: 'General' }, + { name: 'Prometheus', type: 'DATASOURCE', refId: 'prometheus' }, + { name: 'Postgres', type: 'DATASOURCE', refId: 'postgres' }, + { name: 'Loki', type: 'DATASOURCE', refId: 'loki' }, + { name: 'Alert Rule A', type: 'ALERT_RULE', refId: 'alert-rule-a', parentName: 'FolderA' }, + { name: 'Alert Rule B', type: 'ALERT_RULE', refId: 'alert-rule-b', parentName: 'FolderB' }, + { name: 'Alert Rule C', type: 'ALERT_RULE', refId: 'alert-rule-c', parentName: 'FolderB' }, + { name: 'Alert Rule Group A', type: 'ALERT_RULE_GROUP', refId: 'alert-rule-group-a', parentName: 'FolderA' }, + { name: 'Alert Rule Group B', type: 'ALERT_RULE_GROUP', refId: 'alert-rule-group-b', parentName: 'FolderB' }, + { name: 'Contact Point A', type: 'CONTACT_POINT', refId: 'contact-point-a' }, + { name: 'Contact Point B', type: 'CONTACT_POINT', refId: 'contact-point-b' }, + { name: 'Contact Point C', type: 'CONTACT_POINT', refId: 'contact-point-c' }, + { name: 'Notification Policy A', type: 'NOTIFICATION_POLICY', refId: 'notification-policy-a' }, + { name: 'Notification Template A', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-a' }, + { name: 'Notification Template B', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-b' }, + { name: 'Notification Template C', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-c' }, + { name: 'Plugin A', type: 'PLUGIN', refId: 'plugin-a' }, + { name: 'Plugin B', type: 'PLUGIN', refId: 'plugin-b' }, + { name: 'Plugin C', type: 'PLUGIN', refId: 'plugin-c' }, + { name: 'Mute Timing A', type: 'MUTE_TIMING', refId: 'mute-timing-a' }, + { name: 'Mute Timing B', type: 'MUTE_TIMING', refId: 'mute-timing-b' }, + ]; + + const MIGRATION_SESSION = { + uid: SESSION_UID, + slug: 'test-slug', + created: '2025-04-02T21:36:08+02:00', + updated: '2025-04-02T21:36:08+02:00', + }; + + const STATS = { + types: SNAPSHOT_RESULTS.reduce>((acc, r) => { + acc[r.type] = (acc[r.type] || 0) + 1; + return acc; + }, {}), + statuses: { + PENDING: SNAPSHOT_RESULTS.length, + }, + total: SNAPSHOT_RESULTS.length, + }; + + // TODO: fix the test. It makes it most of the way through. Probably a network mock issue. + test.skip('creates and uploads a snapshot successfully', async ({ page }) => { + // Visit the migrate to cloud onprem page + await page.goto('/admin/migrate-to-cloud'); + + // Open the connect modal and enter the token + const connectButton = page.getByTestId('migrate-to-cloud-connect-session-modal-button'); + await expect(connectButton).toBeVisible(); + await connectButton.click(); + + const tokenInput = page.getByTestId('migrate-to-cloud-connect-session-modal-token-input'); + await expect(tokenInput).toBeVisible(); + await tokenInput.focus(); + await tokenInput.fill('test'); + + // Mock API responses + await page.route(/api\/cloudmigration\/migration/, async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MIGRATION_SESSION), + }); + } else if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + sessions: [MIGRATION_SESSION], + }), + }); + } + }); + + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [], + }), + }); + }); + + // Click the connect button to create the token + const connectSessionButton = page.getByTestId('migrate-to-cloud-connect-session-modal-connect-button'); + await expect(connectSessionButton).toBeVisible(); + await connectSessionButton.click(); + + // Check the 'Include all' resources checkbox + const includeAllCheckbox = page.getByTestId( + 'migrate-to-cloud-configure-snapshot-checkbox-resource-include-all' + ); + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + // And validate that all resources are indeed checked + for (const resourceType of [ + 'alert_rule', + 'alert_rule_group', + 'contact_point', + 'dashboard', + 'datasource', + 'folder', + 'library_element', + 'mute_timing', + 'notification_policy', + 'notification_template', + 'plugin', + ]) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).toBeChecked(); + } + + // Remove one of the resources that has dependencies + // Mute Timings are dependencies of Alert Rules, which are dependencies of Alert Rule Groups + const muteTimingCheckbox = page.getByTestId( + 'migrate-to-cloud-configure-snapshot-checkbox-resource-mute_timing' + ); + await muteTimingCheckbox.uncheck({ force: true }); + await expect(muteTimingCheckbox).not.toBeChecked(); + + // Validate that those resources are now unchecked + for (const resourceType of ['alert_rule', 'alert_rule_group', 'include-all']) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).not.toBeChecked(); + } + + // Check everything again because we can + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + // Validate that those resources are now checked again + for (const resourceType of ['alert_rule', 'alert_rule_group', 'mute_timing']) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).toBeChecked(); + } + + // Mock snapshot creation + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + }), + }); + }); + + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [ + { + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'CREATING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + }, + ], + }), + }); + }); + + let getSnapshotCalled = false; + await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\?resultPage=1&resultLimit=50/, + async (route) => { + if (!getSnapshotCalled) { + getSnapshotCalled = true; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'CREATING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: [], + stats: { + types: {}, + statuses: {}, + total: 0, + }, + }), + }); + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'PENDING_UPLOAD', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'PENDING' })), + stats: STATS, + }), + }); + } + } + ); + + // Build the snapshot + const buildSnapshotButton = page.getByTestId('migrate-to-cloud-configure-snapshot-build-snapshot-button'); + await expect(buildSnapshotButton).toBeVisible(); + await buildSnapshotButton.click(); + + // Mock upload + const uploadSnapshot = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\/upload/, + async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ success: true }), + }); + } + ); + + // Upload the snapshot + const uploadButton = page.getByTestId('migrate-to-cloud-summary-upload-snapshot-button'); + await expect(uploadButton).toBeVisible(); + + await uploadButton.focus(); + await uploadButton.click({ force: true }); + + // Mock uploading status + const getSnapshotListUploading = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, + async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [ + { + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'UPLOADING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + }, + ], + }), + }); + } + ); + + // Simulate the snapshot being uploaded + let getSnapshotUploadingCalls = 0; + const getSnapshotUploading = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\?resultPage=1&resultLimit=50/, + async (route) => { + if (getSnapshotUploadingCalls <= 1) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: getSnapshotUploadingCalls === 1 ? 'PROCESSING' : 'UPLOADING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'PENDING' })), + stats: STATS, + }), + }); + getSnapshotUploadingCalls++; + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'FINISHED', + created: '2025-03-27T12:00:00Z', + finished: '2025-03-27T12:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'OK' })), + stats: { + types: STATS.types, + statuses: SNAPSHOT_RESULTS.reduce( + (acc, r) => { + const status = (r as { status?: string }).status || 'UNKNOWN'; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, + {} as Record + ), + total: SNAPSHOT_RESULTS.length, + }, + }), + }); + } + } + ); + + await uploadSnapshot; + await getSnapshotListUploading; + await getSnapshotUploading; + + // At least some of the items are marked with "Uploaded to cloud" status + await expect(page.getByText('Uploaded to cloud')).toBeVisible(); + + // We can now reconfigure the snapshot + const reconfigureButton = page.getByTestId('migrate-to-cloud-summary-reconfigure-snapshot-button'); + await expect(reconfigureButton).toBeVisible(); + await reconfigureButton.click(); + + // Check the 'Include all' resources checkbox + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + }); + }); + + test.describe('with a fake GMS backend implementation', () => { + test.afterEach(async ({ page }) => { + const disconnectButton = page.getByTestId('migrate-to-cloud-summary-disconnect-button'); + await expect(disconnectButton).toBeVisible(); + await disconnectButton.click(); + }); + + // Manually crafted base64 token for testing, does not contain any sensitive data + const TEST_TOKEN = + 'eyJUb2tlbiI6ImdsY19kZXZfZXlKdklqb2lNVEl6TkNJc0ltNGlPaUpuY21GbVlXNWhMV05zYjNWa0xXMXBaM0poZEdsdmJuTXRNVEl6TkNJc0ltc2lPaUowWlhOMElpd2liU0k2ZXlKeUlqb2laR1YyTFhWekxXTmxiblJ5WVd3aWZYMEsiLCJJbnN0YW5jZSI6eyJTdGFja0lEIjoxMjM0LCJTbHVnIjoidGVzdC1zbHVnIiwiUmVnaW9uU2x1ZyI6ImRldi11cy1jZW50cmFsIiwiQ2x1c3RlclNsdWciOiJkZXYtdXMtY2VudHJhbC0wIn19Cg=='; + + test('creates a snapshot successfully', async ({ page }) => { + // Visit the migrate to cloud onprem page + await page.goto('/admin/migrate-to-cloud'); + + // Open the connect modal and enter the token + const connectButton = page.getByTestId('migrate-to-cloud-connect-session-modal-button'); + await expect(connectButton).toBeVisible(); + await connectButton.click(); + + const tokenInput = page.getByTestId('migrate-to-cloud-connect-session-modal-token-input'); + await expect(tokenInput).toBeVisible(); + await tokenInput.focus(); + await tokenInput.fill(TEST_TOKEN); + + // Click the connect button to create the token + const connectSessionButton = page.getByTestId('migrate-to-cloud-connect-session-modal-connect-button'); + await expect(connectSessionButton).toBeVisible(); + await connectSessionButton.click(); + + // Build the snapshot + const buildSnapshotButton = page.getByTestId('migrate-to-cloud-configure-snapshot-build-snapshot-button'); + await expect(buildSnapshotButton).toBeVisible(); + await buildSnapshotButton.click(); + + // And the rebuild button should be visible + const reconfigureButton = page.getByTestId('migrate-to-cloud-summary-reconfigure-snapshot-button'); + await expect(reconfigureButton).toBeVisible(); + + // We don't upload the snapshot yet because we need to create a mock server to validate the uploaded items, + // similarly to what the SMTP (tester) server does + }); + }); + } +); diff --git a/e2e-playwright/various-suite/navigation.spec.ts b/e2e-playwright/various-suite/navigation.spec.ts new file mode 100644 index 00000000000..24defee5d6a --- /dev/null +++ b/e2e-playwright/various-suite/navigation.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Docked Navigation', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + // This is a breakpoint where the mega menu can be docked (and docked is the default state) + await page.setViewportSize({ width: 1280, height: 800 }); + }); + + test('should remain un-docked when reloading the page', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + + // Reload the page + await page.reload(); + await expect(navMenu).toBeHidden(); + }); + + test('Can re-dock after undock', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + const openMenuButton = page.getByRole('button', { name: 'Open menu' }); + await openMenuButton.click(); + + const dockButton = page.getByRole('button', { name: 'Dock menu' }); + await dockButton.click(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + }); + + test('should remain in same state when navigating to another page', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + // Navigate + const openMenuButton = page.getByRole('button', { name: 'Open menu' }); + await openMenuButton.click(); + + const administrationLink = page.getByRole('link', { name: 'Administration' }); + await administrationLink.click(); + + // Still undocked + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + + // Dock the menu + await openMenuButton.click(); + const dockButton = page.getByRole('button', { name: 'Dock menu' }); + await dockButton.click(); + + // Navigate + const usersLink = page.getByRole('link', { name: 'Users' }); + await usersLink.click(); + + // Still docked + await expect(navMenu).toBeVisible(); + }); + + test('should undock on smaller viewport sizes', async ({ page, selectors }) => { + await page.setViewportSize({ width: 1120, height: 1080 }); + await page.reload(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/pie-chart.spec.ts b/e2e-playwright/various-suite/pie-chart.spec.ts new file mode 100644 index 00000000000..6ead086de5b --- /dev/null +++ b/e2e-playwright/various-suite/pie-chart.spec.ts @@ -0,0 +1,21 @@ +import { selectors } from '@grafana/e2e-selectors'; +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Pie Chart Panel', + { + tag: ['@various'], + }, + () => { + test('Pie Chart rendering e2e tests', async ({ page }) => { + // Open Panel Tests - Pie Chart + await page.goto('/d/lVE-2YFMz/panel-tests-pie-chart'); + + // Check that there are 5 pie chart slices + const pieChartSlices = page.locator( + `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` + ); + await expect(pieChartSlices).toHaveCount(5); + }); + } +); diff --git a/e2e-playwright/various-suite/prometheus-annotations.spec.ts b/e2e-playwright/various-suite/prometheus-annotations.spec.ts new file mode 100644 index 00000000000..a14d054cab6 --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-annotations.spec.ts @@ -0,0 +1,137 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +import { addDashboard } from '../utils/dashboard-helpers'; +import { getResources } from '../utils/prometheus-helpers'; + +test.describe( + 'Prometheus annotations', + { + tag: ['@various'], + }, + () => { + const DATASOURCE_NAME = 'aprometheusAnnotationDS'; + + test('should navigate to variable query editor', async ({ page, selectors, createDataSourceConfigPage }) => { + const annotationName = 'promAnnotation'; + + await createDataSourceConfigPage({ type: 'prometheus', name: DATASOURCE_NAME }); + + // Add a new dashboard + await addDashboard(page); + + // Navigate to annotations + await navigateToAnnotations(page, selectors); + + // Add Prometheus annotation + await addPrometheusAnnotation(page, selectors, annotationName); + + // Open metrics browser + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + // Type in the metric selector + const selectMetricInput = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetricInput).toBeVisible(); + await selectMetricInput.fill('met'); + + // Select metric1 from the list + const metricList = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList + ); + await expect(metricList).toBeVisible(); + const metric1Option = metricList.getByText('metric1'); + await metric1Option.click(); + + // Use the query + const useQueryButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQueryButton).toBeVisible(); + await useQueryButton.click(); + + // Verify query field contains metric1 + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + await expect(queryField).toContainText('metric1'); + + // Check for other parts of the annotations + // Min step + const minStepInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.minStep); + await expect(minStepInput).toBeVisible(); + + // Title + const titleInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.title); + await titleInput.scrollIntoViewIfNeeded(); + await expect(titleInput).toBeVisible(); + + // Tags + const tagsInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.tags); + await tagsInput.scrollIntoViewIfNeeded(); + await expect(tagsInput).toBeVisible(); + + // Text + const textInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.text); + await textInput.scrollIntoViewIfNeeded(); + await expect(textInput).toBeVisible(); + + // Series value as timestamp + const seriesValueSwitch = page.getByTestId( + selectors.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp + ); + await seriesValueSwitch.scrollIntoViewIfNeeded(); + await expect(seriesValueSwitch).toBeVisible(); + + // Go back to dashboard + const backButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.backToDashboardButton); + await expect(backButton).toBeVisible(); + await backButton.click(); + + // Check that annotation exists + await expect(page.getByText(annotationName)).toBeVisible(); + }); + + /** + * Click dashboard settings and then the annotations tab + */ + async function navigateToAnnotations(page: Page, selectors: E2ESelectorGroups) { + const editButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + const settingsButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.settingsButton); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + const annotationsTab = page.getByTestId(selectors.components.Tab.title('Annotations')); + await annotationsTab.click(); + } + + async function addPrometheusAnnotation(page: Page, selectors: E2ESelectorGroups, annotationName: string) { + const addAnnotationButton = page.getByTestId( + selectors.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2 + ); + await addAnnotationButton.click(); + + await getResources(page); + + const nameInput = page.getByTestId(selectors.pages.Dashboard.Settings.Annotations.Settings.name); + await nameInput.clear(); + await nameInput.fill(annotationName); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(DATASOURCE_NAME); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + } + } +); diff --git a/e2e-playwright/various-suite/prometheus-config.spec.ts b/e2e-playwright/various-suite/prometheus-config.spec.ts new file mode 100644 index 00000000000..a92c9b7c2bd --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-config.spec.ts @@ -0,0 +1,193 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +// Todo: Fix datasource creation +test.describe.skip( + 'Prometheus config', + { + tag: ['@various'], + }, + () => { + const DATASOURCE_ID = 'Prometheus'; + const DATASOURCE_TYPED_NAME = 'PrometheusDatasourceInstance'; + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + // Navigate to add data source page + await page.goto('/datasources/new'); + + // Select the Prometheus data source + const prometheusPlugin = page.getByRole('button', { name: DATASOURCE_ID }); + await prometheusPlugin.scrollIntoViewIfNeeded(); + await expect(prometheusPlugin).toBeVisible(); + await prometheusPlugin.click(); + }); + + test('should have the following components: connection settings, managed alerts, scrape interval, query timeout, default editor, disable metric lookup, prometheus type, cache level, incremental querying, disable recording rules, custom query parameters, http method', async ({ + page, + selectors, + }) => { + // connection settings + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await expect(connectionSettings).toBeVisible(); + + // managed alerts + const manageAlerts = page.locator(`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`); + await manageAlerts.scrollIntoViewIfNeeded(); + await expect(manageAlerts).toBeVisible(); + + // scrape interval + const scrapeInterval = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.scrapeInterval); + await scrapeInterval.scrollIntoViewIfNeeded(); + await expect(scrapeInterval).toBeVisible(); + + // query timeout + const queryTimeout = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.queryTimeout); + await queryTimeout.scrollIntoViewIfNeeded(); + await expect(queryTimeout).toBeVisible(); + + // default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + + // disable metric lookup + const disableMetricLookup = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.disableMetricLookup}` + ); + await disableMetricLookup.scrollIntoViewIfNeeded(); + await expect(disableMetricLookup).toBeVisible(); + + // prometheus type + const prometheusType = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.prometheusType); + await prometheusType.scrollIntoViewIfNeeded(); + await expect(prometheusType).toBeVisible(); + + // cache level + const cacheLevel = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.cacheLevel); + await cacheLevel.scrollIntoViewIfNeeded(); + await expect(cacheLevel).toBeVisible(); + + // incremental querying + const incrementalQuerying = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}` + ); + await incrementalQuerying.scrollIntoViewIfNeeded(); + await expect(incrementalQuerying).toBeVisible(); + + // disable recording rules + const disableRecordingRules = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.disableRecordingRules}` + ); + await disableRecordingRules.scrollIntoViewIfNeeded(); + await expect(disableRecordingRules).toBeVisible(); + + // custom query parameters + const customQueryParameters = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.customQueryParameters + ); + await customQueryParameters.scrollIntoViewIfNeeded(); + await expect(customQueryParameters).toBeVisible(); + + // http method + const httpMethod = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.httpMethod); + await httpMethod.scrollIntoViewIfNeeded(); + await expect(httpMethod).toBeVisible(); + }); + + test('should save the default editor when navigating to explore', async ({ page, selectors }) => { + // Click on default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + await defaultEditor.click(); + + // Select 'Builder' option + await selectOption(page, 'Builder'); + + // Set connection settings + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.fill('http://prom-url:9090'); + + // Set data source name + const nameInput = page.getByTestId(selectors.pages.DataSource.name); + await nameInput.clear(); + await nameInput.fill(DATASOURCE_TYPED_NAME); + + // Save and test + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + + // Navigate to explore + await page.goto('/explore'); + + // Select the data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + // Type the data source name and press enter + await page.keyboard.type(DATASOURCE_TYPED_NAME); + await page.keyboard.press('Enter'); + + // Verify the builder metric select is visible + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + }); + + test('should allow a user to add the version when the Prom type is selected', async ({ page, selectors }) => { + // Click on prometheus type + const prometheusType = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.prometheusType); + await prometheusType.scrollIntoViewIfNeeded(); + await expect(prometheusType).toBeVisible(); + await prometheusType.click(); + + // Select 'Prometheus' option + await selectOption(page, 'Prometheus'); + + // Verify prometheus version is visible + const prometheusVersion = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.prometheusVersion + ); + await prometheusVersion.scrollIntoViewIfNeeded(); + await expect(prometheusVersion).toBeVisible(); + }); + + test('should have a cache level component', async ({ page, selectors }) => { + // Verify cache level is visible + const cacheLevel = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.cacheLevel); + await cacheLevel.scrollIntoViewIfNeeded(); + await expect(cacheLevel).toBeVisible(); + }); + + test('should allow a user to select a query overlap window when incremental querying is selected', async ({ + page, + selectors, + }) => { + // Check the incremental querying checkbox + const incrementalQuerying = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}` + ); + await incrementalQuerying.scrollIntoViewIfNeeded(); + await expect(incrementalQuerying).toBeVisible(); + await incrementalQuerying.check({ force: true }); + + // Verify query overlap window is visible + const queryOverlapWindow = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.queryOverlapWindow + ); + await queryOverlapWindow.scrollIntoViewIfNeeded(); + await expect(queryOverlapWindow).toBeVisible(); + }); + } +); + +async function selectOption(page, option) { + const optionElement = page.getByRole('option').filter({ hasText: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/prometheus-editor.spec.ts b/e2e-playwright/various-suite/prometheus-editor.spec.ts new file mode 100644 index 00000000000..2aafd3cb9aa --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-editor.spec.ts @@ -0,0 +1,287 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +import { getResources } from '../utils/prometheus-helpers'; + +// TODO: fix some tests. Race conditions with other tests in the file cause some to fail. +test.describe.skip( + 'Prometheus query editor', + { + tag: ['@various', '@wip'], + }, + () => { + const DATASOURCE_ID = 'Prometheus'; + + type EditorType = 'Code' | 'Builder'; + + /** + * Create and save a Prometheus data source, navigate to code or builder + */ + async function navigateToEditor(page: Page, selectors: E2ESelectorGroups, editorType: string, name: string) { + // Navigate to add data source page + await page.goto('/datasources/new'); + + // Select the Prometheus data source + const prometheusPlugin = page.getByRole('button', { name: DATASOURCE_ID }); + await prometheusPlugin.scrollIntoViewIfNeeded(); + await expect(prometheusPlugin).toBeVisible(); + await prometheusPlugin.click(); + + // Choose default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + await defaultEditor.click(); + + await selectOption(page, editorType, selectors); + + // Add URL for DS to save without error + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.fill('http://prom-url:9090'); + + // Name the DS + const nameInput = page.getByTestId(selectors.pages.DataSource.name); + await nameInput.clear(); + await nameInput.fill(name); + + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + + // Visit explore + await page.goto('/explore'); + + // Choose the right DS + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByRole('button', { name: `${name} ${DATASOURCE_ID}`, exact: true }); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + } + + test('should have a kickstart component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const queryPatterns = page.getByTestId(selectors.components.QueryBuilder.queryPatterns); + await queryPatterns.scrollIntoViewIfNeeded(); + await expect(queryPatterns).toBeVisible(); + }); + + test('should have an explain component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const explain = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.explain); + await explain.scrollIntoViewIfNeeded(); + await expect(explain).toBeVisible(); + }); + + test('should have an editor toggle component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const editorToggle = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.editorToggle); + await editorToggle.scrollIntoViewIfNeeded(); + await expect(editorToggle).toBeVisible(); + }); + + test('should have an options component with legend, format, step, type and exemplars', async ({ + page, + selectors, + }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + // Open options + const options = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.options); + await options.scrollIntoViewIfNeeded(); + await expect(options).toBeVisible(); + await options.click(); + + // Check options + const legend = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.legend); + await legend.scrollIntoViewIfNeeded(); + await expect(legend).toBeVisible(); + + const format = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.format); + await format.scrollIntoViewIfNeeded(); + await expect(format).toBeVisible(); + + const step = page.locator('[data-test-id="prometheus-step"]'); + await step.scrollIntoViewIfNeeded(); + await expect(step).toBeVisible(); + + const type = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.type); + await type.scrollIntoViewIfNeeded(); + await expect(type).toBeVisible(); + + const exemplars = page.getByTestId('prometheus-exemplars'); + await exemplars.scrollIntoViewIfNeeded(); + await expect(exemplars).toBeVisible(); + }); + + test.describe('Code editor', () => { + test('navigates to the code editor with editor type as code', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + }); + + test('navigates to the code editor and opens the metrics browser with metric search, labels, label values, and all components', async ({ + page, + selectors, + }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + + await getResources(page); + + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + const selectMetric = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetric).toBeVisible(); + + const labelNamesFilter = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelNamesFilter + ); + await expect(labelNamesFilter).toBeVisible(); + + const labelValuesFilter = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelValuesFilter + ); + await expect(labelValuesFilter).toBeVisible(); + + const useQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQuery).toBeVisible(); + + const useAsRateQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useAsRateQuery + ); + await expect(useAsRateQuery).toBeVisible(); + + const validateSelector = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.validateSelector + ); + await expect(validateSelector).toBeVisible(); + + const clear = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.clear + ); + await expect(clear).toBeVisible(); + }); + + test('selects a metric in the metrics browser and uses the query', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + + await getResources(page); + + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + const selectMetric = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetric).toBeVisible(); + await selectMetric.fill('met'); + + const metricList = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList + ); + await expect(metricList).toBeVisible(); + + const metricOption = metricList.getByText('metric1'); + await expect(metricOption).toBeVisible(); + await metricOption.click(); + + const useQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQuery).toBeVisible(); + await useQuery.click(); + + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + await expect(queryField).toContainText('metric1'); + }); + }); + + test.describe('Query builder', () => { + test('navigates to the query builder with editor type as code', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + }); + + test('the query builder contains metric select, label filters and operations', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + + const labelSelect = page.getByTestId(selectors.components.QueryBuilder.labelSelect); + await expect(labelSelect).toBeVisible(); + + const matchOperatorSelect = page.getByTestId(selectors.components.QueryBuilder.matchOperatorSelect); + await expect(matchOperatorSelect).toBeVisible(); + + const valueSelect = page.getByTestId(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + }); + + test('can select a metric and provide a hint', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + + await page.getByText('metric1').click(); + + const hints = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.builder.hints); + await expect(hints).toContainText('hint: add rate'); + }); + + test('should have the metrics explorer opened via the metric select', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + + await selectOption(page, 'Metrics explorer', selectors); + + const metricsExplorer = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricsExplorer + ); + await expect(metricsExplorer).toBeVisible(); + }); + }); + } +); + +async function selectOption(page: Page, option: string, selectors: E2ESelectorGroups) { + const optionElement = page.getByTestId(selectors.components.Select.option).filter({ hasText: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts new file mode 100644 index 00000000000..a2b4dfcd672 --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts @@ -0,0 +1,171 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { addDashboard } from '../utils/dashboard-helpers'; +import { getResources } from '../utils/prometheus-helpers'; + +test.describe.skip( + 'Prometheus variable query editor', + { + tag: ['@various', '@wip'], + }, + () => { + const DATASOURCE_NAME = 'prometheusVariableDS'; + + /** + * Click dashboard settings and then the variables tab + */ + async function navigateToVariables(page, selectors) { + const editButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + const settingsButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.settingsButton); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + const variablesTab = page.getByTestId(selectors.components.Tab.title('Variables')); + await variablesTab.click(); + } + + /** + * Begin the process of adding a query type variable for a Prometheus data source + */ + async function addPrometheusQueryVariable(page, selectors, variableName) { + const addVariableButton = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.addVariableCTAV2); + await addVariableButton.click(); + + const nameInput = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2); + await nameInput.clear(); + await nameInput.fill(variableName); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(DATASOURCE_NAME); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + + await getResources(page); + } + + /** + * Create a Prometheus variable and navigate to the query editor to check that it is available to use. + */ + async function variableFlowToQueryEditor(page, selectors, variableName, queryType) { + await addDashboard(page); + await navigateToVariables(page, selectors); + await addPrometheusQueryVariable(page, selectors, variableName); + + // Select query type + const queryTypeSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.variableQueryEditor.queryType + ); + await queryTypeSelect.click(); + await selectOption(page, queryType); + + // Apply the variable + const applyButton = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton); + await applyButton.click(); + + // Close to return to dashboard + const backToDashboardButton = page.getByTestId( + selectors.components.NavToolbar.editDashboard.backToDashboardButton + ); + await expect(backToDashboardButton).toBeVisible(); + await backToDashboardButton.click(); + + // Add visualization + const createNewPanelButton = page.getByTestId(selectors.pages.AddDashboard.itemButton('Create new panel button')); + await expect(createNewPanelButton).toBeVisible(); + await createNewPanelButton.click(); + + // Close the data source picker modal + const closeButton = page.getByRole('button', { name: 'Close menu' }); + await closeButton.click({ force: true }); + + // Select prom data source from the data source list + const dataSourcePickerInput = page.getByTestId(selectors.components.DataSourcePicker.inputV2); + await dataSourcePickerInput.click(); + await dataSourcePickerInput.fill(DATASOURCE_NAME); + await page.keyboard.press('Enter'); + + // Confirm the variable exists in the correct input + switch (queryType) { + case 'Label names': + const labelSelect = page.getByTestId(selectors.components.QueryBuilder.labelSelect); + await expect(labelSelect).toBeVisible(); + await labelSelect.click(); + await selectOption(page, variableName); + break; + case 'Label values': + const valueSelect = page.getByTestId(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + await valueSelect.click(); + await selectOption(page, variableName); + break; + case 'Metrics': + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + await selectOption(page, variableName); + break; + default: + // do nothing + break; + } + } + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + await createDataSourceConfigPage({ type: 'prometheus', name: DATASOURCE_NAME }); + }); + + test('should navigate to variable query editor', async ({ page, selectors }) => { + await addDashboard(page); + await navigateToVariables(page, selectors); + }); + + test('should select a query type for a Prometheus variable query', async ({ page, selectors }) => { + await addDashboard(page); + await navigateToVariables(page, selectors); + await addPrometheusQueryVariable(page, selectors, 'labelsVariable'); + + // Select query type + const queryTypeSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.variableQueryEditor.queryType + ); + await queryTypeSelect.click(); + await selectOption(page, 'Label names'); + }); + + test('should create a label names variable that is selectable in the label select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'labelnames', 'Label names'); + }); + + test('should create a label values variable that is selectable in the label values select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'labelvalues', 'Label values'); + }); + + test('should create a metric names variable that is selectable in the metric select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'metrics', 'Metrics'); + }); + } +); + +async function selectOption(page, option) { + const optionElement = page.getByRole('option', { name: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/query-editor.spec.ts b/e2e-playwright/various-suite/query-editor.spec.ts new file mode 100644 index 00000000000..ee1e4e3e372 --- /dev/null +++ b/e2e-playwright/various-suite/query-editor.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Query editor', + { + tag: ['@various'], + }, + () => { + test('Undo should work in query editor for prometheus', async ({ page }) => { + // Visit the explore page + await page.goto('/explore'); + + // Click on the data source picker + const dataSourcePicker = page.getByTestId('data-testid Data source picker select container'); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + // Select the prometheus data source + const prometheusOption = page.getByText('gdev-prometheus'); + await expect(prometheusOption).toBeVisible(); + await prometheusOption.click(); + + const queryText = `rate(http_requests_total{job="grafana"}[5m])`; + + // Click on the Code radio button + const codeRadioButton = page.getByRole('radio', { name: 'Code' }); + await expect(codeRadioButton).toBeVisible(); + await codeRadioButton.click(); + + // Wait for Monaco editor to load + await page.waitForSelector('.monaco-editor'); + + // Type the query text and then backspace + const queryField = page.locator('.monaco-editor textarea'); + await queryField.fill(queryText); + await queryField.press('Backspace'); + + // Verify the text is truncated + await expect(page.getByText(queryText.slice(0, -1))).toBeVisible(); + + // Use undo (Ctrl+Z) + await queryField.press('Control+z'); + + // Verify the full query text is restored + await expect(page.getByText(queryText)).toBeVisible(); + + // Verify no error alerts are visible + const errorAlert = page.getByTestId('alert-error'); + await expect(errorAlert).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/return-to-previous.spec.ts b/e2e-playwright/various-suite/return-to-previous.spec.ts new file mode 100644 index 00000000000..9c6b9f7748e --- /dev/null +++ b/e2e-playwright/various-suite/return-to-previous.spec.ts @@ -0,0 +1,133 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'ReturnToPrevious button', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page, selectors }) => { + // Navigate to alerting list + await page.goto('/alerting/list'); + + // Click on the first group toggle to expand it + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.first().click(); + + // Click on the toggle to expand the content + const toggle = page.getByTestId(selectors.components.AlertRules.toggle); + await toggle.click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // Store the alert rule URL for later comparison + const alertRuleUrl = page.url(); + + // Click on "View panel" link + const viewPanelLink = page.getByRole('link', { name: 'View panel' }); + await viewPanelLink.click(); + + // Store the URL for use in tests + test.info().annotations.push({ + type: 'alertRuleUrl', + description: alertRuleUrl, + }); + }); + + test('should appear when changing context and go back to alert rule when clicking "Back"', async ({ + page, + selectors, + }) => { + // Check whether all elements of RTP are available + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeVisible(); + + const dismissButton = page.getByTestId(selectors.components.ReturnToPrevious.dismissButton); + await expect(dismissButton).toBeVisible(); + + const backButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(backButton).toBeVisible(); + + // Check that the button contains the expected text + await expect(backButton.getByText('Back to e2e-ReturnToPrevious-test')).toBeVisible(); + + // Click the back button + await backButton.click(); + + // Check whether the RTP button leads back to alert rule + const alertRuleUrl = test.info().annotations.find((a) => a.type === 'alertRuleUrl')?.description; + expect(page.url()).toBe(alertRuleUrl); + }); + + test('should disappear when clicking "Dismiss"', async ({ page, selectors }) => { + const dismissButton = page.getByTestId(selectors.components.ReturnToPrevious.dismissButton); + await expect(dismissButton).toBeVisible(); + await dismissButton.click(); + + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeHidden(); + }); + + test('should not persist when going back to the alert rule details view', async ({ page, selectors }) => { + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeVisible(); + + // Navigate back to alerting list + await page.goto('/alerting/list'); + + // Click on the first group toggle + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.first().click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // The ReturnToPrevious button should not exist + const rtpButtonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(rtpButtonGroup).toBeHidden(); + }); + + test('should override the button label and change the href when user changes alert rules', async ({ + page, + selectors, + }) => { + const backButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(backButton.getByText('Back to e2e-ReturnToPrevious-test')).toBeVisible(); + + // Navigate back to alerting list + await page.goto('/alerting/list'); + + // Click on the last group toggle (different alert rule) + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.last().click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // Store the second alert rule URL + const alertRule2Url = page.url(); + + // Click on "View panel" link + const viewPanelLink = page.getByRole('link', { name: 'View panel' }); + await viewPanelLink.click(); + + // Check that the button now shows the new alert rule name + const newBackButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(newBackButton.getByText('Back to e2e-ReturnToPrevious-test-2')).toBeVisible(); + + // Click the back button + await newBackButton.click(); + + // The ReturnToPrevious button should disappear + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeHidden(); + + // Check whether the RTP button leads back to the second alert rule + expect(page.url()).toBe(alertRule2Url); + }); + } +); diff --git a/e2e-playwright/various-suite/solo-route.spec.ts b/e2e-playwright/various-suite/solo-route.spec.ts new file mode 100644 index 00000000000..81c9e8baf7b --- /dev/null +++ b/e2e-playwright/various-suite/solo-route.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Solo Route', + { + tag: ['@various'], + }, + () => { + test('Can view panels with shared queries in fullscreen', async ({ page, selectors }) => { + // open Panel Tests - Bar Gauge + const soloPanelUrl = selectors.pages.SoloPanel.url('ZqZnVvFZz/datasource-tests-shared-queries?orgId=1&panelId=4'); + await page.goto(soloPanelUrl); + + // Check that there are 6 canvas elements + const canvasElements = page.locator('canvas'); + await expect(canvasElements).toHaveCount(6); + }); + + test('Can view solo panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'TkZXxlNG3/panel-tests-graph-ng?orgId=1&from=1699954597665&to=1699956397665&panelId=54&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Interpolation: Step before')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + + test('Can view solo repeated panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=A')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + + test('Can view solo in repeated row and panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server = B, pod = Rob')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/trace-view-scrolling.spec.ts b/e2e-playwright/various-suite/trace-view-scrolling.spec.ts new file mode 100644 index 00000000000..4343ed5eb13 --- /dev/null +++ b/e2e-playwright/various-suite/trace-view-scrolling.spec.ts @@ -0,0 +1,63 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { test, expect } from '@grafana/plugin-e2e'; + +// this test requires a larger viewport +test.use({ + viewport: { width: 1280, height: 1080 }, +}); + +// TODO for some reason, this test gives "connection refused" errors in CI +test.describe.skip( + 'Trace view', + { + tag: ['@various'], + }, + () => { + test('Can lazy load big traces', async ({ page, selectors }) => { + // Load the fixture data + const fixturePath = join(__dirname, '../fixtures/long-trace-response.json'); + const longTraceResponse = JSON.parse(readFileSync(fixturePath, 'utf8')); + + // Mock the API response + await page.route('*/**/api/traces/trace', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(longTraceResponse), + }); + }); + + // Navigate to Explore page + await page.goto(selectors.pages.Explore.url); + + // Select the Jaeger data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await dataSourcePicker.click(); + const datasourceList = page.getByTestId(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-jaeger').click(); + + // Check that gdev-jaeger is visible in the query editor + await expect(page.getByText('gdev-jaeger')).toBeVisible(); + + // Type the query + const queryField = page + .getByTestId(selectors.components.QueryField.container) + .locator('[contenteditable="true"]'); + await queryField.fill('trace'); + + // Use Shift+Enter to execute the query + await queryField.press('Shift+Enter'); + + // Get the initial count of span bars + const initialSpanBars = page.getByTestId(selectors.components.TraceViewer.spanBar); + const initialSpanBarCount = await initialSpanBars.count(); + + await initialSpanBars.last().scrollIntoViewIfNeeded(); + await expect + .poll(async () => await page.getByTestId(selectors.components.TraceViewer.spanBar).count()) + .toBeGreaterThan(initialSpanBarCount); + }); + } +); diff --git a/e2e-playwright/various-suite/verify-i18n.spec.ts b/e2e-playwright/various-suite/verify-i18n.spec.ts new file mode 100644 index 00000000000..f6a2a00eb9b --- /dev/null +++ b/e2e-playwright/various-suite/verify-i18n.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const I18N_USER = 'i18n-test'; +const I18N_PASSWORD = 'i18n-test'; + +// Separate user to isolate changes from other tests +test.use({ + user: { + user: I18N_USER, + password: I18N_PASSWORD, + }, + storageState: { + cookies: [], + origins: [], + }, +}); + +test.describe( + 'Verify i18n', + { + tag: ['@various'], + }, + () => { + // Map between languages in the language picker and the corresponding translation of the 'Language' label + const languageMap: Record = { + Deutsch: 'Sprache', + Español: 'Idioma', + Français: 'Langue', + 'Português Brasileiro': 'Idioma', + '中文(简体)': '语言', + English: 'Language', + }; + + // Basic test which loops through the defined languages in the picker + // and verifies that the corresponding label is translated correctly + test('loads all the languages correctly', async ({ page, selectors, createUser }) => { + await createUser(); + // login manually for now + await page.getByTestId(selectors.pages.Login.username).fill(I18N_USER); + await page.getByTestId(selectors.pages.Login.password).fill(I18N_PASSWORD); + await page.getByTestId(selectors.pages.Login.submit).click(); + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + + // Navigate to profile page + await page.goto('/profile'); + + const LANGUAGE_SELECTOR = '[id="language-preference-select"]'; + + // Loop through each language and test the translation + for (const [language, label] of Object.entries(languageMap)) { + // Check that the language selector is not disabled + const languageSelector = page.locator(LANGUAGE_SELECTOR); + await expect(languageSelector).not.toBeDisabled(); + + // Click on the language selector + await languageSelector.click(); + + // Clear and type the language name + await languageSelector.clear(); + await languageSelector.fill(language); + + // Press down arrow and enter to select the option + await languageSelector.press('ArrowDown'); + await languageSelector.press('Enter'); + + // Click the save preferences button + const saveButton = page.getByTestId(selectors.components.UserProfile.preferencesSaveButton); + await saveButton.click(); + + // Check that the language label is visible + const languageLabel = page.locator('label').filter({ hasText: label }); + await expect(languageLabel).toBeVisible(); + + // Verify the language selector has the correct value + await expect(languageSelector).toHaveValue(language); + } + }); + } +); diff --git a/e2e-playwright/various-suite/visualization-suggestions.spec.ts b/e2e-playwright/various-suite/visualization-suggestions.spec.ts new file mode 100644 index 00000000000..964af821caa --- /dev/null +++ b/e2e-playwright/various-suite/visualization-suggestions.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Visualization suggestions', + { + tag: ['@various'], + }, + () => { + test('Should be shown and clickable', async ({ page, selectors, gotoPanelEditPage }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage + .getByGrafanaSelector(selectors.components.RadioButton.container) + .filter({ hasText: 'Suggestions' }) + .click(); + + // Verify we see suggestions + const lineChartCard = panelEditPage.getByGrafanaSelector( + selectors.components.VisualizationPreview.card('Line chart') + ); + await expect(lineChartCard).toBeVisible(); + + // Verify search works + const searchInput = page.getByPlaceholder('Search for...'); + await searchInput.fill('Table'); + + // Should no longer see line chart + await expect(lineChartCard).toBeHidden(); + + // Select a visualization + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + + // Verify table header is visible + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header) + ).toBeVisible(); + }); + } +); diff --git a/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts b/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts deleted file mode 100644 index bb1ccee1f72..00000000000 --- a/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'grafana-azure-monitor-datasource' }); - - await expect(await page.getByText('Type: Azure Monitor', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Authentication', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts b/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts deleted file mode 100644 index db3ab66e985..00000000000 --- a/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'stackdriver' }); - - await expect(await page.getByText('Type: Google Cloud Monitoring', { exact: true })).toBeVisible(); - await expect(await page.getByText('Google JWT File', { exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts b/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts deleted file mode 100644 index e834252208d..00000000000 --- a/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'cloudwatch' }); - - await expect(await page.getByText('Type: CloudWatch', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection Details', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts b/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts deleted file mode 100644 index f1232c11d0f..00000000000 --- a/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'elasticsearch' }); - - await expect(await page.getByText('Type: Elasticsearch', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/graphite/graphite.spec.ts b/e2e/plugin-e2e/graphite/graphite.spec.ts deleted file mode 100644 index 9bf58625c4b..00000000000 --- a/e2e/plugin-e2e/graphite/graphite.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'graphite' }); - - await expect(await page.getByText('Type: Graphite', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/influxdb/influxdb.spec.ts b/e2e/plugin-e2e/influxdb/influxdb.spec.ts deleted file mode 100644 index 1538690c9df..00000000000 --- a/e2e/plugin-e2e/influxdb/influxdb.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'influxdb' }); - - await expect(await page.getByText('Type: InfluxDB', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/jaeger/jaeger.spec.ts b/e2e/plugin-e2e/jaeger/jaeger.spec.ts deleted file mode 100644 index c7e1bc88951..00000000000 --- a/e2e/plugin-e2e/jaeger/jaeger.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'jaeger' }); - - await expect(await page.getByText('Type: Jaeger', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mssql/mssql.spec.ts b/e2e/plugin-e2e/mssql/mssql.spec.ts deleted file mode 100644 index 125024935ea..00000000000 --- a/e2e/plugin-e2e/mssql/mssql.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'mssql' }); - - await expect(await page.getByText('Type: Microsoft SQL Server', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mysql/mysql.spec.ts b/e2e/plugin-e2e/mysql/mysql.spec.ts deleted file mode 100644 index 525e0074918..00000000000 --- a/e2e/plugin-e2e/mysql/mysql.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { tableNameWithSpecialCharacter } from './mocks/mysql.mocks'; -import { mockDataSourceRequest } from './utils'; - -test.beforeEach(mockDataSourceRequest); - -test('code editor autocomplete should handle table name escaping/quoting', async ({ explorePage, selectors, page }) => { - await page.getByLabel('Code').check(); - - const editor = explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox'); - await editor.fill('S'); - await page.getByLabel('SELECT FROM
').locator('a').click(); - await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); - await page.keyboard.press('Enter'); - - await expect(editor).toHaveValue(`SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``); - - for (let i = 0; i < tableNameWithSpecialCharacter.length + 2; i++) { - await page.keyboard.press('Backspace'); - } - - await page.keyboard.press('Control+I'); - await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts b/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts deleted file mode 100644 index 68450533ee1..00000000000 --- a/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { selectors } from '@grafana/e2e-selectors'; -import { test, expect } from '@grafana/plugin-e2e'; - -import { normalTableName } from './mocks/mysql.mocks'; -import { mockDataSourceRequest } from './utils'; - -test.beforeEach(mockDataSourceRequest); - -test('visual query builder should handle macros', async ({ explorePage, page }) => { - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); - await page.getByText(normalTableName, { exact: true }).click(); - - // Open Data operations - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).click(); - const select = page.getByLabel('Select options menu'); - await select.locator(page.getByText('$__timeGroupAlias')).click(); - - // Open column selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')).click(); - await select.locator(page.getByText('createdAt')).click(); - - // Open Interval selector - await explorePage - .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Interval')) - .click(); - await select.locator(page.getByText('$__interval')).click(); - - await page.getByRole('button', { name: 'Add column' }).click(); - - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).nth(1).click(); - await select.locator(page.getByText('AVG')).click(); - - await explorePage - .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) - .nth(1) - .click(); - await select.locator(page.getByText('bigint')).click(); - - // Validate the query - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue( - `SELECT\n $__timeGroupAlias(createdAt, $__interval),\n AVG(\`bigint\`)\nFROM\n DataMaker.normalTable\nLIMIT\n 50` - ); -}); - -test('visual query builder should handle time filter macro', async ({ explorePage, page }) => { - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); - await page.getByText(normalTableName, { exact: true }).click(); - - // Open column selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectColumn).click(); - const select = page.getByLabel('Select options menu'); - await select.locator(page.getByText('createdAt')).click(); - - // Toggle where row - await page.getByLabel('Filter').last().click(); - - // Click add filter button - await page.getByRole('button', { name: 'Add filter' }).click(); - await page.getByRole('button', { name: 'Add filter' }).click(); // For some reason we need to click twice - - // Open field selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterField).click(); - await select.locator(page.getByText('createdAt')).click(); - - // Open operator selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); - await select.locator(page.getByText('Macros')).click(); - - // Open macros value selector - await explorePage.getByGrafanaSelector('Macros value selector').click(); - await select.locator(page.getByText('timeFilter', { exact: true })).click(); - - // Validate that the timeFilter macro was added - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n $__timeFilter(createdAt)\nLIMIT\n 50`); - - // Validate that the timeFilter macro was removed when changed to equals operator - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); - await select.locator(page.getByText('==')).click(); - - await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).click(); - await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).blur(); - - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).not.toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n createdAt = NULL\nLIMIT\n 50`); -}); - -test('visual query builder should not crash when filter is set to select_any_in', async ({ explorePage, page }) => { - const queryParams = new URLSearchParams(); - queryParams.set('schemaVersion', '1'); - queryParams.set('orgId', '1'); - const panes = { - mmm: { - datasource: 'P4FDCC188E688367F', - queries: [ - { - refId: 'A', - datasource: { - type: 'mysql', - uid: 'P4FDCC188E688367F', - }, - format: 'table', - rawSql: "SELECT * FROM DataMaker.normalTable WHERE name IN ('a') LIMIT 50 ", - editorMode: 'builder', - sql: { - columns: [ - { - type: 'function', - parameters: [ - { - type: 'functionParameter', - name: '*', - }, - ], - }, - ], - groupBy: [ - { - type: 'groupBy', - property: { - type: 'string', - }, - }, - ], - limit: 50, - whereJsonTree: { - id: 'baa99aa9-0123-4456-b89a-b195d1dcfc6a', - type: 'group', - children1: [ - { - type: 'rule', - id: 'bb9a8bba-89ab-4cde-b012-3195d1dd2c91', - properties: { - fieldSrc: 'field', - field: 'name', - operator: 'select_any_in', - value: ['a'], - valueSrc: ['value'], - valueType: ['text'], - }, - }, - ], - }, - whereString: "name IN ('a')", - }, - dataset: 'DataMaker', - table: 'normalTable', - }, - ], - }, - }; - queryParams.set('panes', JSON.stringify(panes)); - - await explorePage.goto({ queryParams }); - - // Validate the query - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue(`SELECT\n *\nFROM\n DataMaker.normalTable\nWHERE\n name IN ('a')\nLIMIT\n 50`); -}); diff --git a/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts b/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts deleted file mode 100644 index c5e38fcbd10..00000000000 --- a/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'opentsdb' }); - - await expect(await page.getByText('Type: OpenTSDB', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts deleted file mode 100644 index 118cfa7e6f7..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as e2e from '@grafana/e2e-selectors'; -import { expect, test } from '@grafana/plugin-e2e'; - -// let's disable the feature toggles for now, otherwise the getAlertRuleQueryRow fails and I don't see any other way to get the query row -test.use({ featureToggles: { alertingQueryAndExpressionsStepMode: false, alertingNotificationsStepMode: false } }); -test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({ - page, - alertRuleEditPage, -}) => { - await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule'); - - //add working query - const queryA = alertRuleEditPage.getAlertRuleQueryRow('A'); - await queryA.datasource.set('gdev-prometheus'); - await queryA.locator.getByLabel('Code').click(); - await page.waitForFunction(() => window.monaco); - await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); - await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))'); - - //add broken query - const newQuery = await alertRuleEditPage.clickAddQueryRow(); - await newQuery.datasource.set('gdev-prometheus'); - await newQuery.locator.getByLabel('Code').click(); - await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); - await page.keyboard.insertText('topk(5,'); - - await expect(alertRuleEditPage.evaluate()).not.toBeOK(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts deleted file mode 100644 index b37ab2722bf..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test('add panel in already existing dashboard', async ({ gotoDashboardPage, page }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.addPanel(); - await expect(page.url()).toContain('editPanel'); -}); - -test('add panel in new dashboard', async ({ dashboardPage, page }) => { - const panelEditPage = await dashboardPage.addPanel(); - await expect(panelEditPage.panel.locator).toBeVisible(); - await expect(page.url()).toContain('editPanel'); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts deleted file mode 100644 index bf7ebe37653..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; - -test.describe('test createDataSourceConfigPage fixture, saveAndTest and toBeOK matcher', () => { - test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); - await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); - await expect( - configPage.saveAndTest(), - formatExpectError('Expected save data source config to fail when Prometheus server is not running') - ).not.toBeOK(); - }); - - test('valid credentials should return a 200 status code', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); - configPage.mockHealthCheckResponse({ status: 200 }); - await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); - await expect( - configPage.saveAndTest(), - formatExpectError('Expected data source config to be successfully saved') - ).toBeOK(); - }); -}); - -test.describe('test data source with frontend only health check', () => { - test('valid credentials should display a success alert on the page', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'zipkin' }); - configPage.mockHealthCheckResponse({ message: 'Data source is working', status: 'OK' }, 200); - await page.getByPlaceholder('http://localhost:9411').fill('http://localhost:9411'); - await expect(configPage.saveAndTest()).toBeOK(); - await expect( - configPage, - formatExpectError('Expected data source config to display success alert after save') - ).toHaveAlert('success', { hasNotText: 'Datasource updated' }); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts deleted file mode 100644 index a4823228495..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; - -test('query data response should be OK when query is valid', async ({ explorePage }) => { - await explorePage.datasource.set('gdev-testdata'); - await expect(explorePage.runQuery(), formatExpectError('Expected Explore query to execute successfully')).toBeOK(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts deleted file mode 100644 index 48ded217e40..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -const TRUTHY_CUSTOM_TOGGLE = 'custom_toggle1'; -const FALSY_CUSTOM_TOGGLE = 'custom_toggle2'; - -// override the feature toggles defined in playwright.config.ts only for tests in this file -test.use({ - featureToggles: { - [TRUTHY_CUSTOM_TOGGLE]: true, - [FALSY_CUSTOM_TOGGLE]: false, - }, -}); - -test('should set and check feature toggles correctly', async ({ isFeatureToggleEnabled }) => { - expect(await isFeatureToggleEnabled(TRUTHY_CUSTOM_TOGGLE)).toBeTruthy(); - expect(await isFeatureToggleEnabled(FALSY_CUSTOM_TOGGLE)).toBeFalsy(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts deleted file mode 100644 index c55cc76b42c..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as e2e from '@grafana/e2e-selectors'; -import { expect, test } from '@grafana/plugin-e2e'; - -test.describe('Loki editor', () => { - test('Autocomplete features should work as expected.', async ({ page }) => { - // Go to loki datasource in explore - await page.goto( - '/explore?schemaVersion=1&panes=%7B%22iap%22:%7B%22datasource%22:%22gdev-loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22gdev-loki%22%7D,%22editorMode%22:%22builder%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1' - ); - - const queryEditor = page.getByTestId(e2e.selectors.components.QueryField.container); - const queryEditorRows = page.getByTestId('query-editor-rows'); - - async function assertQueryEditorEmpty() { - const queryEditorEmptyText = /^Enter to Rename.+/; - await expect(queryEditor).toHaveText(queryEditorEmptyText); - } - - async function clearInput() { - // Clear focused input - // Monaco appears to need some time to init keybindings after a change, adding this timeout to prevent flake - await page.waitForTimeout(100); - await page.keyboard.press('ControlOrMeta+A'); - await page.keyboard.press('Backspace'); - } - - // assert that the query builder is shown by default - await expect(page.getByText('Label filters')).toHaveCount(1); - - // switch to code editor - await page.getByLabel('Code').click(); - - await page.waitForFunction(() => window.monaco); - await expect(queryEditor).toHaveCount(1); - await assertQueryEditorEmpty(); - - // assert editor automatically adds close paren - await queryEditor.click(); - await page.keyboard.type('time('); - await expect(queryEditor).toContainText('time()'); - - // removes closing brace when opening brace is removed - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('avg_over_time('); - await expect(queryEditor).toContainText('avg_over_time()'); - await page.keyboard.press('Backspace'); - await expect(queryEditor).not.toContainText('avg_over_time()'); - await expect(queryEditor).toContainText('avg_over_time'); - - // keeps closing brace when opening brace is removed and inner values exist - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('time(test'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('Backspace'); - await expect(queryEditor).toContainText('timetest'); - - // overrides an automatically inserted paren - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('time()'); - await expect(queryEditor).toContainText('time()'); - - // does not override manually inserted braces - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('))'); - await expect(queryEditor).toContainText('))'); - - // Should execute the query when enter with shift is pressed - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.press('Shift+Enter'); - await expect(page.getByTestId('explore-no-data')).toHaveCount(1); - - // Suggestions plugin - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('av'); - await expect(queryEditorRows.getByLabel(/avg, docs:/)).toHaveCount(1); - await expect(queryEditorRows.getByLabel(/avg_over_time, docs:/)).toHaveCount(1); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts deleted file mode 100644 index 29bd1b834dc..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { successfulDataQuery } from '../mocks/queries'; - -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test.describe('panel edit page', () => { - test('table panel data assertions with provisioned dashboard', async ({ gotoPanelEditPage }) => { - const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['Field', 'Max', 'Mean', 'Last']); - }); - - test('table panel data assertions', async ({ panelEditPage }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization('Table'); - await panelEditPage.refreshPanel(); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['col1', 'col2']); - await expect(panelEditPage.panel.data, formatExpectError('Could not locate headers in table panel')).toContainText([ - 'val1', - 'val2', - 'val3', - 'val4', - ]); - }); - - test('timeseries panel - table view assertions', async ({ panelEditPage }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization('Time series'); - await panelEditPage.refreshPanel(); - await panelEditPage.toggleTableView(); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['col1', 'col2']); - await expect( - panelEditPage.panel.data, - formatExpectError('Could not locate data elements in table panel') - ).toContainText(['val1', 'val2', 'val3', 'val4']); - }); -}); - -test.describe('dashboard page', () => { - test('getting panel by title', async ({ gotoDashboardPage }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.goto(); - const panel = await dashboardPage.getPanelByTitle('Colored background'); - await expect(panel.fieldNames).toContainText(['Field', 'Max', 'Mean', 'Last']); - }); - - test('getting panel by id', async ({ gotoDashboardPage }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.goto(); - const panel = await dashboardPage.getPanelByTitle('Colored background'); - await expect(panel.fieldNames, formatExpectError('Could not locate header elements in table panel')).toContainText([ - 'Field', - 'Max', - 'Mean', - 'Last', - ]); - }); -}); - -test.describe('explore page', () => { - test('table panel', async ({ explorePage }) => { - const url = - 'left=%7B"datasource":"grafana","queries":%5B%7B"queryType":"randomWalk","refId":"A","datasource":%7B"type":"datasource","uid":"grafana"%7D%7D%5D,"range":%7B"from":"1547161200000","to":"1576364400000"%7D%7D&orgId=1'; - await explorePage.goto({ - queryParams: new URLSearchParams(url), - }); - await expect( - explorePage.timeSeriesPanel.locator, - formatExpectError('Could not locate time series panel in explore page') - ).toBeVisible(); - await expect( - explorePage.tablePanel.locator, - formatExpectError('Could not locate table panel in explore page') - ).toBeVisible(); - await expect(explorePage.tablePanel.fieldNames).toContainText(['time', 'A-series']); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts deleted file mode 100644 index f8bc623f267..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { successfulDataQuery } from '../mocks/queries'; -import { scenarios } from '../mocks/resources'; - -const PANEL_TITLE = 'Table panel E2E test'; -const TABLE_VIZ_NAME = 'Table'; -const TIME_SERIES_VIZ_NAME = 'Time series'; -const STANDARD_OTIONS_CATEGORY = 'Standard options'; -const DISPLAY_NAME_LABEL = 'Display name'; -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test.describe('query editor query data', () => { - test('query data response should be OK when query is valid', async ({ panelEditPage }) => { - await panelEditPage.datasource.set('gdev-testdata'); - await expect( - panelEditPage.refreshPanel(), - formatExpectError('Expected panel query to execute successfully') - ).toBeOK(); - }); - - test('query data response should not be OK and panel error should be displayed when query is invalid', async ({ - panelEditPage, - }) => { - await panelEditPage.datasource.set('gdev-testdata'); - const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); - await queryEditorRow.getByLabel('Labels').fill('invalid-label-format'); - await expect(panelEditPage.refreshPanel(), formatExpectError('Expected panel query to fail')).not.toBeOK(); - await expect( - panelEditPage.panel.getErrorIcon(), - formatExpectError('Expected panel error to be displayed after query execution') - ).toBeVisible(); - }); -}); - -test.describe('query editor with mocked responses', () => { - test('and resource `scenarios` is mocked', async ({ selectors, dashboardPage }) => { - await dashboardPage.mockResourceResponse('scenarios', scenarios); - const panelEditPage = await dashboardPage.addPanel(); - await panelEditPage.datasource.set('gdev-testdata'); - const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); - await queryEditorRow.getByLabel('Scenario').last().click(); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Select.option), - formatExpectError('Expected certain select options to be displayed after clicking on the select input') - ).toHaveText(scenarios.map((s) => s.name)); - }); - - test('mocked query data response', async ({ panelEditPage, selectors }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization(TABLE_VIZ_NAME); - await panelEditPage.refreshPanel(); - await expect( - panelEditPage.panel.getErrorIcon(), - formatExpectError('Did not expect panel error to be displayed after query execution') - ).not.toBeVisible(); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body), - formatExpectError('Expected certain select options to be displayed after clicking on the select input') - ).toHaveText('val1val2val3val4'); - }); -}); - -test.describe('edit panel plugin settings', () => { - test('change viz to table panel, set panel title and collapse section', async ({ - panelEditPage, - selectors, - page, - }) => { - await panelEditPage.setVisualization(TABLE_VIZ_NAME); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker), - formatExpectError('Expected panel visualization to be set to table') - ).toHaveText(TABLE_VIZ_NAME); - await panelEditPage.setPanelTitle(PANEL_TITLE); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_TITLE)), - formatExpectError('Expected panel title to be updated') - ).toBeVisible(); - await panelEditPage.collapseSection(STANDARD_OTIONS_CATEGORY); - await expect( - page.getByText(DISPLAY_NAME_LABEL), - formatExpectError('Expected section to be collapsed') - ).toBeVisible(); - }); - - test('Select time zone in timezone picker', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = await panelEditPage.getCustomOptions('Axis'); - const timeZonePicker = axisOptions.getSelect('Time zone'); - - await timeZonePicker.selectOption('Europe/Stockholm'); - await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); - }); - - test('select unit in unit picker', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const standardOptions = panelEditPage.getStandardOptions(); - const unitPicker = standardOptions.getUnitPicker('Unit'); - - await unitPicker.selectOption('Misc > Pixels'); - - await expect(unitPicker).toHaveSelected('Pixels'); - }); - - test('enter value in number input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const lineWith = axisOptions.getNumberInput('Soft min'); - - await lineWith.fill('10'); - - await expect(lineWith).toHaveValue('10'); - }); - - test('enter value in slider', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const graphOptions = panelEditPage.getCustomOptions('Graph styles'); - const lineWidth = graphOptions.getSliderInput('Line width'); - - await lineWidth.fill('10'); - - await expect(lineWidth).toHaveValue('10'); - }); - - test('select value in single value select', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const standardOptions = panelEditPage.getStandardOptions(); - const colorSchemeSelect = standardOptions.getSelect('Color scheme'); - - await colorSchemeSelect.selectOption('Classic palette'); - await expect(colorSchemeSelect).toHaveSelected('Classic palette'); - }); - - test('clear input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const panelOptions = panelEditPage.getPanelOptions(); - const title = panelOptions.getTextInput('Title'); - - await expect(title).toHaveValue('New panel'); - await title.clear(); - await expect(title).toHaveValue(''); - }); - - test('enter value in input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const panelOptions = panelEditPage.getPanelOptions(); - const description = panelOptions.getTextInput('Description'); - - await expect(description).toHaveValue(''); - await description.fill('This is a panel'); - await expect(description).toHaveValue('This is a panel'); - }); - - test('unchecking switch', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const showBorder = axisOptions.getSwitch('Show border'); - - await expect(showBorder).toBeChecked({ checked: false }); - await showBorder.check(); - await expect(showBorder).toBeChecked(); - - await showBorder.uncheck(); - await expect(showBorder).toBeChecked({ checked: false }); - }); - - test('checking switch', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const showBorder = axisOptions.getSwitch('Show border'); - - await expect(showBorder).toBeChecked({ checked: false }); - await showBorder.check(); - await expect(showBorder).toBeChecked(); - }); - - test('re-selecting value in radio button group', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const placement = axisOptions.getRadioGroup('Placement'); - - await placement.check('Right'); - await expect(placement).toHaveChecked('Right'); - - await placement.check('Auto'); - await expect(placement).toHaveChecked('Auto'); - }); - - test('selecting value in radio button group', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const placement = axisOptions.getRadioGroup('Placement'); - - await placement.check('Right'); - await expect(placement).toHaveChecked('Right'); - }); -}); - -test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => { - const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); - await panelEditPage.backToDashboard(); - await expect(page.url()).not.toContain('editPanel'); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts deleted file mode 100644 index b93d9a8e5dd..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { prometheusLabels } from '../mocks/resources'; - -test('variable query with mocked response', async ({ variableEditPage, page }) => { - variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); - variableEditPage.mockResourceResponse('suggestions*', prometheusLabels); - await variableEditPage.datasource.set('gdev-prometheus'); - await variableEditPage.getByGrafanaSelector('Query type').fill('Label names'); - await page.keyboard.press('Tab'); - await variableEditPage.runQuery(); - await expect( - variableEditPage, - formatExpectError('Expected variable edit page to display certain label names after query execution') - ).toDisplayPreviews(prometheusLabels.data); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts deleted file mode 100644 index 7c7711fa625..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { - await page.goto('/'); - const homePageURL = new URL(page.url()); - await page.goto('/datasources', { waitUntil: 'networkidle' }); - const redirectedPageURL = new URL(page.url()); - expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname); -}); - -test('current user should have viewer role', async ({ page, request }) => { - await page.goto('/'); - const response = await request.get('/api/user/orgs'); - await expect(response).toBeOK(); - await expect(await response.json()).toContainEqual(expect.objectContaining({ role: 'Viewer' })); -}); diff --git a/e2e/plugin-e2e/postgres/postgres.spec.ts b/e2e/plugin-e2e/postgres/postgres.spec.ts deleted file mode 100644 index aafdcbee40b..00000000000 --- a/e2e/plugin-e2e/postgres/postgres.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'grafana-postgresql-datasource' }); - - await expect(await page.getByText('Type: PostgreSQL', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/zipkin/zipkin.spec.ts b/e2e/plugin-e2e/zipkin/zipkin.spec.ts deleted file mode 100644 index 5a58dc656fb..00000000000 --- a/e2e/plugin-e2e/zipkin/zipkin.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'zipkin' }); - - await expect(await page.getByText('Type: Zipkin', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts deleted file mode 100644 index 12d270ae565..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; -import { testIds } from '../testIds'; -import pluginJson from '../plugin.json'; - -test('should display component exposed by another app', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/exposed-components`); - await expect(page.getByTestId(testIds.appB.exposedComponent)).toHaveText('Hello World!'); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts deleted file mode 100644 index a0d6d2246a0..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -import pluginJson from '../plugin.json'; -import { testIds } from '../testIds'; - -test('should render component with usePluginComponents hook', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-components`); - await expect( - page.getByTestId(testIds.addedComponentsPage.container).getByTestId(testIds.appB.reusableAddedComponent) - ).toHaveText('Hello World!'); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts deleted file mode 100644 index 008d3ec5baf..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -import pluginJson from '../plugin.json'; -import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json'; -import { testIds } from '../testIds'; - -test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Go to A').click(); - await page.getByTestId(testIds.modal.open).click(); - await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); -}); - -test('should extend main app with link extension from app B', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Open from B').click(); - await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); -}); - -test('should extend main app with basic link extension from app A', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Basic link').click(); - await page.getByTestId(testIds.modal.open).click(); - await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); -}); - -test('should not display any extensions when extension point is not declared in plugin json when in development mode', async ({ - page, -}) => { - await page.goto(`/a/${testApp3pluginJson.id}`); - const container = await page.getByTestId(testIds.appC.section1); - await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible(); -}); - -const panelTitle = 'Link with defaults'; -const extensionTitle = 'Open from time series...'; - -const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; -const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; - -test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({ - gotoDashboardPage, - page, -}) => { - const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible(); -}); - -test('should add link extension (onclick) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { - const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); -}); - -test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { - const panelTitle = 'Link with new name'; - const extensionTitle = 'Open from piechart'; - const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); -}); diff --git a/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts b/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts deleted file mode 100644 index b72ff0580b5..00000000000 --- a/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; - -// The following tests verify that label and input field association is working correctly. -// If these tests break, e2e tests in external plugins will break too. - -test.describe('config editor ', () => { - let configPage: DataSourceConfigPage; - test.beforeEach(async ({ createDataSourceConfigPage }) => { - configPage = await createDataSourceConfigPage({ type: 'grafana-e2etest-datasource' }); - }); - - test('text input field', async ({ page }) => { - const field = page.getByRole('textbox', { name: 'API key' }); - await expect(field).toBeEmpty(); - await field.fill('test text'); - await expect(field).toHaveValue('test text'); - }); - - test('switch field', async ({ page }) => { - const field = page.getByLabel('Switch Enabled'); - await expect(field).not.toBeChecked(); - await field.check(); - await expect(field).toBeChecked(); - }); - - test('checkbox field', async ({ page }) => { - const field = page.getByRole('checkbox', { name: 'Checkbox Enabled' }); - await expect(field).not.toBeChecked(); - await field.check({ force: true }); - await expect(field).toBeChecked(); - }); - - test('select field', async ({ page, selectors }) => { - const field = page.getByRole('combobox', { name: 'Auth type' }); - await field.click(); - const option = selectors.components.Select.option; - await expect(configPage.getByGrafanaSelector(option)).toHaveText(['keys', 'credentials']); - }); -}); diff --git a/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts b/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts deleted file mode 100644 index 72565e5a885..00000000000 --- a/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('should render variable editor', async ({ variableEditPage, page }) => { - await variableEditPage.datasource.set('gdev-e2etestdatasource'); - await expect(page.getByRole('textbox', { name: 'Query Text' })).toBeVisible(); -}); - -test('create new, successful variable query', async ({ variableEditPage, page }) => { - await variableEditPage.datasource.set('gdev-e2etestdatasource'); - await page.getByRole('textbox', { name: 'Query Text' }).fill('variableQuery'); - await variableEditPage.runQuery(); - await expect(variableEditPage).toDisplayPreviews(['A', 'B']); -}); diff --git a/eslint.config.js b/eslint.config.js index 9f5bcc9da52..d5bd122f12c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -43,7 +43,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', diff --git a/package.json b/package.json index 617104b7a1f..55d58e1198c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", "e2e:playwright": "yarn playwright test", - "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e/plugin-e2e/start-and-run-suite", + "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e-playwright/start-and-run-suite", + "e2e:playwright:storybook": "yarn playwright test -c playwright.storybook.config.ts", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", "e2e:plugin:build": "nx run-many -t build --projects='@test-plugins/*'", "e2e:plugin:build:dev": "nx run-many -t dev --projects='@test-plugins/*' --maxParallel=100", @@ -454,7 +455,7 @@ "packages": [ "packages/*", "public/app/plugins/*/*", - "e2e/test-plugins/*" + "e2e-playwright/test-plugins/*" ] }, "engines": { diff --git a/pkg/build/a11y/main.go b/pkg/build/a11y/main.go index 444e59213d2..dd85c2c3328 100644 --- a/pkg/build/a11y/main.go +++ b/pkg/build/a11y/main.go @@ -91,7 +91,7 @@ func run(ctx context.Context, cmd *cli.Command) error { hostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ Include: []string{ "./devenv", - "./e2e/test-plugins", // Directory is included so provisioning works, but they're not actually build + "./e2e-playwright/test-plugins", // Directory is included so provisioning works, but they're not actually build "./scripts/grafana-server/custom.ini", "./scripts/grafana-server/start-server", "./scripts/grafana-server/kill-server", diff --git a/pkg/build/a11y/service.go b/pkg/build/a11y/service.go index c866bc60520..98bcb5cf20b 100644 --- a/pkg/build/a11y/service.go +++ b/pkg/build/a11y/service.go @@ -22,7 +22,7 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp WithExec([]string{"mkdir", "-p", "/src/grafana"}). WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). WithDirectory("/src/grafana/devenv", opts.HostSrc.Directory("./devenv")). - WithDirectory("/src/grafana/e2e/test-plugins", opts.HostSrc.Directory("./e2e/test-plugins")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", opts.HostSrc.Directory("./e2e-playwright/test-plugins")). WithDirectory("/src/grafana/scripts", opts.HostSrc.Directory("./scripts")). WithWorkdir("/src/grafana"). WithEnvVariable("GF_APP_MODE", "development"). diff --git a/pkg/build/e2e-playwright/README.md b/pkg/build/e2e-playwright/README.md new file mode 100644 index 00000000000..d5fc311d696 --- /dev/null +++ b/pkg/build/e2e-playwright/README.md @@ -0,0 +1,22 @@ +# Pa11y accessability tests + +We use pa11y to run some automated simple accessability tests. They're ran with dagger to help orchestrate starting server + tests in a reproducable manner. + +To run the tests locally: + +1. Install dagger locally https://docs.dagger.io/install/ +2. Grab the grafana.tar.gz artifact by either + 1. Downloading it from the Github Action artifact from your PR + 1. Build it locally with: + ```sh + dagger run go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="$PWD" > dist/files.txt + cat dist/files.txt # Will output the path to the grafana.tar.gz + ``` +3. Run the dagger pipeline with: + ```sh + dagger -v run go run ./pkg/build/a11y --package=(full path to .tar.gz) --results=./pa11y-ci-results.json + ``` + The JSON results file will be saved to the file from the `--results` arg +4. If they fail and you want to see the full output + 1. Run the dagger command with `dagger -vE [...]` + 2. At the end, arrow up to the exec pa11y-ci segment and hit Enter \ No newline at end of file diff --git a/pkg/build/e2e-playwright/e2e.go b/pkg/build/e2e-playwright/e2e.go new file mode 100644 index 00000000000..35579e8dd2f --- /dev/null +++ b/pkg/build/e2e-playwright/e2e.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "dagger.io/dagger" +) + +var ( + // Locations in the container to write results to + testResultsDir = "/playwright-test-results" + htmlResultsDir = "/playwright-html-report" + blobResultsDir = "/playwright-blob-report" +) + +type RunTestOpts struct { + GrafanaService *dagger.Service + FrontendContainer *dagger.Container + HostSrc *dagger.Directory + Shard string + HTMLReportExportDir string + BlobReportExportDir string + TestResultsExportDir string +} + +func RunTest( + ctx context.Context, + d *dagger.Client, + opts RunTestOpts, +) (*dagger.Container, error) { + playwrightCommand := buildPlaywrightCommand(opts) + + e2eContainer := opts.FrontendContainer. + WithWorkdir("/src"). + WithDirectory("/src", opts.HostSrc). + WithMountedCache(".nx", d.CacheVolume("nx-cache")). + WithEnvVariable("HOST", grafanaHost). + WithEnvVariable("PORT", fmt.Sprint(grafanaPort)). + WithServiceBinding(grafanaHost, opts.GrafanaService). + WithEnvVariable("bustcache", "1"). + WithEnvVariable("PLAYWRIGHT_HTML_OPEN", "never"). + WithEnvVariable("PLAYWRIGHT_HTML_OUTPUT_DIR", htmlResultsDir). + WithEnvVariable("PLAYWRIGHT_BLOB_OUTPUT_DIR", blobResultsDir). + WithExec(playwrightCommand, dagger.ContainerWithExecOpts{ + Expect: dagger.ReturnTypeAny, + }) + + if opts.TestResultsExportDir != "" { + _, err := e2eContainer.Directory(testResultsDir).Export(ctx, opts.TestResultsExportDir) + if err != nil { + return nil, err + } + } + + if opts.HTMLReportExportDir != "" { + _, err := e2eContainer.Directory(htmlResultsDir).Export(ctx, opts.HTMLReportExportDir) + if err != nil { + return nil, err + } + } + + if opts.BlobReportExportDir != "" { + _, err := e2eContainer.Directory(blobResultsDir).Export(ctx, opts.BlobReportExportDir) + if err != nil { + return nil, err + } + } + + return e2eContainer, nil +} + +func buildPlaywrightCommand(opts RunTestOpts) []string { + playwrightReporters := []string{ + "dot", // minimal output in shards + } + + if opts.HTMLReportExportDir != "" { + playwrightReporters = append(playwrightReporters, "html") + } + + if opts.BlobReportExportDir != "" { + playwrightReporters = append(playwrightReporters, "blob") + } + + playwrightCommand := []string{ + "yarn", + "e2e:playwright", + "--reporter", + strings.Join(playwrightReporters, ","), + "--output", + testResultsDir, + } + + if opts.Shard != "" { + playwrightCommand = append(playwrightCommand, "--shard", opts.Shard) + } + + return playwrightCommand +} diff --git a/pkg/build/e2e-playwright/frontend.go b/pkg/build/e2e-playwright/frontend.go new file mode 100644 index 00000000000..625ea155ae4 --- /dev/null +++ b/pkg/build/e2e-playwright/frontend.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "dagger.io/dagger" +) + +type Deps struct { + NodeVersion string + PlaywrightVersion string +} + +// Create a container with frontend dependencies installed, ready to build plugins +// or run e2e tests. +// Theoretically we would setup Playwright in e2e.go, but to optimise layer caching +// we want it to happen before yarn install. +func WithFrontendContainer(ctx context.Context, d *dagger.Client, yarnHostSrc *dagger.Directory) (*dagger.Container, error) { + deps, err := GetVersions(ctx, yarnHostSrc) + if err != nil { + return nil, err + } + + nodeBase := WithNode(d, deps.NodeVersion) + playwrightBase := WithPlaywright(d, nodeBase, deps.PlaywrightVersion) + + return WithYarnInstall(d, playwrightBase, yarnHostSrc), nil +} + +func GetVersions(ctx context.Context, src *dagger.Directory) (Deps, error) { + nvmrc, err := src.File(".nvmrc").Contents(ctx) + if err != nil { + return Deps{}, err + } + pkgJSON, err := src.File("package.json").Contents(ctx) + if err != nil { + return Deps{}, err + } + + // parse package.json + var pkgJson struct { + DevDependencies map[string]string `json:"devDependencies"` + } + if err := json.Unmarshal([]byte(pkgJSON), &pkgJson); err != nil { + return Deps{}, err + } + + return Deps{ + NodeVersion: strings.TrimSpace(strings.TrimPrefix(nvmrc, "v")), + PlaywrightVersion: strings.TrimSpace(pkgJson.DevDependencies["@playwright/test"]), + }, nil +} + +func WithNode(d *dagger.Client, version string) *dagger.Container { + nodeImage := fmt.Sprintf("node:%s-slim", strings.TrimPrefix(version, "v")) + return d.Container().From(nodeImage) +} + +func WithPlaywright(d *dagger.Client, base *dagger.Container, version string) *dagger.Container { + brCache := d.CacheVolume("playwright-browsers") + return base. + WithEnvVariable("PLAYWRIGHT_BROWSERS_PATH", "/playwright-cache"). + WithMountedCache("/playwright-cache", brCache). + WithExec([]string{"npx", "-y", "playwright@" + version, "install", "--with-deps"}) +} + +func WithYarnInstall(d *dagger.Client, base *dagger.Container, yarnHostSrc *dagger.Directory) *dagger.Container { + yarnCache := d.CacheVolume("yarn-cache") + + return base. + WithWorkdir("/src"). + WithMountedCache("/.yarn", yarnCache). + WithEnvVariable("YARN_CACHE_FOLDER", "/.yarn"). + WithEnvVariable("CYPRESS_INSTALL_BINARY", "0"). // Don't download Cypress binaries + + // It's important to copy all files here because the whole src directory is then copied into the test runner container + WithDirectory("/src", yarnHostSrc). + WithExec([]string{"corepack", "enable"}). + WithExec([]string{"corepack", "install"}). + WithExec([]string{"yarn", "install", "--immutable"}) +} diff --git a/pkg/build/e2e-playwright/main.go b/pkg/build/e2e-playwright/main.go new file mode 100644 index 00000000000..09384c8614b --- /dev/null +++ b/pkg/build/e2e-playwright/main.go @@ -0,0 +1,243 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + + "dagger.io/dagger" + "github.com/urfave/cli/v3" +) + +var ( + grafanaHost = "grafana" + grafanaPort = 3001 +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := NewApp().Run(ctx, os.Args); err != nil { + cancel() + fmt.Println(err) + os.Exit(1) + } +} + +func NewApp() *cli.Command { + return &cli.Command{ + Name: "a11y", + Usage: "Run Grafana playwright e2e tests", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "grafana-dir", + Usage: "Path to the grafana/grafana clone directory", + Value: ".", + Validator: mustBeDir("grafana-dir", false, false), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "package", + Usage: "Path to the grafana tar.gz package", + Value: "grafana.tar.gz", + Validator: mustBeFile("package", false), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "license", + Usage: "Path to the Grafana Enterprise license file (optional)", + Validator: mustBeFile("license", true), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "shard", + Usage: "Test shard to run. See Playwright docs", + }, + &cli.StringFlag{ + Name: "results-dir", + Usage: "Path to the directory to export the playwright test results to (optional)", + Validator: mustBeDir("results-dir", true, true), + }, + &cli.StringFlag{ + Name: "html-dir", + Usage: "Enables the HTML reporter, exported to this directory (optional)", + Validator: mustBeDir("html-dir", true, true), + }, + &cli.StringFlag{ + Name: "blob-dir", + Usage: "Enables the blob reporter, exported to this directory. Useful with --shard (optional)", + Validator: mustBeDir("blob-dir", true, true), + }, + }, + Action: run, + } +} + +func run(ctx context.Context, cmd *cli.Command) error { + grafanaDir := cmd.String("grafana-dir") + targzPath := cmd.String("package") + licensePath := cmd.String("license") + pwShard := cmd.String("shard") + resultsDir := cmd.String("results-dir") + htmlDir := cmd.String("html-dir") + blobDir := cmd.String("blob-dir") + // pa11yConfigPath := cmd.String("config") + // pa11yResultsPath := cmd.String("results") + // noThresholdFail := cmd.Bool("no-threshold-fail") + + d, err := dagger.Connect(ctx) + if err != nil { + return fmt.Errorf("failed to connect to Dagger: %w", err) + } + + // Explicitly only the files used by the grafana-server service + grafanaHostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ + Include: []string{ + "./devenv", + + // Must build test plugins to run e2e tests + "./e2e-playwright/test-plugins", + "./packages/grafana-plugin-configs", + + "./scripts/grafana-server/custom.ini", + "./scripts/grafana-server/start-server", + "./scripts/grafana-server/kill-server", + "./scripts/grafana-server/variables", + }, + }) + + // Minimal files needed to run yarn install + yarnHostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ + Include: []string{ + "package.json", + "yarn.lock", + ".yarnrc.yml", + ".yarn", + "packages/*/package.json", + "packages/grafana-plugin-configs", + "public/app/plugins/*/*/package.json", + "e2e-playwright/test-plugins/*/package.json", + ".nvmrc", + }, + }) + + // Files needed to run e2e tests. yarnHostSrc will be copied into the test runner container as well. + e2eHostSrc := d.Host().Directory(".", dagger.HostDirectoryOpts{ + Include: []string{ + "public/app/types/*.d.ts", + "public/app/core/icons/cached.json", + + // packages we use in playwright tests + "packages", // TODO: do we need all of this? + "public/app/plugins", // TODO: do we need all of this? + + // e2e files + "e2e-playwright", + "e2e-playwright/test-plugins", + "playwright.config.ts", + }, + Exclude: []string{ + "**/dist", + }, + }) + + frontendContainer, err := WithFrontendContainer(ctx, d, yarnHostSrc) + if err != nil { + return fmt.Errorf("failed to create frontend container: %w", err) + } + + targz := d.Host().File(targzPath) + + var license *dagger.File + if licensePath != "" { + license = d.Host().File(licensePath) + } + + svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{ + HostSrc: grafanaHostSrc, + FrontendContainer: frontendContainer, + GrafanaTarGz: targz, + License: license, + }) + if err != nil { + return fmt.Errorf("failed to create Grafana service: %w", err) + } + + runOpts := RunTestOpts{ + GrafanaService: svc, + FrontendContainer: frontendContainer, + HostSrc: e2eHostSrc, + Shard: pwShard, + TestResultsExportDir: resultsDir, + HTMLReportExportDir: htmlDir, + BlobReportExportDir: blobDir, + } + + c, runErr := RunTest(ctx, d, runOpts) + if runErr != nil { + return fmt.Errorf("failed to run e2e test suite: %w", runErr) + } + + c, syncErr := c.Sync(ctx) + if syncErr != nil { + return fmt.Errorf("failed to sync e2e test suite: %w", syncErr) + } + + code, codeErr := c.ExitCode(ctx) + if codeErr != nil { + return fmt.Errorf("failed to get exit code of e2e test suite: %w", codeErr) + } + + if code == 0 { + log.Printf("e2e tests passed with exit code %d", code) + } else { + return fmt.Errorf("e2e tests failed with exit code %d", code) + } + + log.Println("e2e tests completed successfully") + return nil +} + +func mustBeFile(arg string, emptyOk bool) func(string) error { + return func(s string) error { + if s == "" { + if emptyOk { + return nil + } + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if stat.IsDir() { + return cli.Exit(arg+" must be a file, not a directory: "+s, 1) + } + return nil + } +} + +func mustBeDir(arg string, emptyOk bool, notExistOk bool) func(string) error { + return func(s string) error { + if s == "" { + if emptyOk { + return nil + } + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + if notExistOk { + return nil + } + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if !stat.IsDir() { + return cli.Exit(arg+" must be a directory: "+s, 1) + } + return nil + } +} diff --git a/pkg/build/e2e-playwright/service.go b/pkg/build/e2e-playwright/service.go new file mode 100644 index 00000000000..d162451ef70 --- /dev/null +++ b/pkg/build/e2e-playwright/service.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "dagger.io/dagger" +) + +type GrafanaServiceOpts struct { + HostSrc *dagger.Directory + FrontendContainer *dagger.Container + GrafanaTarGz *dagger.File + License *dagger.File +} + +func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOpts) (*dagger.Service, error) { + testPlugins := opts.FrontendContainer. + WithDirectory("e2e-playwright/test-plugins", opts.HostSrc.Directory("./e2e-playwright/test-plugins")). + WithDirectory("packages/grafana-plugin-configs", opts.HostSrc.Directory("./packages/grafana-plugin-configs")). + WithExec([]string{"yarn", "e2e:plugin:build"}) + + container := d.Container().From("alpine:3"). + WithExec([]string{"apk", "add", "--no-cache", "bash", "tar", "netcat-openbsd", "util-linux"}). + WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz). + WithExec([]string{"mkdir", "-p", "/src/grafana"}). + WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). + WithDirectory("/src/grafana/devenv", opts.HostSrc.Directory("./devenv")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", testPlugins.Directory("./e2e-playwright/test-plugins")). + WithDirectory("/src/grafana/scripts", opts.HostSrc.Directory("./scripts")). + WithWorkdir("/src/grafana"). + // Only set config variables here that are specific to the dagger/GHA runner. + // Prefer to configure scripts/grafana-server/custom.ini instead so they're also used + // when running locally. + WithEnvVariable("GF_SERVER_HTTP_PORT", fmt.Sprint(grafanaPort)). + WithExposedPort(grafanaPort) + + var licenseArg string + if opts.License != nil { + licenseArg = "/src/license.jwt" + container = container.WithMountedFile(licenseArg, opts.License) + } + + // We add all GF_ environment variables to allow for overriding Grafana configuration. + // It is unlikely the runner has any such otherwise. + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GF_") { + parts := strings.SplitN(env, "=", 2) + container = container.WithEnvVariable(parts[0], parts[1]) + } + } + + svc := container.AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "scripts/grafana-server/start-server", licenseArg}}) + + return svc, nil +} diff --git a/pkg/build/e2e/service.go b/pkg/build/e2e/service.go index 95de093ec18..5c6eb0c4e78 100644 --- a/pkg/build/e2e/service.go +++ b/pkg/build/e2e/service.go @@ -82,6 +82,7 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). WithDirectory("/src/grafana/devenv", src.Directory("/src/devenv")). WithDirectory("/src/grafana/e2e", src.Directory("/src/e2e")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", src.Directory("/src/e2e-playwright/test-plugins")). WithDirectory("/src/grafana/scripts", src.Directory("/src/scripts")). WithDirectory("/src/grafana/tools", src.Directory("/src/tools")). WithWorkdir("/src/grafana"). diff --git a/playwright.config.ts b/playwright.config.ts index 39ac6aebd5f..648e464f387 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,7 +3,8 @@ import path, { dirname } from 'path'; import { PluginOptions } from '@grafana/plugin-e2e'; -const testDirRoot = 'e2e/plugin-e2e/'; +const testDirRoot = 'e2e-playwright'; +const pluginDirRoot = path.join(testDirRoot, 'plugin-e2e'); export default defineConfig({ fullyParallel: true, @@ -11,7 +12,9 @@ export default defineConfig({ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, - reporter: 'html', + reporter: [ + ['html'], // pretty + ], use: { baseURL: `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}`, trace: 'retain-on-failure', @@ -19,6 +22,8 @@ export default defineConfig({ username: 'admin', password: 'admin', }, + screenshot: 'only-on-failure', + permissions: ['clipboard-read', 'clipboard-write'], provisioningRootDir: path.join(process.cwd(), process.env.PROV_DIR ?? 'conf/provisioning'), }, projects: [ @@ -44,7 +49,7 @@ export default defineConfig({ // Run all tests in parallel using user with admin role { name: 'admin', - testDir: path.join(testDirRoot, '/plugin-e2e-api-tests/as-admin-user'), + testDir: path.join(pluginDirRoot, '/plugin-e2e-api-tests/as-admin-user'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -54,7 +59,7 @@ export default defineConfig({ // Run all tests in parallel using user with viewer role { name: 'viewer', - testDir: path.join(testDirRoot, '/plugin-e2e-api-tests/as-viewer-user'), + testDir: path.join(pluginDirRoot, '/plugin-e2e-api-tests/as-viewer-user'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/viewer.json', @@ -63,7 +68,7 @@ export default defineConfig({ }, { name: 'elasticsearch', - testDir: path.join(testDirRoot, '/elasticsearch'), + testDir: path.join(pluginDirRoot, '/elasticsearch'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -72,7 +77,7 @@ export default defineConfig({ }, { name: 'mysql', - testDir: path.join(testDirRoot, '/mysql'), + testDir: path.join(pluginDirRoot, '/mysql'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -81,7 +86,7 @@ export default defineConfig({ }, { name: 'mssql', - testDir: path.join(testDirRoot, '/mssql'), + testDir: path.join(pluginDirRoot, '/mssql'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -90,7 +95,7 @@ export default defineConfig({ }, { name: 'extensions-test-app', - testDir: 'e2e/test-plugins/grafana-extensionstest-app', + testDir: path.join(testDirRoot, '/test-plugins/grafana-extensionstest-app'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -99,7 +104,7 @@ export default defineConfig({ }, { name: 'grafana-e2etest-datasource', - testDir: 'e2e/test-plugins/grafana-test-datasource', + testDir: path.join(testDirRoot, '/test-plugins/grafana-test-datasource'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -108,7 +113,7 @@ export default defineConfig({ }, { name: 'cloudwatch', - testDir: path.join(testDirRoot, '/cloudwatch'), + testDir: path.join(pluginDirRoot, '/cloudwatch'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -117,7 +122,7 @@ export default defineConfig({ }, { name: 'azuremonitor', - testDir: path.join(testDirRoot, '/azuremonitor'), + testDir: path.join(pluginDirRoot, '/azuremonitor'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -126,7 +131,7 @@ export default defineConfig({ }, { name: 'cloudmonitoring', - testDir: path.join(testDirRoot, '/cloudmonitoring'), + testDir: path.join(pluginDirRoot, '/cloudmonitoring'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -135,7 +140,7 @@ export default defineConfig({ }, { name: 'graphite', - testDir: path.join(testDirRoot, '/graphite'), + testDir: path.join(pluginDirRoot, '/graphite'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -144,7 +149,7 @@ export default defineConfig({ }, { name: 'influxdb', - testDir: path.join(testDirRoot, '/influxdb'), + testDir: path.join(pluginDirRoot, '/influxdb'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -153,7 +158,7 @@ export default defineConfig({ }, { name: 'opentsdb', - testDir: path.join(testDirRoot, '/opentsdb'), + testDir: path.join(pluginDirRoot, '/opentsdb'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -162,7 +167,7 @@ export default defineConfig({ }, { name: 'jaeger', - testDir: path.join(testDirRoot, '/jaeger'), + testDir: path.join(pluginDirRoot, '/jaeger'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -171,7 +176,7 @@ export default defineConfig({ }, { name: 'grafana-postgresql-datasource', - testDir: path.join(testDirRoot, '/grafana-postgresql-datasource'), + testDir: path.join(pluginDirRoot, '/grafana-postgresql-datasource'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -180,7 +185,50 @@ export default defineConfig({ }, { name: 'zipkin', - testDir: path.join(testDirRoot, '/zipkin'), + testDir: path.join(pluginDirRoot, '/zipkin'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'scenarios', + testDir: path.join(testDirRoot, '/scenarios'), + use: { + ...devices['Desktop Chrome'], + }, + }, + { + name: 'various', + testDir: path.join(testDirRoot, '/various-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'panels', + testDir: path.join(testDirRoot, '/panels-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'smoke', + testDir: path.join(testDirRoot, '/smoke-tests-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'dashboards', + testDir: path.join(testDirRoot, '/dashboards-suite'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -196,5 +244,23 @@ export default defineConfig({ }, dependencies: ['authenticate'], }, + { + name: 'cloud-plugins', + testDir: path.join(testDirRoot, '/cloud-plugins-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'dashboard-new-layouts', + testDir: path.join(testDirRoot, '/dashboard-new-layouts'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, ], }); diff --git a/playwright.storybook.config.ts b/playwright.storybook.config.ts new file mode 100644 index 00000000000..faa18ce7d05 --- /dev/null +++ b/playwright.storybook.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; + +import { PluginOptions } from '@grafana/plugin-e2e'; + +const testDirRoot = 'e2e-playwright'; + +export default defineConfig({ + fullyParallel: true, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:9001', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + webServer: { + command: 'yarn storybook', + url: 'http://localhost:9001', + stdout: 'ignore', + stderr: 'pipe', + }, + projects: [ + { + name: 'storybook', + testDir: path.join(testDirRoot, '/storybook'), + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}); diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 50b23341467..63175726fd5 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -834,7 +834,7 @@ def playwright_e2e_tests_step(): "commands": [ "npx wait-on@7.0.1 http://$HOST:$PORT", "yarn playwright install --with-deps chromium", - "yarn e2e:playwright", + "yarn e2e:playwright --grep @plugins", ], } diff --git a/scripts/grafana-server/custom.ini b/scripts/grafana-server/custom.ini index 68968c502fa..d8e92d8782e 100644 --- a/scripts/grafana-server/custom.ini +++ b/scripts/grafana-server/custom.ini @@ -1,3 +1,8 @@ +app_mode = development + +[log] +level = error +filters = http.server:info,settings:info [security] content_security_policy = true diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server index 6162439a299..6a71bf6e19b 100755 --- a/scripts/grafana-server/start-server +++ b/scripts/grafana-server/start-server @@ -41,11 +41,11 @@ echo -e "Copying custom plugins from e2e tests" mkdir -p "$RUNDIR/data/plugins" -if [ -d "./e2e/test-plugins" ]; then - ln -s $(realpath ./e2e/test-plugins/*) "$RUNDIR/data/plugins" +if [ -d "./e2e-playwright/test-plugins" ]; then + ln -s $(realpath ./e2e-playwright/test-plugins/*) "$RUNDIR/data/plugins" # when running in CI -elif [ -d "../e2e/test-plugins" ]; then - cp -r "../e2e/test-plugins" "$RUNDIR/data/plugins" +elif [ -d "../e2e-playwright/test-plugins" ]; then + cp -r "../e2e-playwright/test-plugins" "$RUNDIR/data/plugins" fi if [ "$INSTALL_IMAGE_RENDERER" ]; then @@ -60,17 +60,17 @@ cp devenv/dashboards.yaml $PROV_DIR/dashboards cp devenv/alert_rules.yaml $PROV_DIR/alerting cp devenv/plugins.yaml $PROV_DIR/plugins -cp -r devenv $RUNDIR +cp -r devenv $ -echo -e "Starting Grafana Server port $PORT" +echo -e "Starting Grafana server port $PORT" -$RUNDIR/bin/"$ARCH"grafana-server \ - --homepath=$HOME_PATH \ - --pidfile=$RUNDIR/pid \ - cfg:server.http_port=$PORT \ - cfg:server.router_logging=1 \ - cfg:app_mode=development \ - cfg:enterprise.license_path=$1 - -# 2>&1 > $RUNDIR/output.log & -# cfg:log.level=debug \ +# We get a lot of "context canceled" errors from navigating away from pages, +# so filter them out because they're not useful +{ + exec "$RUNDIR/bin/${ARCH}grafana" server \ + --homepath="$HOME_PATH" \ + --pidfile="$RUNDIR/pid" \ + cfg:server.http_port="$PORT" \ + cfg:enterprise.license_path="$1" \ + 2>&1 +} | grep -v -i "context canceled" diff --git a/yarn.lock b/yarn.lock index 58e016dab57..192b15b61bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8583,9 +8583,9 @@ __metadata: languageName: node linkType: hard -"@test-plugins/extensions-test-app@workspace:e2e/test-plugins/grafana-extensionstest-app": +"@test-plugins/extensions-test-app@workspace:e2e-playwright/test-plugins/grafana-extensionstest-app": version: 0.0.0-use.local - resolution: "@test-plugins/extensions-test-app@workspace:e2e/test-plugins/grafana-extensionstest-app" + resolution: "@test-plugins/extensions-test-app@workspace:e2e-playwright/test-plugins/grafana-extensionstest-app" dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" @@ -8615,9 +8615,9 @@ __metadata: languageName: unknown linkType: soft -"@test-plugins/grafana-e2etest-datasource@workspace:e2e/test-plugins/grafana-test-datasource": +"@test-plugins/grafana-e2etest-datasource@workspace:e2e-playwright/test-plugins/grafana-test-datasource": version: 0.0.0-use.local - resolution: "@test-plugins/grafana-e2etest-datasource@workspace:e2e/test-plugins/grafana-test-datasource" + resolution: "@test-plugins/grafana-e2etest-datasource@workspace:e2e-playwright/test-plugins/grafana-test-datasource" dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*"