diff --git a/.drone.yml b/.drone.yml index f8d744be837..fa0cfee8ed5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,7 +11,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -111,7 +111,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -309,15 +309,18 @@ steps: - package image: grafana/build-container:1.4.9 name: copy-packages-for-docker -- depends_on: +- commands: + - ./bin/grabpl build-docker --edition oss -archs amd64 + depends_on: - copy-packages-for-docker - image: grafana/drone-grafana-docker:0.3.2 + environment: + GCP_KEY: + from_secret: gcp_key + image: google/cloud-sdk name: build-docker-images - settings: - archs: amd64 - dry_run: true - edition: oss - ubuntu: false + volumes: + - name: docker + path: /var/run/docker.sock trigger: event: - pull_request @@ -359,7 +362,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -420,7 +423,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -521,7 +524,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -761,30 +764,66 @@ steps: - package image: grafana/build-container:1.4.9 name: copy-packages-for-docker -- depends_on: +- commands: + - ./bin/grabpl build-docker --edition oss + depends_on: - copy-packages-for-docker - image: grafana/drone-grafana-docker:0.3.2 + environment: + GCP_KEY: + from_secret: gcp_key + image: google/cloud-sdk name: build-docker-images - settings: - dry_run: false - edition: oss - password: - from_secret: docker_password - ubuntu: false - username: - from_secret: docker_user -- depends_on: + volumes: + - name: docker + path: /var/run/docker.sock +- commands: + - ./bin/grabpl build-docker --edition oss --ubuntu + depends_on: - copy-packages-for-docker - image: grafana/drone-grafana-docker:0.3.2 + environment: + GCP_KEY: + from_secret: gcp_key + image: google/cloud-sdk name: build-docker-images-ubuntu - settings: - dry_run: false - edition: oss - password: + volumes: + - name: docker + path: /var/run/docker.sock +- commands: + - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana --base alpine --base + ubuntu --arch amd64 --arch arm64 --arch armv7 + depends_on: + - build-docker-images + - build-docker-images-ubuntu + environment: + DOCKER_PASSWORD: from_secret: docker_password - ubuntu: true - username: - from_secret: docker_user + DOCKER_USER: + from_secret: docker_username + GCP_KEY: + from_secret: gcp_key + image: google/cloud-sdk + name: publish-images-grafana + volumes: + - name: docker + path: /var/run/docker.sock +- commands: + - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana-oss --base alpine + --base ubuntu --arch amd64 --arch arm64 --arch armv7 + depends_on: + - build-docker-images + - build-docker-images-ubuntu + environment: + DOCKER_PASSWORD: + from_secret: docker_password + DOCKER_USER: + from_secret: docker_username + GCP_KEY: + from_secret: gcp_key + image: google/cloud-sdk + name: publish-images-grafana-oss + volumes: + - name: docker + path: /var/run/docker.sock - commands: - ./scripts/circle-release-canary-packages.sh depends_on: @@ -805,18 +844,18 @@ steps: - end-to-end-tests-smoke-tests-suite - end-to-end-tests-various-suite environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket image: grafana/grafana-ci-deploy:1.3.1 name: upload-packages - commands: - - ./bin/grabpl upload-cdn --edition oss --bucket "grafana-static-assets" + - ./bin/grabpl upload-cdn --edition oss --src-bucket "grafana-static-assets" depends_on: - grafana-server environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -870,7 +909,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -938,7 +977,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -1021,7 +1060,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1034,7 +1073,6 @@ steps: image: grafana/build-container:1.4.9 name: initialize - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - ./bin/grabpl store-packages --edition oss --gcp-key /tmp/gcpkey.json --build-id ${DRONE_BUILD_NUMBER} depends_on: @@ -1094,7 +1132,7 @@ type: docker --- depends_on: [] kind: pipeline -name: oss-build-publish-e2e-release +name: oss-build-e2e-publish-release node: type: no-parallel platform: @@ -1104,7 +1142,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1202,8 +1240,6 @@ steps: image: grafana/build-container:1.4.9 name: copy-packages-for-docker - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey.json - ./bin/grabpl build-docker --edition oss --shouldSave depends_on: - copy-packages-for-docker @@ -1211,13 +1247,11 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk - name: package-docker-images + name: build-docker-images volumes: - name: docker path: /var/run/docker.sock - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey.json - ./bin/grabpl build-docker --edition oss --shouldSave --ubuntu depends_on: - copy-packages-for-docker @@ -1225,7 +1259,7 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk - name: package-docker-images-ubuntu + name: build-docker-images-ubuntu volumes: - name: docker path: /var/run/docker.sock @@ -1319,11 +1353,12 @@ steps: image: grafana/build-container:1.4.9 name: build-storybook - commands: - - ./bin/grabpl upload-cdn --edition oss --bucket "$${PRERELEASE_BUCKET}/artifacts/static-assets" + - ./bin/grabpl upload-cdn --edition oss --src-bucket "$${PRERELEASE_BUCKET}" --src-dir + artifacts/static-assets depends_on: - grafana-server environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -1337,7 +1372,7 @@ steps: - end-to-end-tests-smoke-tests-suite - end-to-end-tests-various-suite environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -1411,7 +1446,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1532,7 +1567,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1599,7 +1634,7 @@ volumes: medium: memory --- depends_on: -- oss-build-publish-e2e-release +- oss-build-e2e-publish-release - oss-test-release - oss-integration-tests-release kind: pipeline @@ -1616,7 +1651,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -1675,7 +1710,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1807,8 +1842,6 @@ steps: image: grafana/build-container:1.4.9 name: copy-packages-for-docker - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey.json - ./bin/grabpl build-docker --edition enterprise --shouldSave depends_on: - copy-packages-for-docker @@ -1816,13 +1849,11 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk - name: package-docker-images + name: build-docker-images volumes: - name: docker path: /var/run/docker.sock - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey.json - ./bin/grabpl build-docker --edition enterprise --shouldSave --ubuntu depends_on: - copy-packages-for-docker @@ -1830,7 +1861,7 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk - name: package-docker-images-ubuntu + name: build-docker-images-ubuntu volumes: - name: docker path: /var/run/docker.sock @@ -1916,11 +1947,12 @@ steps: - success - failure - commands: - - ./bin/grabpl upload-cdn --edition enterprise --bucket "$${PRERELEASE_BUCKET}/artifacts/static-assets" + - ./bin/grabpl upload-cdn --edition enterprise --src-bucket "$${PRERELEASE_BUCKET}" + --src-dir artifacts/static-assets depends_on: - package environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -1931,7 +1963,7 @@ steps: depends_on: - package environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -1959,11 +1991,12 @@ steps: image: grafana/build-container:1.4.9 name: package-enterprise2 - commands: - - ./bin/grabpl upload-cdn --edition enterprise2 --bucket "$${PRERELEASE_BUCKET}/artifacts/static-assets" + - ./bin/grabpl upload-cdn --edition enterprise2 --src-bucket "$${PRERELEASE_BUCKET}" + --src-dir artifacts/static-assets depends_on: - package-enterprise2 environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -1974,7 +2007,7 @@ steps: depends_on: - package-enterprise2 environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -2017,7 +2050,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2190,7 +2223,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2318,7 +2351,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2393,7 +2426,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2415,8 +2448,8 @@ steps: - name: docker path: /var/run/docker.sock - commands: - - ./bin/grabpl artifacts docker publish --version-tag ${TAG} --dockerhub-repo grafana - --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 + - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana --base alpine --base + ubuntu --arch amd64 --arch arm64 --arch armv7 --version-tag ${TAG} depends_on: - fetch-images-oss environment: @@ -2432,8 +2465,8 @@ steps: - name: docker path: /var/run/docker.sock - commands: - - ./bin/grabpl artifacts docker publish --version-tag ${TAG} --dockerhub-repo grafana-oss - --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 + - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana-oss --base alpine + --base ubuntu --arch amd64 --arch arm64 --arch armv7 --version-tag ${TAG} depends_on: - fetch-images-oss environment: @@ -2471,7 +2504,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2493,8 +2526,8 @@ steps: - name: docker path: /var/run/docker.sock - commands: - - ./bin/grabpl artifacts docker publish --version-tag ${TAG} --dockerhub-repo grafana-enterprise - --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 + - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana-enterprise --base + alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 --version-tag ${TAG} depends_on: - fetch-images-enterprise environment: @@ -2532,7 +2565,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2554,8 +2587,8 @@ steps: - name: docker path: /var/run/docker.sock - commands: - - ./bin/grabpl artifacts docker publish --security --version-tag ${TAG} --dockerhub-repo - grafana --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 + - ./bin/grabpl artifacts docker publish --security --dockerhub-repo grafana --base + alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 --version-tag ${TAG} depends_on: - fetch-images-oss environment: @@ -2571,8 +2604,9 @@ steps: - name: docker path: /var/run/docker.sock - commands: - - ./bin/grabpl artifacts docker publish --security --version-tag ${TAG} --dockerhub-repo - grafana-oss --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 + - ./bin/grabpl artifacts docker publish --security --dockerhub-repo grafana-oss + --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 --version-tag + ${TAG} depends_on: - fetch-images-oss environment: @@ -2610,7 +2644,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2632,9 +2666,9 @@ steps: - name: docker path: /var/run/docker.sock - commands: - - ./bin/grabpl artifacts docker publish --security --version-tag ${TAG} --dockerhub-repo - grafana-enterprise --base alpine --base ubuntu --arch amd64 --arch arm64 --arch - armv7 + - ./bin/grabpl artifacts docker publish --security --dockerhub-repo grafana-enterprise + --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7 --version-tag + ${TAG} depends_on: - fetch-images-enterprise environment: @@ -2672,7 +2706,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2708,7 +2742,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2755,7 +2789,7 @@ steps: name: initialize - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2803,12 +2837,11 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - ./bin/grabpl store-packages --edition oss --packages-bucket grafana-downloads --gcp-key /tmp/gcpkey.json ${DRONE_TAG} depends_on: @@ -2827,7 +2860,6 @@ steps: image: grafana/grafana-ci-deploy:1.3.1 name: store-packages-oss - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - ./bin/grabpl store-packages --edition enterprise --packages-bucket grafana-downloads --gcp-key /tmp/gcpkey.json ${DRONE_TAG} depends_on: @@ -2858,7 +2890,7 @@ volumes: --- depends_on: [] kind: pipeline -name: oss-build-publish-e2e-release-branch +name: oss-build-e2e-publish-release-branch node: type: no-parallel platform: @@ -2868,7 +2900,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2963,8 +2995,6 @@ steps: image: grafana/build-container:1.4.9 name: copy-packages-for-docker - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey.json - ./bin/grabpl build-docker --edition oss --shouldSave depends_on: - copy-packages-for-docker @@ -2972,13 +3002,11 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk - name: package-docker-images + name: build-docker-images volumes: - name: docker path: /var/run/docker.sock - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey.json - ./bin/grabpl build-docker --edition oss --shouldSave --ubuntu depends_on: - copy-packages-for-docker @@ -2986,7 +3014,7 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk - name: package-docker-images-ubuntu + name: build-docker-images-ubuntu volumes: - name: docker path: /var/run/docker.sock @@ -3080,11 +3108,11 @@ steps: image: grafana/build-container:1.4.9 name: build-storybook - commands: - - ./bin/grabpl upload-cdn --edition oss --bucket "grafana-static-assets" + - ./bin/grabpl upload-cdn --edition oss --src-bucket "grafana-static-assets" depends_on: - grafana-server environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -3098,7 +3126,7 @@ steps: - end-to-end-tests-smoke-tests-suite - end-to-end-tests-various-suite environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -3131,7 +3159,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3245,7 +3273,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3305,7 +3333,7 @@ volumes: medium: memory --- depends_on: -- oss-build-publish-e2e-release-branch +- oss-build-e2e-publish-release-branch - oss-test-release-branch - oss-integration-tests-release-branch kind: pipeline @@ -3322,7 +3350,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -3370,7 +3398,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3496,8 +3524,6 @@ steps: image: grafana/build-container:1.4.9 name: copy-packages-for-docker - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey.json - ./bin/grabpl build-docker --edition enterprise --shouldSave depends_on: - copy-packages-for-docker @@ -3505,13 +3531,11 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk - name: package-docker-images + name: build-docker-images volumes: - name: docker path: /var/run/docker.sock - commands: - - printenv GCP_KEY | base64 -d > /tmp/gcpkey.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey.json - ./bin/grabpl build-docker --edition enterprise --shouldSave --ubuntu depends_on: - copy-packages-for-docker @@ -3519,7 +3543,7 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk - name: package-docker-images-ubuntu + name: build-docker-images-ubuntu volumes: - name: docker path: /var/run/docker.sock @@ -3614,11 +3638,11 @@ steps: image: grafana/build-container:1.4.9 name: build-storybook - commands: - - ./bin/grabpl upload-cdn --edition enterprise --bucket "grafana-static-assets" + - ./bin/grabpl upload-cdn --edition enterprise --src-bucket "grafana-static-assets" depends_on: - package environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -3629,7 +3653,7 @@ steps: depends_on: - package environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -3657,11 +3681,11 @@ steps: image: grafana/build-container:1.4.9 name: package-enterprise2 - commands: - - ./bin/grabpl upload-cdn --edition enterprise2 --bucket "grafana-static-assets" + - ./bin/grabpl upload-cdn --edition enterprise2 --src-bucket "grafana-static-assets" depends_on: - package-enterprise2 environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -3672,7 +3696,7 @@ steps: depends_on: - package-enterprise2 environment: - GCP_GRAFANA_UPLOAD_KEY: + GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket @@ -3709,7 +3733,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3874,7 +3898,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3994,7 +4018,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.8.7/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -4191,6 +4215,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 4d1a5696bf1e510fb51a021c07e240c50cb913724ce08ed52cce037ff02dd8de +hmac: 713e8fdfc36e773a17e3fc79107bd1dd1647400f83f6d40922f566b1c7f97edb ... diff --git a/.github/workflows/detect-breaking-changes-build.yml b/.github/workflows/detect-breaking-changes-build.yml index c6ceb52d9dd..333061c3c1d 100644 --- a/.github/workflows/detect-breaking-changes-build.yml +++ b/.github/workflows/detect-breaking-changes-build.yml @@ -43,18 +43,18 @@ jobs: name: buildPr path: './pr/pr_built_packages.zip' - buildMain: - name: Build Main + buildBase: + name: Build Base runs-on: ubuntu-latest defaults: run: - working-directory: './main' + working-directory: './base' steps: - uses: actions/checkout@v2 with: - path: './main' - ref: 'main' + path: './base' + ref: ${{ github.event.pull_request.base.ref }} - name: Get yarn cache directory path id: yarn-cache-dir-path @@ -76,18 +76,18 @@ jobs: run: yarn packages:build - name: Zip built packages - run: zip -r ./main_built_packages.zip ./packages/**/dist + run: zip -r ./base_built_packages.zip ./packages/**/dist - name: Upload build output as artifact uses: actions/upload-artifact@v2 with: - name: buildMain - path: './main/main_built_packages.zip' + name: buildBase + path: './base/base_built_packages.zip' Detect: name: Detect breaking changes runs-on: ubuntu-latest - needs: ['buildPR', 'buildMain'] + needs: ['buildPR', 'buildBase'] env: GITHUB_STEP_NUMBER: 7 @@ -99,16 +99,16 @@ jobs: with: name: buildPr - - name: Get built packages from main + - name: Get built packages from base uses: actions/download-artifact@v2 with: - name: buildMain + name: buildBase - name: Unzip artifact from pr run: unzip pr_built_packages.zip -d ./pr && rm pr_built_packages.zip - - name: Unzip artifact from main - run: unzip main_built_packages.zip -d ./main && rm main_built_packages.zip + - name: Unzip artifact from base + run: unzip base_built_packages.zip -d ./base && rm base_built_packages.zip - name: Get link for the Github Action job id: job diff --git a/.yarnrc.yml b/.yarnrc.yml index 0a07e82236b..6cc7483e6ed 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -9,28 +9,28 @@ packageExtensions: "@mdx-js/loader@1.6.22": peerDependencies: react: 17.0.1 - "@storybook/addon-docs@6.4.13": + "@storybook/addon-docs@6.4.15": peerDependencies: - "@storybook/manager-webpack5": 6.4.13 - "@storybook/addon-essentials@6.4.13": + "@storybook/manager-webpack5": 6.4.15 + "@storybook/addon-essentials@6.4.15": peerDependencies: - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 - "@storybook/manager-webpack5": 6.4.13 - "@storybook/theming": 6.4.13 - "@storybook/core-server@6.4.13": + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 + "@storybook/manager-webpack5": 6.4.15 + "@storybook/theming": 6.4.15 + "@storybook/core-server@6.4.15": peerDependencies: "@babel/core": ^7.0.0 - "@storybook/core@6.4.13": + "@storybook/core@6.4.15": peerDependencies: "@babel/core": ^7.0.0 - "@storybook/manager-webpack5": 6.4.13 - "@storybook/csf-tools@6.4.13": + "@storybook/manager-webpack5": 6.4.15 + "@storybook/csf-tools@6.4.15": peerDependencies: "@babel/core": ^7.0.0 - "@storybook/react@6.4.13": + "@storybook/react@6.4.15": peerDependencies: - "@storybook/manager-webpack5": 6.4.13 + "@storybook/manager-webpack5": 6.4.15 doctrine@3.0.0: dependencies: assert: 2.0.0 @@ -60,10 +60,6 @@ packageExtensions: react-simple-compat: 1.2.1 peerDependencies: framework-utils: ^1.1.0 - react-dev-utils@11.0.4: - peerDependencies: - typescript: 4.4.3 - webpack: 5.51.1 react-docgen-typescript-loader@3.7.2: peerDependencies: webpack: 4.41.5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e10774dee..6d2878e5a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ + + +# 8.4.0-beta1 (2022-02-02) + +### Features and enhancements + +- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath) +- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman) +- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer) +- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark) +- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne) +- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi) +- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa) +- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv) +- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas) +- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90) +- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney) + + + + +# 8.3.5 (2022-02-08) + +- **Security**: Fixes CVE-2022-21702. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) +- **Security**: Fixes CVE-2022-21703. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) +- **Security**: Fixes CVE-2022-21713. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) + + # 8.3.4 (2022-01-17) @@ -1259,7 +1288,15 @@ Issue [#33352](https://github.com/grafana/grafana/issues/33352) - **AGPL License:** Update license from Apache 2.0 to the GNU Affero General Public License (AGPL). [#33184](https://github.com/grafana/grafana/pull/33184) + +# 7.5.15 (2022-02-08) + +- **Security**: Fixes CVE-2022-21702. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) +- **Security**: Fixes CVE-2022-21703. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) +- **Security**: Fixes CVE-2022-21713. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) + + # 7.5.13 (2022-01-18) diff --git a/Dockerfile b/Dockerfile index 8dd105f442a..618c0fa1e88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ COPY plugins-bundled plugins-bundled RUN yarn install -COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js babel.config.json ./ +COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js babel.config.json .linguirc ./ COPY public public COPY tools tools COPY scripts scripts @@ -30,6 +30,7 @@ COPY go.mod go.sum embed.go Makefile build.go package.json ./ COPY cue cue COPY packages/grafana-schema packages/grafana-schema COPY public/app/plugins public/app/plugins +COPY public/api-spec.json public/api-spec.json COPY pkg pkg COPY scripts scripts COPY cue.mod cue.mod diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 9323c935bdb..db3794d1bb1 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -12,7 +12,7 @@ COPY plugins-bundled plugins-bundled RUN yarn install -COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js babel.config.json ./ +COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js babel.config.json .linguirc ./ COPY public public COPY tools tools COPY scripts scripts @@ -33,6 +33,7 @@ COPY cue cue/ COPY cue.mod cue.mod/ COPY packages/grafana-schema packages/grafana-schema/ COPY public/app/plugins public/app/plugins/ +COPY public/api-spec.json public/api-spec.json RUN go mod verify RUN make build-go diff --git a/Makefile b/Makefile index 45544ca5e8a..1c74cc3569a 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,13 @@ WIRE_TAGS = "oss" -include local/Makefile include .bingo/Variables.mk -.PHONY: all deps-go deps-js deps build-go build-server build-cli build-js build build-docker-full lint-go golangci-lint test-go test-js gen-ts test run run-frontend clean devenv devenv-down protobuf drone help +.PHONY: all deps-go deps-js deps build-go build-server build-cli build-js build build-docker-full build-docker-full-ubuntu lint-go golangci-lint test-go test-js gen-ts test run run-frontend clean devenv devenv-down protobuf drone help GO = go GO_FILES ?= ./pkg/... SH_FILES ?= $(shell find ./scripts -name *.sh) +API_DEFINITION_FILES = $(shell find ./pkg/api/docs/definitions -name '*.go' -print) +SWAGGER_TAG ?= latest all: deps build @@ -28,13 +30,62 @@ node_modules: package.json yarn.lock ## Install node modules. @echo "install frontend dependencies" YARN_ENABLE_PROGRESS_BARS=false yarn install --immutable +##@ Swagger +SPEC_TARGET = public/api-spec.json +MERGED_SPEC_TARGET := public/api-merged.json +NGALERT_SPEC_TARGET = pkg/services/ngalert/api/tooling/post.json + +$(SPEC_TARGET): $(API_DEFINITION_FILES) ## Generate API spec + docker run --rm -it \ + -e GOPATH=${HOME}/go:/go \ + -e SWAGGER_GENERATE_EXTENSION=false \ + -v ${HOME}/go:/go \ + -v $$(pwd):/grafana \ + -w $$(pwd)/pkg/api/docs quay.io/goswagger/swagger:$(SWAGGER_TAG) \ + generate spec -m -o /grafana/public/api-spec.json \ + -w /grafana/pkg/server \ + -x "grafana/grafana/pkg/services/ngalert/api/tooling/definitions" \ + -x "github.com/prometheus/alertmanager" \ + -i /grafana/pkg/api/docs/tags.json + +swagger-api-spec: gen-go $(SPEC_TARGET) $(MERGED_SPEC_TARGET) + +$(NGALERT_SPEC_TARGET): + +$(MAKE) -C pkg/services/ngalert/api/tooling post.json + +$(MERGED_SPEC_TARGET): $(SPEC_TARGET) $(NGALERT_SPEC_TARGET) ## Merge generated and ngalert API specs + go run pkg/api/docs/merge/merge_specs.go -o=public/api-merged.json $(<) pkg/services/ngalert/api/tooling/post.json + +ensure_go-swagger_mac: + @hash swagger &>/dev/null || (brew tap go-swagger/go-swagger && brew install go-swagger) + +--swagger-api-spec-mac: ensure_go-swagger_mac $(API_DEFINITION_FILES) ## Generate API spec (for M1 Mac) + swagger generate spec -m -w pkg/server -o public/api-spec.json \ + -x "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" \ + -x "github.com/prometheus/alertmanager" \ + -i pkg/api/docs/tags.json + +swagger-api-spec-mac: gen-go --swagger-api-spec-mac $(MERGED_SPEC_TARGET) + +validate-api-spec: $(MERGED_SPEC_TARGET) ## Validate API spec + docker run --rm -it \ + -e GOPATH=${HOME}/go:/go \ + -e SWAGGER_GENERATE_EXTENSION=false \ + -v ${HOME}/go:/go \ + -v $$(pwd):/grafana \ + -w $$(pwd)/pkg/api/docs quay.io/goswagger/swagger:$(SWAGGER_TAG) \ + validate /grafana/$(<) + +clean-api-spec: + rm $(SPEC_TARGET) $(MERGED_SPEC_TARGET) + ##@ Building gen-go: $(WIRE) @echo "generate go files" $(WIRE) gen -tags $(WIRE_TAGS) ./pkg/server ./pkg/cmd/grafana-cli/runner -build-go: gen-go ## Build all Go binaries. +build-go: $(MERGED_SPEC_TARGET) gen-go ## Build all Go binaries. @echo "build go files" $(GO) run build.go build @@ -99,6 +150,11 @@ build-docker-full: ## Build Docker image for development. @echo "build docker container" docker build --tag grafana/grafana:dev . +build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development. + @echo "build docker container" + docker build --tag grafana/grafana:dev-ubuntu -f ./Dockerfile.ubuntu . + + ##@ Services # create docker-compose file with provided sources and start them diff --git a/conf/defaults.ini b/conf/defaults.ini index 03aad2655a1..a852dee4e75 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -122,6 +122,9 @@ path = grafana.db # For "sqlite3" only. cache mode setting used for connecting to the database cache_mode = private +# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0. +locking_attempt_timeout_sec = 0 + #################################### Cache server ############################# [remote_cache] # Either "redis", "memcached" or "database" default is "database" @@ -403,6 +406,9 @@ api_key_max_seconds_to_live = -1 # Set to true to enable SigV4 authentication option for HTTP-based datasources sigv4_auth_enabled = false +# Set to true to enable verbose logging of SigV4 request signing +sigv4_verbose_logging = false + #################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access diff --git a/conf/sample.ini b/conf/sample.ini index 2a7355fea4d..710a8ea4810 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -123,6 +123,9 @@ # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared) ;cache_mode = private +# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0. +;locking_attempt_timeout_sec = 0 + ################################### Data sources ######################### [datasources] # Upper limit of data sources that Grafana will return. This limit is a temporary configuration and it will be deprecated when pagination will be introduced on the list data sources API. @@ -397,6 +400,9 @@ # Set to true to enable SigV4 authentication option for HTTP-based datasources. ;sigv4_auth_enabled = false +# Set to true to enable verbose logging of SigV4 request signing +;sigv4_verbose_logging = false + #################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index 088d8fdc9d3..97d454d71ad 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -314,6 +314,10 @@ The maximum number of open connections to the database. Sets the maximum amount of time a connection may be reused. The default is 14400 (which means 14400 seconds or 4 hours). For MySQL, this setting should be shorter than the [`wait_timeout`](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_wait_timeout) variable. +### locking_attempt_timeout_sec + +For "mysql", if `lockingMigration` feature toggle is set, specify the time (in seconds) to wait before failing to lock the database for the migrations. Default is 0. + ### log_queries Set to `true` to log the sql calls and execution times. @@ -757,6 +761,12 @@ Limit of API key seconds to live before expiration. Default is -1 (unlimited). Set to `true` to enable the AWS Signature Version 4 Authentication option for HTTP-based datasources. Default is `false`. +### sigv4_verbose_logging + +> Only available in Grafana 8.4+. + +Set to `true` to enable verbose request signature logging when AWS Signature Version 4 Authentication is enabled. Default is `false`. +
## [auth.anonymous] diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index ac3a4398e98..d94d09cda63 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -15,10 +15,11 @@ Grafana 8.0 introduced new and improved alerting that centralizes alerting infor Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "./unified-alerting/opt-in.md" >}}) feature. -| Release | Cloud | Enterprise | OSS | -| ----------- | ------------- | ---------- | -------------------------------- | -| Grafana 8.2 | On by default | Opt-in | Opt-in | -| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* | +| Release | Cloud | Enterprise | OSS | +| ------------------------ | ------------- | ------------- | -------------------------------- | +| Grafana 8.2 | On by default | Opt-in | Opt-in | +| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* | +| Grafana 9.0 (unreleased) | On by default | On by default | On by default | > **Note:** New installs include existing installs which do not have any alerts configured. diff --git a/docs/sources/alerting/old-alerting/_index.md b/docs/sources/alerting/old-alerting/_index.md index 32695e28573..8d0cd673290 100644 --- a/docs/sources/alerting/old-alerting/_index.md +++ b/docs/sources/alerting/old-alerting/_index.md @@ -7,7 +7,7 @@ weight = 114 Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "../unified-alerting/opt-in.md" >}}) feature. -> **Note**: Legacy dashboard alerts is deprecated and will be removed in a future release. We encourage you to migrate to [Grafana alerting]({{< relref "../unified-alerting/_index.md" >}}) for all existing installations. +> **Note**: Legacy dashboard alerts are deprecated and will be removed in Grafana 9. We encourage you to migrate to [Grafana alerting]({{< relref "../unified-alerting/_index.md" >}}) for all existing installations. Legacy dashboard alerts have two main components: diff --git a/docs/sources/datasources/loki.md b/docs/sources/datasources/loki.md index ada73be0f69..47e5fbdbba6 100644 --- a/docs/sources/datasources/loki.md +++ b/docs/sources/datasources/loki.md @@ -43,7 +43,7 @@ Each derived field consists of: - **Name -** Shown in the log details as a label. - **Regex -** A Regex pattern that runs on the log message and captures part of it as the value of the new field. Can only contain a single capture group. -- **URL/query -** If the link is external, then enter the full link URL. If the link is internal link, then this input serves as query for the target data source. In both cases, you can interpolate the value from the field with `${__value.raw }` macro. +- **URL/query -** If the link is external, then enter the full link URL. If the link is internal link, then this input serves as query for the target data source. In both cases, you can interpolate the value from the field with `${__value.raw}` macro. - **URL Label -** (Optional) Set a custom display label for the link. The link label defaults to the full external URL or name of the linked internal data source and is overridden by this setting. - **Internal link -** Select if the link is internal or external. In case of internal link, a data source selector allows you to select the target data source. Only tracing data sources are supported. diff --git a/docs/sources/developers/angular_deprecation.md b/docs/sources/developers/angular_deprecation.md new file mode 100644 index 00000000000..5307fa180e8 --- /dev/null +++ b/docs/sources/developers/angular_deprecation.md @@ -0,0 +1,32 @@ ++++ +title = "Angular support deprecation" +keywords = ["grafana", "documentation", "developers", "resources"] ++++ + +# Angular support deprecation + +Angular plugin support is deprecated, and it will be removed in a future release. There are still many community plugins that depend on Grafana’s angular plugin support for them to work. The same is true for many internal (private) plugins developed over the years by Grafana users. Grafana version 9 will have a server configuration option, global for the whole instance, that will control if angular plugin support is available or not. By default, angular plugin support will be disabled. + +## Why are we deprecating angular support? + +AngularJS is an old frontend framework that stopped active development many years ago. As a result, it is a security risk. AngularJS also requires unsafe-eval in the CSP (Content Security Policy) settings which also reduces the security level of how javascript is executed in the browser. + +## When will angular plugins stop working? + +In Grafana version 9 coming in June 2022, all angular plugins will stop working unless a new server configuration option is turned on. If you still depend on community or internally developed plugins that require AngularJS then you will have to turn this option on. + +This is a good time to start working on migrating plugins to React. + +Our plan is to fully remove angular plugin support in version 10 released in 2023. Meaning all plugins that do depend on angular will stop working and this temporary option to enable it introduced in v9 will be removed. + +## How do I migrate an angular plugin to React? + +Depending on if it’s a data source plugin, panel plugin, or app plugin the process will differ. + +For panels, the rendering logic could in some cases be easily preserved but all options need to be redone to use the declarative options framework. For data source plugins the query editor and config options will likely need a total rewrite. + +### Links + +- [Migrate Angular to React](https://grafana.com/docs/grafana/latest/developers/plugins/migration-guide/#migrate-a-plugin-from-angular-to-react) +- [Build a panel plugin](https://grafana.com/tutorials/build-a-panel-plugin/) +- [Build a data source plugin](https://grafana.com/tutorials/build-a-data-source-plugin/) diff --git a/docs/sources/developers/plugins/add-support-for-variables.md b/docs/sources/developers/plugins/add-support-for-variables.md index ee2916dea19..c8a964f2a69 100644 --- a/docs/sources/developers/plugins/add-support-for-variables.md +++ b/docs/sources/developers/plugins/add-support-for-variables.md @@ -80,26 +80,19 @@ For more information on the available variable formats, refer to [Advanced varia ## Set a variable from your plugin -Not only can you read the value of a variable, you can also update the variable from your plugin. Use LocationSrv.update() +Not only can you read the value of a variable, you can also update the variable from your plugin. Use `locationService.partial(query, replace)`. The following example shows how to update a variable called `service`. - `query` contains the query parameters you want to update. Query parameters controlling variables are prefixed with `var-`. -- `partial: true` makes the update only affect the query parameters listed in `query`, and leaves the other query parameters unchanged. - `replace: true` tells Grafana to update the current URL state, rather than creating a new history entry. ```ts -import { getLocationSrv } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; ``` ```ts -getLocationSrv().update({ - query: { - 'var-service': 'billing', - }, - partial: true, - replace: true, -}); +locationService.partial({ 'var-service': 'billing' }, true); ``` > **Note:** Grafana queries your data source whenever you update a variable. Excessive updates to variables can slow down Grafana and lead to a poor user experience. diff --git a/docs/sources/developers/plugins/migration-guide.md b/docs/sources/developers/plugins/migration-guide.md index a0f87d22cf9..7f6056af876 100644 --- a/docs/sources/developers/plugins/migration-guide.md +++ b/docs/sources/developers/plugins/migration-guide.md @@ -15,6 +15,8 @@ This guide helps you identify the steps you need to take based on the Grafana ve - [From version 8.3.x to 8.4.x](#from-version-83x-to-84x) - [Value Mapping Editor has been removed from @grafana-ui library](#value-mapping-editor-has-been-removed-from-grafana-ui-library) - [Thresholds Editor has been removed from @grafana-ui library](#thresholds-editor-has-been-removed-from-grafana-ui-library) + - [8.4 Deprecations](#84-deprecations) + - [LocationService replaces getLocationSrv](#locationservice-replaces-getlocationsrv) - [From version 7.x.x to 8.x.x](#from-version-7xx-to-8xx) - [Backend plugin v1 support has been dropped](#backend-plugin-v1-support-has-been-dropped) - [1. Add dependency on grafana-plugin-sdk-go](#1-add-dependency-on-grafana-plugin-sdk-go) @@ -48,6 +50,81 @@ Removed due to being an internal component. Removed due to being an internal component. +### 8.4 deprecations + +#### LocationService replaces getLocationSrv + +In a previous release, we migrated to use a new routing system and introduced a new service for managing locations, navigation, and related information. In this release, we are making that new service the primary service. + +**Example:** Import the service. + +```ts +// before +import { getLocationSrv } from '@grafana/runtime'; + +// after +import { locationService } from '@grafana/runtime'; +``` + +**Example:** Navigate to a path and add a new record in the navigation history so that you can navigate back to the previous one. + +```ts +// before +getLocationSrv.update({ + path: '/route-to-navigate-to', + replace: false, +}); + +// after +locationService.push('/route-to-navigate-to'); +``` + +**Example:** Navigate to a path and replace the current record in the navigation history. + +```ts +// before +getLocationSrv.update({ + path: '/route-to-navigate-to', + replace: true, +}); + +// after +locationService.replace('/route-to-navigate-to'); +``` + +**Example:** Update the search or query parameter for the current route and add a new record in the navigation history so that you can navigate back to the previous one. + +```ts +// How to navigate to a new path +// before +getLocationSrv.update({ + query: { + value: 1, + }, + partial: true, + replace: false, +}); + +// after +locationService.partial({ value: 1 }); +``` + +**Example:** Update the search or query parameter for the current route and add replacing it in the navigation history. + +```ts +// before +getLocationSrv.update({ + query: { + 'var-variable': 1, + }, + partial: true, + replace: true, +}); + +// after +locationService.partial({ 'var-variable': 1 }, true); +``` + ## From version 7.x.x to 8.x.x This section explains how to migrate Grafana v7.x.x plugins to the updated plugin system available in Grafana v8.x.x. Depending on your plugin, you need to perform one or more of the following steps. We have documented the breaking changes in Grafana v8.x.x and the steps you need to take to upgrade your plugin. diff --git a/docs/sources/enterprise/access-control/fine-grained-access-control-references.md b/docs/sources/enterprise/access-control/fine-grained-access-control-references.md index 36e1e83342b..f95a6b90bba 100644 --- a/docs/sources/enterprise/access-control/fine-grained-access-control-references.md +++ b/docs/sources/enterprise/access-control/fine-grained-access-control-references.md @@ -38,13 +38,14 @@ The reference information that follows complements conceptual information about | `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | | `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | | `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | -| | +| `fixed:teams:creator` ` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | +| `fixed:teams:writer` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | ## Default built-in role assignments | Built-in role | Associated role | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer` | Default [Grafana server administrator]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) assignments. | -| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
| Default [Grafana organization administrator]({{< relref "../../permissions/organization_roles.md" >}}) assignments. | -| Editor | `fixed:datasources:explorer` | Default [Editor]({{< relref "../../permissions/organization_roles.md" >}}) assignments. | +| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
| Default [Grafana organization administrator]({{< relref "../../permissions/organization_roles.md" >}}) assignments. | +| Editor | `fixed:datasources:explorer` and
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled | Default [Editor]({{< relref "../../permissions/organization_roles.md" >}}) assignments. | | Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader` | Default [Viewer]({{< relref "../../permissions/organization_roles.md" >}}) assignments. | diff --git a/docs/sources/enterprise/access-control/permissions.md b/docs/sources/enterprise/access-control/permissions.md index 6bca31e0889..6c636fbdc0c 100644 --- a/docs/sources/enterprise/access-control/permissions.md +++ b/docs/sources/enterprise/access-control/permissions.md @@ -91,6 +91,12 @@ The following list contains fine-grained access control actions. | `licensing:update` | n/a | Update the license token. | | `licensing:delete` | n/a | Delete the license token. | | `licensing.reports:read` | n/a | Get custom permission reports. | +| `teams:create` | n/a | Create teams. | +| `teams:read` | `teams:*`
`teams:id:*` | Read one or more teams and team preferences. | +| `teams:write` | `teams:*`
`teams:id:*` | Update one or more teams and team preferences. | +| `teams:delete` | `teams:*`
`teams:id:*` | Delete one or more teams. | +| `teams.permissions:read` | `teams:*`
`teams:id:*` | Read members and External Group Synchronization setup for teams. | +| `teams.permissions:write` | `teams:*`
`teams:id:*` | Add, remove and update members and manage External Group Synchronization setup for teams. | ## Scope definitions diff --git a/docs/sources/enterprise/enterprise-configuration.md b/docs/sources/enterprise/enterprise-configuration.md index ebee7b16687..1639e9e2abf 100644 --- a/docs/sources/enterprise/enterprise-configuration.md +++ b/docs/sources/enterprise/enterprise-configuration.md @@ -501,9 +501,3 @@ The org id of the datasource where the query data will be written. If all `default_remote_write_*` properties are set, this information will be populated at startup. If a remote write target has already been configured, nothing will happen. - -## [feature_highlights] - -### enabled - -Whether the feature highlights feature is enabled diff --git a/docs/sources/explore/logs-integration.md b/docs/sources/explore/logs-integration.md index e62035b2698..50f394afe38 100644 --- a/docs/sources/explore/logs-integration.md +++ b/docs/sources/explore/logs-integration.md @@ -23,7 +23,7 @@ During an infrastructure monitoring and incident response, you can dig deeper in Results of log queries are shown as histograms in the graph and individual logs are explained in the following sections. -If the data source supports a full range log volume histogram, the graph with log distribution for all entered log queries is shown automatically. This feature is currently supported by Elasticsearch data source. +If the data source supports a full range log volume histogram, the graph with log distribution for all entered log queries is shown automatically. This feature is currently supported by Elasticsearch and Loki data sources. If the data source does not support loading full range log volume histogram, the logs model computes a time series based on the log row counts bucketed by an automatically calculated time interval, and the first log row's timestamp then anchors the start of the histogram from the result. The end of the time series is anchored to the time picker's **To** range. diff --git a/docs/sources/http_api/access_control.md b/docs/sources/http_api/access_control.md index f9b5f9a0dda..9b61737d7e1 100644 --- a/docs/sources/http_api/access_control.md +++ b/docs/sources/http_api/access_control.md @@ -433,9 +433,10 @@ Accept: application/json #### Query parameters -| Param | Type | Required | Description | -| ----- | ------- | -------- | ----------------------------------------------------------------------- | -| force | boolean | No | When set to `true`, the role will be deleted with all it's assignments. | +| Param | Type | Required | Description | +| ------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| force | boolean | No | When set to `true`, the role will be deleted with all it's assignments. | +| global | boolean | No | A flag indicating if the role is global or not. If set to false, the default org ID of the authenticated user will be used from the request. Refer to the [Role scopes]({{< relref "../enterprise/access-control/roles.md#built-in-role-assignments" >}}) for more information. | #### Example response diff --git a/docs/sources/http_api/external_group_sync.md b/docs/sources/http_api/external_group_sync.md index 4930de025f5..095739bd7e1 100644 --- a/docs/sources/http_api/external_group_sync.md +++ b/docs/sources/http_api/external_group_sync.md @@ -9,10 +9,21 @@ aliases = ["/docs/grafana/latest/http_api/external_group_sync/"] > External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../enterprise" >}}). +> If you have [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, access to endpoints will be controlled by Fine-grained access control permissions. +> Refer to specific endpoints to understand what permissions are required. + ## Get External Groups `GET /api/teams/:teamId/groups` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ---------------------- | -------- | +| teams.permissions:read | teams:\* | + **Example Request**: ```http @@ -47,6 +58,14 @@ Status Codes: `POST /api/teams/:teamId/groups` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ----------------------- | -------- | +| teams.permissions:write | teams:\* | + **Example Request**: ```http @@ -81,6 +100,14 @@ Status Codes: `DELETE /api/teams/:teamId/groups/:groupId` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ----------------------- | -------- | +| teams.permissions:write | teams:\* | + **Example Request**: ```http diff --git a/docs/sources/http_api/library_element.md b/docs/sources/http_api/library_element.md index 1d0cb086297..3b500f3837a 100644 --- a/docs/sources/http_api/library_element.md +++ b/docs/sources/http_api/library_element.md @@ -269,8 +269,8 @@ Creates a new library element. JSON Body schema: -- **folderId** – ID of the folder where the library element is stored. -- **name** – Name of the library element. +- **folderId** – Optional, the ID of the folder where the library element is stored. +- **name** – Optional, the name of the library element. - **model** – The JSON model for the library element. - **kind** – Kind of element to create, Use `1` for library panels or `2` for library variables. - **uid** – Optional, the [unique identifier](/http_api/library_element/#identifier-id-vs-unique-identifier-uid). diff --git a/docs/sources/http_api/team.md b/docs/sources/http_api/team.md index d9d0135fdc6..0a49dd99528 100644 --- a/docs/sources/http_api/team.md +++ b/docs/sources/http_api/team.md @@ -7,7 +7,17 @@ aliases = ["/docs/grafana/latest/http_api/team/"] # Team API -This API can be used to create/update/delete Teams and to add/remove users to Teams. All actions require that the user has the Admin role for the organization. +This API can be used to manage Teams and Team Memberships. + +Access to these API endpoints is restricted as follows: + +- All authenticated users are able to view details of teams they are a member of. +- Organization Admins are able to manage all teams and team members. +- If you enable `editors_can_admin` configuration flag, then Organization Editors can create teams and manage teams where they are Admin. + - If you enable `editors_can_admin` configuration flag, Editors can find out whether a team that they are not members of exists by trying to create a team with the same name. + +> If you are running Grafana Enterprise and have [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, access to endpoints will be controlled by Fine-grained access control permissions. +> Refer to specific endpoints to understand what permissions are required. ## Team Search With Paging @@ -17,25 +27,23 @@ or `GET /api/teams/search?name=myteam` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ---------- | -------- | +| teams:read | teams:\* | + +**Example Request**: + ```http -GET /api/teams/search?perpage=10&page=1&query=myteam HTTP/1.1 +GET /api/teams/search?perpage=10&page=1&query=mytestteam HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Basic YWRtaW46YWRtaW4= ``` -### Using the query parameter - -Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. - -The `totalCount` field in the response can be used for pagination of the teams list E.g. if `totalCount` is equal to 100 teams and the `perpage` parameter is set to 10 then there are 10 pages of teams. - -The `query` parameter is optional and it will return results where the query value is contained in the `name` field. Query values with spaces need to be URL encoded e.g. `query=my%20team`. - -### Using the name parameter - -The `name` parameter returns a single team if the parameter matches the `name` field. - **Example Response**: ```http @@ -59,7 +67,19 @@ Content-Type: application/json } ``` -Status Codes: +### Using the query parameter + +Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. + +The `totalCount` field in the response can be used for pagination of the teams list E.g. if `totalCount` is equal to 100 teams and the `perpage` parameter is set to 10 then there are 10 pages of teams. + +The `query` parameter is optional and it will return results where the query value is contained in the `name` field. Query values with spaces need to be URL encoded e.g. `query=my%20team`. + +### Using the name parameter + +The `name` parameter returns a single team if the parameter matches the `name` field. + +#### Status Codes: - **200** - Ok - **401** - Unauthorized @@ -70,6 +90,14 @@ Status Codes: `GET /api/teams/:id` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ---------- | -------- | +| teams:read | teams:\* | + **Example Request**: ```http @@ -108,6 +136,14 @@ The Team `name` needs to be unique. `name` is required and `email`,`orgId` is op `POST /api/teams` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ------------ | ----- | +| teams:create | N/A | + **Example Request**: ```http @@ -145,6 +181,14 @@ There are two fields that can be updated for a team: `name` and `email`. `PUT /api/teams/:id` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ----------- | -------- | +| teams:write | teams:\* | + **Example Request**: ```http @@ -180,6 +224,14 @@ Status Codes: `DELETE /api/teams/:id` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ------------ | -------- | +| teams:delete | teams:\* | + **Example Request**: ```http @@ -209,6 +261,14 @@ Status Codes: `GET /api/teams/:teamId/members` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ---------------------- | -------- | +| teams.permissions:read | teams:\* | + **Example Request**: ```http @@ -254,6 +314,14 @@ Status Codes: `POST /api/teams/:teamId/members` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ----------------------- | -------- | +| teams.permissions:write | teams:\* | + **Example Request**: ```http @@ -288,6 +356,14 @@ Status Codes: `DELETE /api/teams/:teamId/members/:userId` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ----------------------- | -------- | +| teams.permissions:write | teams:\* | + **Example Request**: ```http @@ -317,6 +393,14 @@ Status Codes: `GET /api/teams/:teamId/preferences` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ---------- | -------- | +| teams:read | teams:\* | + **Example Request**: ```http @@ -343,6 +427,14 @@ Content-Type: application/json `PUT /api/teams/:teamId/preferences` +#### Required permissions + +See note in the [introduction]({{< ref "#team-api" >}}) for an explanation. + +| Action | Scope | +| ----------- | -------- | +| teams:write | teams:\* | + **Example Request**: ```http diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 2ae655180d9..18b76d97a44 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -8,6 +8,8 @@ weight = 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) +- [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}}) - [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}}) - [Release notes for 8.3.3]({{< relref "release-notes-8-3-3" >}}) - [Release notes for 8.3.2]({{< relref "release-notes-8-3-2" >}}) @@ -48,6 +50,7 @@ about deprecations, breaking changes as well as changes that relate to plugin de - [Release notes for 8.0.0-beta3]({{< relref "release-notes-8-0-0-beta3" >}}) - [Release notes for 8.0.0-beta2]({{< relref "release-notes-8-0-0-beta2" >}}) - [Release notes for 8.0.0-beta1]({{< relref "release-notes-8-0-0-beta1" >}}) +- [Release notes for 7.5.15]({{< relref "release-notes-7-5-15" >}}) - [Release notes for 7.5.13]({{< relref "release-notes-7-5-13" >}}) - [Release notes for 7.5.12]({{< relref "release-notes-7-5-12" >}}) - [Release notes for 7.5.11]({{< relref "release-notes-7-5-11" >}}) diff --git a/docs/sources/release-notes/release-notes-7-5-15.md b/docs/sources/release-notes/release-notes-7-5-15.md new file mode 100644 index 00000000000..7a64378d955 --- /dev/null +++ b/docs/sources/release-notes/release-notes-7-5-15.md @@ -0,0 +1,11 @@ ++++ +title = "Release notes for Grafana 7.5.15" +[_build] +list = false ++++ + +# Release notes for Grafana 7.5.15 + +- **Security**: Fixes CVE-2022-21702. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) +- **Security**: Fixes CVE-2022-21703. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) +- **Security**: Fixes CVE-2022-21713. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) diff --git a/docs/sources/release-notes/release-notes-8-3-5.md b/docs/sources/release-notes/release-notes-8-3-5.md new file mode 100644 index 00000000000..9d7cf1a0f86 --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-3-5.md @@ -0,0 +1,11 @@ ++++ +title = "Release notes for Grafana 8.3.5" +[_build] +list = false ++++ + +# Release notes for Grafana 8.3.5 + +- **Security**: Fixes CVE-2022-21702. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) +- **Security**: Fixes CVE-2022-21703. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) +- **Security**: Fixes CVE-2022-21713. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/) diff --git a/docs/sources/release-notes/release-notes-8-4-0-beta1.md b/docs/sources/release-notes/release-notes-8-4-0-beta1.md new file mode 100644 index 00000000000..1d9aaf557bb --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-4-0-beta1.md @@ -0,0 +1,23 @@ ++++ +title = "Release notes for Grafana 8.4.0-beta1" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.4.0-beta1 + +### Features and enhancements + +- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath) +- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman) +- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer) +- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark) +- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne) +- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi) +- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa) +- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv) +- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas) +- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90) +- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney) diff --git a/docs/sources/visualizations/geomap.md b/docs/sources/visualizations/geomap.md index 4124d468d8a..26f4cee798f 100644 --- a/docs/sources/visualizations/geomap.md +++ b/docs/sources/visualizations/geomap.md @@ -70,7 +70,7 @@ The markers layer allows you to display data points as different marker shapes s ![Markers Layer Options](/static/img/docs/geomap-panel/geomap-markers-options-8-1-0.png) -- **Marker Color** configures the color of the marker. The default `Fixed size` keeps all points a single color. There is an alternate option to have multiple colors depending on the data point values and the threshold set at the `Thresholds` section. +- **Marker Color** configures the color of the marker. The default `Single color` keeps all points a single color. There is an alternate option to have multiple colors depending on the data point values and the threshold set at the `Thresholds` section. - **Marker Size** configures the size of the marker. Default is `Fixed size`, making all marker size the same regardless of the data points. However, there is also an option to scale the circles to the corresponding data points. `Min` and `Max` marker size has to be set such that the Marker layer can scale within this range. - **Marker Shape** allows you to choose the shape, icon, or graphic to aid in providing additional visual context to your data. Choose from assets that are included with Grafana such as simple shapes or the Unicon library. You can also specify a URL containing an image asset. The image must be a scalable vector graphic (SVG). - **Fill opacity** configures the transparency of each marker. diff --git a/docs/sources/visualizations/time-series/_index.md b/docs/sources/visualizations/time-series/_index.md index cf384933397..e0cec74145f 100644 --- a/docs/sources/visualizations/time-series/_index.md +++ b/docs/sources/visualizations/time-series/_index.md @@ -35,6 +35,15 @@ Use these options to choose how to display your time series data. - [Graph stacked time series]({{< relref "./graph-time-series-stacking.md" >}}) - [Graph and color schemes]({{< relref "./graph-color-scheme.md" >}}) +### Transform + +Use this option to transform the series values without affecting the values shown in the tooltip, context menu, and legend. + +- **Negative Y transform -** Flip the results to negative values on the Y axis. +- **Constant -** Show first value as a constant line. + +> **Note:** Transform option is only available as an override. + ## Axis For more information about adjusting your time series axes, refer to [Change axis display]({{< relref "change-axis-display.md" >}}). diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index de3a4f91f66..aa62630e764 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -11,6 +11,7 @@ as info on deprecations, breaking changes and plugin development read the [relea ## Grafana 8 +- [What's new in 8.4]({{< relref "whats-new-in-v8-4" >}}) - [What's new in 8.3]({{< relref "whats-new-in-v8-3" >}}) - [What's new in 8.2]({{< relref "whats-new-in-v8-2" >}}) - [What's new in 8.1]({{< relref "whats-new-in-v8-1" >}}) diff --git a/docs/sources/whatsnew/whats-new-in-v8-4.md b/docs/sources/whatsnew/whats-new-in-v8-4.md new file mode 100644 index 00000000000..4a86ffcd536 --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v8-4.md @@ -0,0 +1,144 @@ ++++ +title = "What's new in Grafana v8.4" +description = "Feature and improvement highlights for Grafana v8.4" +keywords = ["grafana", "new", "documentation", "8.4", "release notes"] +weight = -33 +aliases = ["/docs/grafana/latest/guides/whats-new-in-v8-4/"] +[_build] +list = false ++++ + +# What’s new in Grafana v8.4 + +We’re excited to announce Grafana v8.4, with a variety of improvements that focus on Grafana’s usability, performance, and security. Read on to learn about Alerting enhancements like a WeCom contact point, improved Alert panel and custom mute timings, as well as visualization improvements and details to help you share playlists more easily. In Grafana Enterprise, we’ve made caching more powerful to save you time and money while loading dashboards, boosted database encryption to keep secrets safe in your Grafana database, and made usability improvements to Recorded Queries, which allow you to track any data point over time. + +We’ve summarized what’s new in the release here, but you might also be interested in the announcement blog post. If you’d like all the details you can check out the complete [changelog](https://github.com/grafana/grafana/blob/master/CHANGELOG.md). + +# OSS + +## Ease of use + +### Share playlists + +You can now share links to your playlists the same way as with dashboards, to easily set up the same playlist on multiple devices or kiosks. + +{{< figure src="/static/img/docs/dashboards/share-playlists-8-4.png" max-width="1200px" caption="Share playlist" >}} + +### $**interval and $**interval_ms in panel title + +We’ve expanded the functionality of this existing and popular feature. You can now use $**interval and $**interval_ms in panel titles. This new function displays the interval that’s used in a specific panel without requiring edit mode. + +{{< figure src="/static/img/docs/panels/interval-8-4.png" max-width="1200px" caption="Time intervals" >}} + +### Accessibility improvements + +We’re continuing to improve accessibility across Grafana, particularly focusing on keyboard navigation and screen readers. + +- Navigation: We’ve improved our keyboard navigation support in our main navbar, added focus states, and removed keyboard traps. +- General components (tooltips, color pickers, modals, dropdowns, and so forth): we’ve ensured they’re keyboard navigable, improved focus trapping, and improved screen reader support. +- Time series panel: charts are one of our main areas of limited accessibility. As of 8.4, you can also move a panel and make range selections using your keyboard. - Press arrow keys to move the cursor around. - Hold Shift to increase cursor speed. - Hold space to start rage selection. + You can read our accessibility statement [here](https://grafana.com/accessibility/) and reach out to us with accessibility issues using our community Slack or our community forums. + +### New option to configure OpenTelemetry + +Grafana is used to visualize traces and metrics, but Grafana itself can be traced as well. For example, users running a Grafana instance can export all the traces of endpoints and database requests to Jaeger, which helps you view all Grafana traffic. + +We’re currently using OpenTracing for this, but since the repository is being deprecated, we’ve started our work to migrate to OpenTelemetry and remove OpenTracing. This release is the first step toward this goal. It also adds the option to configure OpenTelemetry instead of OpenTracing. + +### Rotate your encryption key + +In Grafana 8.3, we upgraded Grafana to use envelope encryption, which adds a layer of indirection to the encryption process. Instead of encrypting all secrets in the database with a single key, Grafana uses a set of keys, called data encryption keys (DEKs), to encrypt them. These data encryption keys are themselves encrypted with a single key-encryption key (KEK). + +As of 8.4, you can rotate your KEK and quickly re-encrypt your DEKs in case a key is compromised. + +Envelope encryption is not enabled by default in version 8.4. You can enable it by adding the term `envelopeEncryption` to the list of feature toggles in your [Grafana configuration](https://grafana.com/docs/grafana/next/administration/configuration/#feature_toggles?mdm=email), or by sending a request to support if you use Grafana Cloud. + +{{< figure src="/static/img/docs/encryption/rotate-encryption-8-4.png" max-width="1200px" caption="Rotate encryption" >}} + +## Alerting + +### Support for mute timings + +[Mute timings](https://grafana.com/docs/grafana/next/alerting/unified-alerting/notifications/mute-timings/) have been a popular request from the community. They are a powerful addition to the new alerting feature set and allow you to suppress specific alerts on a recurring interval or schedule, [contrary to](https://grafana.com/docs/grafana/next/alerting/unified-alerting/notifications/mute-timings/#mute-timings-vs-silences) Silences. + +Paired with the existing Silences, this gives you even more control over when alerts should be sent and contact points notified. + +### Custom grouping for the Alert Panel + +The new Alert Panel displays your alerts and associated alert instances, and supports grouping by one or more custom labels. You can also display all alert instances in an ungrouped list by choosing the custom grouping mode without any configured labels. + +Traditionally alerts in the Alert Panel were grouped by the alert rule that created them. When you are monitoring a complex resource like an industrial pump, you typically have multiple alerts defined for that resource to observe different metrics. The new custom grouping feature allows you to view all alert instances for individual resource by specifying a label such as “pump identifier”. + +{{< figure src="/static/img/docs/alerting/unified/custom-grouping-8-4.png" max-width="1200px" caption="Custom grouping in alerting" >}} + +### WeCom contact point + +Starting with Grafana 8.4, you can [configure a WeCom](https://grafana.com/docs/grafana/next/alerting/unified-alerting/contact-points/#wecom) contact point, to send alert notifications to WeCom. + +## New panel options + +### Bar chart + +We’ve expanded the bar chart so that you can: + +- Use time for the x-axis. +- Color bars using a field property (that is, build success). +- Use labels effectively: + - Skip values when there are too many labels. + - Rotate labels. + +{{< figure src="/static/img/docs/bar-chart-panel/bar-chart-8-4.png" max-width="1200px" caption="Updated bar chart" >}} + +### Geomap + +Geomap now supports tooltips with data-links across multiple layers. + +{{< figure src="/static/img/docs/geomap-panel/geomap-tooltips-multiple-layers-8-4.png" max-width="1200px" caption="Assign SAML users role" >}} + +## OpenAPI v2 specification + +The HTTP API details are now [specified](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/papagian/api-spec/public/api-merged.json) using OpenAPI v2. + +The grafana server serves a [SwaggerUI](https://swagger.io/tools/swagger-ui/) editor via the /swagger-ui endpoint that enables users to make requests to the HTTP API via the browser. This is disabled by default and it’s enabled when the `swaggerUi` feature toggle is set. + +# Grafana Enterprise + +## Security improvements + +### Fine-grained access control works for teams + +Occasionally, Viewer, Editor, and Admin roles don’t fit what a certain user needs to do in Grafana. Now you can assign fine-grained roles directly to users so they can create reports, use Explore mode, create data sources, and perform other specific actions in Grafana. Fine-grained access control is currently in beta. + +In Grafana 8.4, you can assign roles to teams, which apply to all members of that team. This is a convenient way to grant certain permissions to a group of users all at once. It also makes permissions easier to manage when you synchronize groups from an SSO provider, like Google Oauth or Okta, to teams in Grafana. + +In 8.4 you can also control access to Team and API key functionality itself, like viewing or editing API keys and adding members to certain teams. + +Enable fine-grained access control by adding the term `accesscontrol` to the list of feature toggles in your [Grafana configuration](https://grafana.com/docs/grafana/next/administration/configuration/#feature_toggles?mdm=email), or by sending a request to support if you use Grafana Cloud. Learn more about fine-grained access control in the [fine-grained access control section of the docs](https://grafana.com/docs/grafana/next/enterprise/access-control/). + +{{< figure src="/static/img/docs/enterprise/8-4-fine-grain-access-control.png" max-width="1200px" caption="Assign SAML users role" >}} + +### Assign SAML users different roles in different Organizations + +You can use Grafana’s SAML integration to map organizations in your SAML service to [Organizations](​​https://grafana.com/docs/grafana/latest/manage-users/server-admin/server-admin-manage-orgs/) in Grafana so that users who authenticate using SAML have the right permissions. Previously, you could only choose a single role (Viewer, Editor, or Admin) for users, which would apply to all of their Organizations. Now, you can map a given SAML user or org to different roles in different Organizations, so that, for example, they can be a Viewer in one Organization and an Admin in another. + +Additionally, you can now grant multiple SAML organizations access to Grafana, using the `allowed_organizations` attribute. Previously, you could only map one. + +{{< figure src="/static/img/docs/enterprise/8-4-SAML-auth.png" max-width="1200px" caption="Assign SAML users role" >}} + +Learn more in our [SAML docs](https://grafana.com/docs/grafana/next/enterprise/saml/). + +## Performance improvements + +### Recorded queries is more stable and usable + +We’ve made stability and usability improvements to Recorded Queries, and removed the feature flag so you can get started with Recorded Queries out of the box. + +### Measure query cache hit rate and clear the cache + +[Query caching](https://grafana.com/blog/2021/09/02/reduce-costs-and-increase-performance-with-query-caching-in-grafana-cloud/) significantly reduces load times and costs of Grafana dashboards, by temporarily storing query results in a cache. Now you can measure the hit rate of your query cache, to see how many queries (and therefore how much time and money) it is saving. You can also use these measurements to tune the cache time to live (TTL), to balance performance and up-to-date data. + +You can also now manually clear the cache for a given data source in case data becomes stale, so that the next set of queries run against the data source itself. + +To learn more, refer to [query caching in the Grafana Enterprise docs](https://grafana.com/docs/grafana/next/enterprise/query-caching/) + +{{< figure src="/static/img/docs/enterprise/8-4-query-caching.png" max-width="1200px" caption="Grafana Enterprise query caching" >}} diff --git a/e2e/panels-suite/panelEdit_base.spec.ts b/e2e/panels-suite/panelEdit_base.spec.ts index 5bc8545c12f..121111ec9d8 100644 --- a/e2e/panels-suite/panelEdit_base.spec.ts +++ b/e2e/panels-suite/panelEdit_base.spec.ts @@ -7,7 +7,7 @@ e2e.scenario({ itName: 'Tests various Panel edit scenarios', addScenarioDataSource: false, addScenarioDashBoard: false, - skipScenario: true, + skipScenario: false, scenario: () => { e2e().intercept('/api/ds/query').as('query'); e2e.flows.openDashboard({ uid: 'TkZXxlNG3' }); @@ -46,11 +46,16 @@ e2e.scenario({ // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) e2e.components.Tab.title('Alert').should('be.visible').click(); e2e.components.Tab.active().should('have.text', 'Alert0'); // there's no alert so therefore Alert + 0 - e2e.components.AlertTab.content().should('not.exist'); + + // Needs to be disabled until Grafana EE turns unified alerting on by default + // e2e.components.AlertTab.content().should('not.exist'); + e2e.components.QueryTab.content().should('not.exist'); e2e.components.TransformTab.content().should('not.exist'); - e2e.components.PanelAlertTabContent.content().should('exist'); - e2e.components.PanelAlertTabContent.content().should('be.visible'); + + // Needs to be disabled until Grafana EE turns unified alerting on by default + // e2e.components.PanelAlertTabContent.content().should('exist'); + // e2e.components.PanelAlertTabContent.content().should('be.visible'); e2e.components.Tab.title('Query').should('be.visible').click(); }); diff --git a/emails/templates/ng_alert_notification.html b/emails/templates/ng_alert_notification.html index 1f967e53353..b7c7f6156dd 100644 --- a/emails/templates/ng_alert_notification.html +++ b/emails/templates/ng_alert_notification.html @@ -1,11 +1,14 @@ + +
+ [[Subject .Subject "[[.Title]]"]] [[ define "alert" ]] - - - Value: [[ .ValueString ]] - - + + + Value: [[ .ValueString ]] + + [[ if gt (len .Annotations.SortedPairs) 0 ]] @@ -187,62 +190,63 @@ - [[ if gt (len .Alerts.Firing) 0 ]] - - - - [[ range .Alerts.Firing ]] + [[ if gt (len .Alerts.Firing) 0 ]] + + + + [[ range .Alerts.Firing ]] + + + + + [[ template "alert" . ]] + [[ end ]] + [[ end ]] + [[ if gt (len .Alerts.Resolved) 0 ]] + + + + [[ range .Alerts.Resolved ]] + + + + + [[ template "alert" . ]] + [[ end ]] + [[ end ]] - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - [[ if gt (len .Alerts.Resolved) 0 ]] - - - - [[ range .Alerts.Resolved ]] - - - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - - - -
- Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
+ Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]][[ end ]] +
+ Firing + + [[ .Labels.alertname ]] +
+ Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]][[ end ]] +
+ Resolved + + [[ .Labels.alertname ]] +
- Firing - - [[ .Labels.alertname ]] + + Go to alerts page
- Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
- Resolved - - [[ .Labels.alertname ]] -
- Go to alerts page -
[[ end ]] + +
diff --git a/go.mod b/go.mod index df9a267e849..4e9cc01470b 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/gosimple/slug v1.9.0 github.com/grafana/cuetsy v0.0.0-20211119211437-8c25464cc9bf - github.com/grafana/grafana-aws-sdk v0.9.1 + github.com/grafana/grafana-aws-sdk v0.10.0 github.com/grafana/grafana-plugin-sdk-go v0.125.0 github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 @@ -77,7 +77,7 @@ require ( github.com/ohler55/ojg v1.12.9 github.com/opentracing/opentracing-go v1.2.0 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 github.com/prometheus/alertmanager v0.23.1-0.20211116083607-e2a10119aaf7 github.com/prometheus/client_golang v1.12.1 @@ -105,7 +105,7 @@ require ( go.opentelemetry.io/otel/trace v1.2.0 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e golang.org/x/exp v0.0.0-20210220032938-85be41e4509f // indirect - golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f + golang.org/x/net v0.0.0-20211013171255-e13a2654a71e golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac @@ -167,10 +167,10 @@ require ( github.com/go-openapi/analysis v0.20.1 // indirect github.com/go-openapi/errors v0.20.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/loads v0.20.2 // indirect github.com/go-openapi/runtime v0.19.29 // indirect - github.com/go-openapi/spec v0.20.3 // indirect + github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/validate v0.20.2 // indirect github.com/gogo/googleapis v1.4.1 // indirect @@ -239,9 +239,9 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/weaveworks/promrus v1.2.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect - go.mongodb.org/mongo-driver v1.5.2 // indirect + go.mongodb.org/mongo-driver v1.7.0 // indirect go.opencensus.io v0.23.0 // indirect - go.uber.org/atomic v1.9.0 // indirect + go.uber.org/atomic v1.9.0 go.uber.org/goleak v1.1.10 // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect @@ -252,7 +252,10 @@ require ( gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect ) -require cloud.google.com/go/kms v1.1.0 +require ( + cloud.google.com/go/kms v1.1.0 + github.com/golang-migrate/migrate/v4 v4.7.0 +) require ( github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect diff --git a/go.sum b/go.sum index 8df53ae14bb..665845a11d3 100644 --- a/go.sum +++ b/go.sum @@ -867,6 +867,8 @@ github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL9 github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.17.2/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= @@ -907,6 +909,8 @@ github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFut github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ= github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.17.2/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= @@ -1040,6 +1044,7 @@ github.com/gogo/status v1.1.0 h1:+eIkrewn5q6b30y+g/BJINVVdi2xH7je5MPJ3ZPK3JA= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= @@ -1238,6 +1243,8 @@ github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5J github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/grafana/grafana-aws-sdk v0.9.1 h1:jMZlsLsWnqOwLt2UNcLUsJ2z6289hLYlscK35QgS158= github.com/grafana/grafana-aws-sdk v0.9.1/go.mod h1:6KaQ8uUD4KpXr/b7bAC7zbfSXTVOiTk4XhIrwkGWn4w= +github.com/grafana/grafana-aws-sdk v0.10.0 h1:q7+mJtT/vsU5InDN57yM+BJ2z1kJDf1W4WwWPEZ0Cxw= +github.com/grafana/grafana-aws-sdk v0.10.0/go.mod h1:vFIOHEnY1u5nY0/tge1IHQjPuG6DRKr2ISf/HikUdjE= github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2ud7NNM7LrGPO4x0NFR8qLq68CqI4SmB7I2yRN2w9oE= github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE= github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M= @@ -1953,8 +1960,8 @@ github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuR github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= -github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982 h1:TdFv+3Gr3GaghJ/o80aulO4ian7GHGWMdLBXoLZH1Is= -github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -2468,8 +2475,9 @@ go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4S go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= -go.mongodb.org/mongo-driver v1.5.2 h1:AsxOLoJTgP6YNM0fXWw4OjdluYmWzQYp+lFJL7xu9fU= go.mongodb.org/mongo-driver v1.5.2/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= +go.mongodb.org/mongo-driver v1.7.0 h1:hHrvOBWlWB2c7+8Gh/Xi5jj82AgidK/t7KVXBZ+IyUA= +go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -2725,8 +2733,9 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211013171255-e13a2654a71e h1:Xj+JO91noE97IN6F/7WZxzC5QE6yENAQPrwIYhW3bsA= +golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/latest.json b/latest.json index 91df80dab6b..c6e1d81072c 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "8.3.4", - "testing": "8.3.4" + "stable": "8.3.5", + "testing": "8.4.0-beta1" } diff --git a/package.json b/package.json index 2698218ff20..1b4e3a3826b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "packages:typecheck": "lerna run typecheck", "packages:clean": "lerna run clean", "precommit": "yarn run lint-staged", - "prettier:check": "prettier --list-different \"**/*.{scss,md,mdx}\"", + "prettier:check": "prettier --check --list-different=false --loglevel=warn \"**/*.{scss,md,mdx}\"", "prettier:write": "prettier --list-different \"**/*.{scss,md,mdx}\" --write", "start": "yarn themes:generate && yarn dev --watch", "start:noTsCheck": "yarn start --env noTsCheck=1", @@ -248,6 +248,7 @@ "@opentelemetry/semantic-conventions": "1.0.1", "@popperjs/core": "2.11.2", "@react-aria/button": "3.3.4", + "@react-aria/dialog": "3.1.4", "@react-aria/focus": "3.5.0", "@react-aria/interactions": "3.7.0", "@react-aria/menu": "3.3.0", @@ -361,7 +362,7 @@ "tether-drop": "https://github.com/torkelo/drop", "tinycolor2": "1.4.2", "tslib": "2.3.1", - "uplot": "1.6.18", + "uplot": "1.6.19", "uuid": "8.3.2", "vendor": "link:./public/vendor", "visjs-network": "4.25.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index cb11f6558d7..55d9d01188c 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -38,7 +38,7 @@ "regenerator-runtime": "0.13.9", "rxjs": "7.5.1", "tslib": "2.3.1", - "uplot": "1.6.18", + "uplot": "1.6.19", "xss": "1.0.10" }, "devDependencies": { diff --git a/packages/grafana-data/src/field/fieldState.ts b/packages/grafana-data/src/field/fieldState.ts index 1bbfde314d9..14e75bd8095 100644 --- a/packages/grafana-data/src/field/fieldState.ts +++ b/packages/grafana-data/src/field/fieldState.ts @@ -38,14 +38,16 @@ export function getFrameDisplayName(frame: DataFrame, index?: number) { export function getFieldDisplayName(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string { const existingTitle = field.state?.displayName; + const multipleFrames = Boolean(allFrames && allFrames.length > 1); - if (existingTitle) { + if (existingTitle && multipleFrames === field.state?.multipleFrames) { return existingTitle; } const displayName = calculateFieldDisplayName(field, frame, allFrames); field.state = field.state || {}; field.state.displayName = displayName; + field.state.multipleFrames = multipleFrames; return displayName; } diff --git a/packages/grafana-data/src/field/overrides/processors.ts b/packages/grafana-data/src/field/overrides/processors.ts index b56f9e2b53e..1ba88fab1a9 100644 --- a/packages/grafana-data/src/field/overrides/processors.ts +++ b/packages/grafana-data/src/field/overrides/processors.ts @@ -74,6 +74,8 @@ export const valueMappingsOverrideProcessor = ( export interface SelectFieldConfigSettings { allowCustomValue?: boolean; + isClearable?: boolean; + /** The default options */ options: Array>; diff --git a/packages/grafana-data/src/themes/createShadows.ts b/packages/grafana-data/src/themes/createShadows.ts index bb6dd49baf3..b4f686318d9 100644 --- a/packages/grafana-data/src/themes/createShadows.ts +++ b/packages/grafana-data/src/themes/createShadows.ts @@ -13,7 +13,7 @@ export function createShadows(colors: ThemeColors): ThemeShadows { return { z1: '0px 1px 2px rgba(24, 26, 27, 0.75)', z2: '0px 4px 8px rgba(24, 26, 27, 0.75)', - z3: '0px 10px 20px rgb(20,20,20)', + z3: '0px 8px 24px rgb(1,4,9)', }; } diff --git a/packages/grafana-data/src/transformations/transformers/order.test.ts b/packages/grafana-data/src/transformations/transformers/order.test.ts index c9e4ea7eafa..8ecd42ec4e1 100644 --- a/packages/grafana-data/src/transformations/transformers/order.test.ts +++ b/packages/grafana-data/src/transformations/transformers/order.test.ts @@ -47,6 +47,7 @@ describe('Order Transformer', () => { labels: undefined, state: { displayName: 'temperature', + multipleFrames: false, }, }, { @@ -57,6 +58,7 @@ describe('Order Transformer', () => { labels: undefined, state: { displayName: 'humidity', + multipleFrames: false, }, }, { @@ -67,6 +69,7 @@ describe('Order Transformer', () => { labels: undefined, state: { displayName: 'time', + multipleFrames: false, }, }, ]); @@ -108,6 +111,7 @@ describe('Order Transformer', () => { labels: undefined, state: { displayName: 'humidity', + multipleFrames: false, }, }, { @@ -118,6 +122,7 @@ describe('Order Transformer', () => { labels: undefined, state: { displayName: 'time', + multipleFrames: false, }, }, { @@ -128,6 +133,7 @@ describe('Order Transformer', () => { labels: undefined, state: { displayName: 'pressure', + multipleFrames: false, }, }, ]); diff --git a/packages/grafana-data/src/transformations/transformers/organize.test.ts b/packages/grafana-data/src/transformations/transformers/organize.test.ts index b0dff65e431..28ec939938a 100644 --- a/packages/grafana-data/src/transformations/transformers/organize.test.ts +++ b/packages/grafana-data/src/transformations/transformers/organize.test.ts @@ -52,6 +52,7 @@ describe('OrganizeFields Transformer', () => { name: 'temperature', state: { displayName: 'temperature', + multipleFrames: false, }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), @@ -64,6 +65,7 @@ describe('OrganizeFields Transformer', () => { name: 'humidity', state: { displayName: 'renamed_humidity', + multipleFrames: false, }, type: FieldType.number, values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), @@ -113,6 +115,7 @@ describe('OrganizeFields Transformer', () => { name: 'time', state: { displayName: 'renamed_time', + multipleFrames: false, }, type: FieldType.time, values: new ArrayVector([3000, 4000, 5000, 6000]), @@ -123,6 +126,7 @@ describe('OrganizeFields Transformer', () => { name: 'pressure', state: { displayName: 'pressure', + multipleFrames: false, }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), diff --git a/packages/grafana-data/src/transformations/transformers/reduce.test.ts b/packages/grafana-data/src/transformations/transformers/reduce.test.ts index da43ed20850..721d34514a5 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.test.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.test.ts @@ -164,7 +164,7 @@ describe('Reducer Transformer', () => { { name: 'Field', type: FieldType.string, - values: new ArrayVector(['A temperature', 'A humidity']), + values: new ArrayVector(['temperature', 'humidity']), config: {}, }, { @@ -213,7 +213,7 @@ describe('Reducer Transformer', () => { { name: 'Field', type: FieldType.string, - values: new ArrayVector(['A temperature']), + values: new ArrayVector(['temperature']), config: {}, }, { @@ -286,16 +286,16 @@ describe('Reducer Transformer', () => { const seriesA = toDataFrame({ name: 'a', fields: [ - { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, - { name: 'value', type: FieldType.number, values: [3, 4, 5, 6], state: { displayName: 'a' } }, + { name: 'Time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, + { name: 'Value', type: FieldType.number, values: [3, 4, 5, 6] }, ], }); const seriesB = toDataFrame({ name: '2021', fields: [ - { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, - { name: 'value', type: FieldType.number, values: [7, 8, 9, 10], state: { displayName: '2021' } }, + { name: 'Time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, + { name: 'Value', type: FieldType.number, values: [7, 8, 9, 10] }, ], }); diff --git a/packages/grafana-data/src/transformations/transformers/rename.test.ts b/packages/grafana-data/src/transformations/transformers/rename.test.ts index 641ab9bc8ef..10cff84ab99 100644 --- a/packages/grafana-data/src/transformations/transformers/rename.test.ts +++ b/packages/grafana-data/src/transformations/transformers/rename.test.ts @@ -48,6 +48,7 @@ describe('Rename Transformer', () => { name: 'time', state: { displayName: 'Total time', + multipleFrames: false, }, type: FieldType.time, values: new ArrayVector([3000, 4000, 5000, 6000]), @@ -60,6 +61,7 @@ describe('Rename Transformer', () => { name: 'temperature', state: { displayName: 'how cold is it?', + multipleFrames: false, }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), @@ -72,6 +74,7 @@ describe('Rename Transformer', () => { labels: undefined, state: { displayName: 'Moistness', + multipleFrames: false, }, type: FieldType.number, values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), @@ -115,6 +118,7 @@ describe('Rename Transformer', () => { labels: undefined, state: { displayName: 'ttl', + multipleFrames: false, }, type: FieldType.time, values: new ArrayVector([3000, 4000, 5000, 6000]), @@ -125,6 +129,7 @@ describe('Rename Transformer', () => { name: 'pressure', state: { displayName: 'pressure', + multipleFrames: false, }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), @@ -137,6 +142,7 @@ describe('Rename Transformer', () => { name: 'humidity', state: { displayName: 'hum', + multipleFrames: false, }, type: FieldType.number, values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), diff --git a/packages/grafana-data/src/transformations/transformers/renameByRegex.test.ts b/packages/grafana-data/src/transformations/transformers/renameByRegex.test.ts index 2c4cac8df3f..ae617320ab9 100644 --- a/packages/grafana-data/src/transformations/transformers/renameByRegex.test.ts +++ b/packages/grafana-data/src/transformations/transformers/renameByRegex.test.ts @@ -46,6 +46,7 @@ describe('Rename By Regex Transformer', () => { "name": "Time", "state": Object { "displayName": "Time", + "multipleFrames": false, }, "type": "time", "values": Array [ @@ -62,6 +63,7 @@ describe('Rename By Regex Transformer', () => { "name": "Value", "state": Object { "displayName": "web-01", + "multipleFrames": false, }, "type": "number", "values": Array [ @@ -96,6 +98,7 @@ describe('Rename By Regex Transformer', () => { "name": "Time", "state": Object { "displayName": "Time", + "multipleFrames": false, }, "type": "time", "values": Array [ @@ -112,6 +115,7 @@ describe('Rename By Regex Transformer', () => { "name": "Value", "state": Object { "displayName": "web-01.example.com", + "multipleFrames": false, }, "type": "number", "values": Array [ @@ -147,6 +151,7 @@ describe('Rename By Regex Transformer', () => { "name": "Time", "state": Object { "displayName": "Time", + "multipleFrames": false, }, "type": "time", "values": Array [ @@ -163,6 +168,7 @@ describe('Rename By Regex Transformer', () => { "name": "Value", "state": Object { "displayName": "web-01.example.com", + "multipleFrames": false, }, "type": "number", "values": Array [ diff --git a/packages/grafana-data/src/transformations/transformers/seriesToColumns.test.ts b/packages/grafana-data/src/transformations/transformers/seriesToColumns.test.ts index ffc8522def3..24cd021ea8c 100644 --- a/packages/grafana-data/src/transformations/transformers/seriesToColumns.test.ts +++ b/packages/grafana-data/src/transformations/transformers/seriesToColumns.test.ts @@ -170,7 +170,9 @@ describe('SeriesToColumns Transformer', () => { "name": "even", }, "name": "time", - "state": Object {}, + "state": Object { + "multipleFrames": true, + }, "type": "time", "values": Array [ 3000, @@ -208,7 +210,9 @@ describe('SeriesToColumns Transformer', () => { "name": "odd", }, "name": "time", - "state": Object {}, + "state": Object { + "multipleFrames": true, + }, "type": "time", "values": Array [ undefined, @@ -267,7 +271,9 @@ describe('SeriesToColumns Transformer', () => { Object { "config": Object {}, "name": "time", - "state": Object {}, + "state": Object { + "multipleFrames": true, + }, "type": "time", "values": Array [ 1000, diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index bb4e9784e34..d0f6726017d 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -67,6 +67,12 @@ export interface FieldConfig { min?: number | null; max?: number | null; + // Interval indicates the expected regular step between values in the series. + // When an interval exists, consumers can identify "missing" values when the expected value is not present. + // The grafana timeseries visualization will render disconnected values when missing values are found it the time field. + // The interval uses the same units as the values. For time.Time, this is defined in milliseconds. + interval?: number | null; + // Convert input values into a display string mappings?: ValueMapping[]; @@ -169,6 +175,12 @@ export interface FieldState { * @internal -- we will try to make this unnecessary */ origin?: DataFrameFieldIndex; + + /** + * Boolean value is true if field is in a larger data set with multiple frames. + * This is only related to the cached displayName property above. + */ + multipleFrames?: boolean; } /** @public */ diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 971014422c9..6e71b8ff045 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -36,4 +36,7 @@ export interface FeatureToggles { showFeatureFlagsInUI?: boolean; disable_http_request_histogram?: boolean; validatedQueries?: boolean; + swaggerUi?: boolean; + featureHighlights?: boolean; + migrationLocking?: boolean; } diff --git a/packages/grafana-runtime/src/services/LocationService.ts b/packages/grafana-runtime/src/services/LocationService.ts index 06196ad3690..e76cc4c57d7 100644 --- a/packages/grafana-runtime/src/services/LocationService.ts +++ b/packages/grafana-runtime/src/services/LocationService.ts @@ -5,7 +5,7 @@ import { attachDebugger, createLogger } from '@grafana/ui'; import { config } from '../config'; /** - * @alpha + * @public * A wrapper to help work with browser location and history */ export interface LocationService { @@ -120,7 +120,7 @@ export class HistoryWrapper implements LocationService { } /** - * @alpha + * @public * Parses a location search string to an object * */ export function locationSearchToObject(search: string | number): UrlQueryMap { @@ -137,12 +137,13 @@ export function locationSearchToObject(search: string | number): UrlQueryMap { } /** - * @alpha + * @public */ export let locationService: LocationService = new HistoryWrapper(); -/** @internal +/** * Used for tests only + * @internal */ export const setLocationService = (location: LocationService) => { if (process.env.NODE_ENV !== 'test') { diff --git a/packages/grafana-runtime/src/services/LocationSrv.ts b/packages/grafana-runtime/src/services/LocationSrv.ts index c1cdd03144c..ac730844889 100644 --- a/packages/grafana-runtime/src/services/LocationSrv.ts +++ b/packages/grafana-runtime/src/services/LocationSrv.ts @@ -1,13 +1,8 @@ -/** - * Passed as options to the {@link LocationSrv} to describe how the automatically navigation - * should be performed. - * - * @public - */ import { UrlQueryMap } from '@grafana/data'; /** * @public + * @deprecated in favor of {@link locationService} and will be removed in Grafana 9 */ export interface LocationUpdate { /** @@ -47,6 +42,7 @@ export interface LocationUpdate { * be done via the LocationSrv and it will make sure to update the application state accordingly. * * @public + * @deprecated in favor of {@link locationService} and will be removed in Grafana 9 */ export interface LocationSrv { update(options: LocationUpdate): void; @@ -69,6 +65,7 @@ export function setLocationSrv(instance: LocationSrv) { * the user to a new place in Grafana. * * @public + * @deprecated in favor of {@link locationService} and will be removed in Grafana 9 */ export function getLocationSrv(): LocationSrv { return singletonInstance; diff --git a/packages/grafana-runtime/src/services/appEvents.ts b/packages/grafana-runtime/src/services/appEvents.ts index c430a554df9..2c8a12e4a00 100644 --- a/packages/grafana-runtime/src/services/appEvents.ts +++ b/packages/grafana-runtime/src/services/appEvents.ts @@ -40,8 +40,8 @@ export class CopyPanelEvent extends BusEventWithPayload { let singletonInstance: EventBus; /** - * Used during startup by Grafana to set the LocationSrv so it is available - * via the {@link getLocationSrv} to the rest of the application. + * Used during startup by Grafana to set the setAppEvents so it is available + * via the {@link setAppEvents} to the rest of the application. * * @internal */ diff --git a/packages/grafana-schema/src/schema/graph.cue b/packages/grafana-schema/src/schema/graph.cue index 0af58f7c5b1..cc1abcf37b9 100644 --- a/packages/grafana-schema/src/schema/graph.cue +++ b/packages/grafana-schema/src/schema/graph.cue @@ -7,6 +7,7 @@ LineInterpolation: "linear" | "smooth" | "stepBefore" | "stepAfter" @cue ScaleDistribution: "linear" | "log" | "ordinal" @cuetsy(kind="enum") GraphGradientMode: "none" | "opacity" | "hue" | "scheme" @cuetsy(kind="enum") StackingMode: "none" | "normal" | "percent" @cuetsy(kind="enum") +GraphTransform: "constant" | "negative-Y" @cuetsy(kind="enum") BarAlignment: -1 | 0 | 1 @cuetsy(kind="enum",memberNames="Before|Center|After") ScaleOrientation: 0 | 1 @cuetsy(kind="enum",memberNames="Horizontal|Vertical") ScaleDirection: 1 | 1 | -1 | -1 @cuetsy(kind="enum",memberNames="Up|Right|Down|Left") @@ -84,4 +85,5 @@ GraphFieldConfig: { drawStyle?: GraphDrawStyle gradientMode?: GraphGradientMode thresholdsStyle?: GraphThresholdsStyleConfig + transform?: GraphTransform } @cuetsy(kind="interface") diff --git a/packages/grafana-schema/src/schema/graph.gen.ts b/packages/grafana-schema/src/schema/graph.gen.ts index 0ed0f810bf4..623d750a250 100644 --- a/packages/grafana-schema/src/schema/graph.gen.ts +++ b/packages/grafana-schema/src/schema/graph.gen.ts @@ -24,6 +24,11 @@ export enum GraphDrawStyle { Points = 'points', } +export enum GraphTransform { + Constant = 'constant', + NegativeY = 'negative-Y', +} + export enum LineInterpolation { Linear = 'linear', Smooth = 'smooth', @@ -256,6 +261,7 @@ export interface GraphFieldConfig drawStyle?: GraphDrawStyle; gradientMode?: GraphGradientMode; thresholdsStyle?: GraphThresholdsStyleConfig; + transform?: GraphTransform; } export interface VizLegendOptions { diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index 1d3b1649e4c..fc600afbdee 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -86,7 +86,7 @@ "postcss-loader": "3.0.0", "postcss-preset-env": "6.7.0", "prettier": "2.5.1", - "react-dev-utils": "^11.0.4", + "react-dev-utils": "^12.0.0", "replace-in-file": "^4.1.0", "replace-in-file-webpack-plugin": "^1.0.6", "rimraf": "^3.0.0", diff --git a/packages/grafana-ui/.storybook/main.ts b/packages/grafana-ui/.storybook/main.ts index d7e254c7915..9b738e8b239 100644 --- a/packages/grafana-ui/.storybook/main.ts +++ b/packages/grafana-ui/.storybook/main.ts @@ -166,34 +166,6 @@ module.exports = { }) ); - config.plugins = fixProcessResolution(config.plugins); - - return config; - }, - // This webpack config only affects the manager (storybook UI) and generally shouldn't need to be changed. - // If stories are failing to build check `webpackFinal` property above. - managerWebpack: async (config: any) => { - config.plugins = fixProcessResolution(config.plugins); - return config; }, }; - -// Storybook 6.4.13 introduced https://github.com/storybookjs/storybook/pull/17213 -// which appears to prevent resolving correctly with yarn pnp. -// We can probably remove this in the next patch release. -function fixProcessResolution(plugins: any[]) { - plugins.forEach((p: any, i: number) => { - if (p.constructor.name === 'ProvidePlugin') { - plugins.splice( - i, - 1, - new ProvidePlugin({ - process: require.resolve('process/browser.js'), - }) - ); - } - }); - - return plugins; -} diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index d74d6fcdfe1..bc6dce050e6 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -40,6 +40,7 @@ "@monaco-editor/react": "4.3.1", "@popperjs/core": "2.11.2", "@react-aria/button": "3.3.4", + "@react-aria/dialog": "3.1.4", "@react-aria/focus": "3.5.0", "@react-aria/menu": "3.3.0", "@react-aria/overlays": "3.7.3", @@ -88,7 +89,7 @@ "slate-plain-serializer": "0.7.10", "tinycolor2": "1.4.2", "tslib": "2.3.1", - "uplot": "1.6.18", + "uplot": "1.6.19", "uuid": "8.3.2" }, "devDependencies": { @@ -98,20 +99,20 @@ "@rollup/plugin-commonjs": "21.0.1", "@rollup/plugin-image": "2.1.1", "@rollup/plugin-node-resolve": "13.1.3", - "@storybook/addon-a11y": "6.4.13", - "@storybook/addon-actions": "6.4.13", - "@storybook/addon-docs": "6.4.13", - "@storybook/addon-essentials": "6.4.13", + "@storybook/addon-a11y": "6.4.15", + "@storybook/addon-actions": "6.4.15", + "@storybook/addon-docs": "6.4.15", + "@storybook/addon-essentials": "6.4.15", "@storybook/addon-knobs": "6.4.0", - "@storybook/addon-storysource": "6.4.13", - "@storybook/addons": "6.4.13", - "@storybook/api": "6.4.13", - "@storybook/builder-webpack5": "6.4.13", - "@storybook/components": "6.4.13", - "@storybook/core-events": "6.4.13", - "@storybook/manager-webpack5": "6.4.13", - "@storybook/react": "6.4.13", - "@storybook/theming": "6.4.13", + "@storybook/addon-storysource": "6.4.15", + "@storybook/addons": "6.4.15", + "@storybook/api": "6.4.15", + "@storybook/builder-webpack5": "6.4.15", + "@storybook/components": "6.4.15", + "@storybook/core-events": "6.4.15", + "@storybook/manager-webpack5": "6.4.15", + "@storybook/react": "6.4.15", + "@storybook/theming": "6.4.15", "@swc/helpers": "0.3.2", "@testing-library/dom": "8.11.1", "@testing-library/jest-dom": "5.16.1", diff --git a/packages/grafana-ui/src/components/Button/ToolbarButton.tsx b/packages/grafana-ui/src/components/Button/ToolbarButton.tsx index 84eb8c0b513..c262b2962eb 100644 --- a/packages/grafana-ui/src/components/Button/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/Button/ToolbarButton.tsx @@ -29,6 +29,8 @@ export interface Props extends ButtonHTMLAttributes { variant?: ToolbarButtonVariant; /** Hide any children and only show icon */ iconOnly?: boolean; + /** Show highlight dot */ + isHighlighted?: boolean; } export type ToolbarButtonVariant = 'default' | 'primary' | 'destructive' | 'active'; @@ -48,6 +50,7 @@ export const ToolbarButton = forwardRef( variant = 'default', iconOnly, 'aria-label': ariaLabel, + isHighlighted, ...rest }, ref @@ -84,6 +87,7 @@ export const ToolbarButton = forwardRef( {children && !iconOnly &&
{children}
} {isOpen === false && } {isOpen === true && } + {isHighlighted &&
} ); @@ -122,6 +126,7 @@ const getStyles = (theme: GrafanaTheme2) => { return { button: css` label: toolbar-button; + position: relative; display: flex; align-items: center; height: ${theme.spacing(theme.components.height.md)}; @@ -209,5 +214,15 @@ const getStyles = (theme: GrafanaTheme2) => { contentWithRightIcon: css` padding-right: ${theme.spacing(0.5)}; `, + highlight: css` + background-color: ${theme.colors.success.main}; + border-radius: 50%; + width: 6px; + height: 6px; + position: absolute; + top: -3px; + right: -3px; + z-index: 1; + `, }; }; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index ff99b57c90e..84767c4fb92 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -104,9 +104,9 @@ class UnThemedColorPickerPopover extends Reac <> {Object.keys(customPickers).map((key) => { return ( -
+
+ ); })} @@ -118,7 +118,11 @@ class UnThemedColorPickerPopover extends Reac const styles = getStyles(theme); return ( -
+ {/* + tabIndex=-1 is needed here to support highlighting text within the picker when using FocusScope + see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 + */} +
); @@ -105,10 +108,7 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE className={styles.element} aria-label={label} > - - {item?.icon && } - {item?.img && {`${item.text}} - + {itemContent} ) : ( - - {item?.icon && } - {item?.img && {`${item.text}} - + {itemContent} ); } const overlayRef = React.useRef(null); + const { dialogProps } = useDialog({}, overlayRef); const { overlayProps } = useOverlay( { onClose: () => state.close(), @@ -155,7 +153,7 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE }} > -
+
state.close()} /> {menu} state.close()} /> diff --git a/public/app/core/components/NavBar/NavBarMenu.tsx b/public/app/core/components/NavBar/NavBarMenu.tsx index c5bb21fac35..ae35bde4235 100644 --- a/public/app/core/components/NavBar/NavBarMenu.tsx +++ b/public/app/core/components/NavBar/NavBarMenu.tsx @@ -2,6 +2,7 @@ import React, { useRef } from 'react'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { CustomScrollbar, Icon, IconButton, IconName, useTheme2 } from '@grafana/ui'; import { FocusScope } from '@react-aria/focus'; +import { useDialog } from '@react-aria/dialog'; import { useOverlay } from '@react-aria/overlays'; import { css } from '@emotion/css'; import { NavBarMenuItem } from './NavBarMenuItem'; @@ -16,6 +17,7 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) { const theme = useTheme2(); const styles = getStyles(theme); const ref = useRef(null); + const { dialogProps } = useDialog({}, ref); const { overlayProps } = useOverlay( { isDismissable: true, @@ -27,7 +29,7 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) { return ( -
+
diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 40034d4299f..0fdecd751b7 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -55,7 +55,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css` width: 100%; flex-grow: 1; - width: 100%; min-height: 0; `, }); diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index ffec4446ec8..8d770f6f051 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -3,6 +3,7 @@ import { css } from '@emotion/css'; import { Tab, TabsBar, Icon, IconName, useStyles2 } from '@grafana/ui'; import { NavModel, NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data'; import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; +import { ProBadge } from '../Upgrade/ProBadge'; export interface Props { model: NavModel; @@ -81,7 +82,6 @@ export const PageHeader: FC = ({ model }) => { const main = model.main; const children = main.children; - return (
@@ -105,20 +105,32 @@ function renderHeaderTitle(main: NavModelItem) {
- {renderTitle(main.text, main.breadcrumbs ?? [])} + {renderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)} {main.subTitle &&
{main.subTitle}
}
); } -function renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[]) { +function renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[], highlightText: NavModelItem['highlightText']) { if (!title && (!breadcrumbs || breadcrumbs.length === 0)) { return null; } if (!breadcrumbs || breadcrumbs.length === 0) { - return

{title}

; + return ( +

+ {title} + {highlightText && ( + + )} +

+ ); } const breadcrumbsResult = []; diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx index 146b21f9e68..4004424e07f 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx @@ -4,6 +4,7 @@ import { GrafanaTheme } from '@grafana/data'; import { css, cx } from '@emotion/css'; import { useUpdateEffect } from 'react-use'; import { Draggable } from 'react-beautiful-dnd'; +import { reportInteraction } from '@grafana/runtime'; interface QueryOperationRowProps { index: number; @@ -47,6 +48,24 @@ export const QueryOperationRow: React.FC = ({ setIsContentVisible(!isContentVisible); }, [isContentVisible, setIsContentVisible]); + const reportDragMousePosition = useCallback((e) => { + // When drag detected react-beautiful-dnd will preventDefault the event + // Ref: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/how-we-use-dom-events.md#a-mouse-drag-has-started-and-the-user-is-now-dragging + if (e.defaultPrevented) { + const rect = e.currentTarget.getBoundingClientRect(); + var x = e.clientX - rect.left; + var y = e.clientY - rect.top; + + // report relative mouse position within the header element + reportInteraction('query_row_reorder_drag_position', { + x: x / rect.width, + y: y / rect.height, + width: rect.width, + height: rect.height, + }); + } + }, []); + useUpdateEffect(() => { if (isContentVisible) { if (onOpen) { @@ -106,7 +125,9 @@ export const QueryOperationRow: React.FC = ({ return ( <>
-
{rowHeader}
+
+ {rowHeader} +
{isContentVisible &&
{children}
}
diff --git a/public/app/core/components/RolePicker/TeamRolePicker.tsx b/public/app/core/components/RolePicker/TeamRolePicker.tsx index c04b76180ac..e7f7d6cbd5c 100644 --- a/public/app/core/components/RolePicker/TeamRolePicker.tsx +++ b/public/app/core/components/RolePicker/TeamRolePicker.tsx @@ -1,38 +1,29 @@ import React, { FC, useState } from 'react'; import { useAsync } from 'react-use'; -import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Role } from 'app/types'; +import { Role } from 'app/types'; import { RolePicker } from './RolePicker'; -import { fetchRoleOptions, fetchTeamRoles, updateTeamRoles } from './api'; +import { fetchTeamRoles, updateTeamRoles } from './api'; export interface Props { teamId: number; orgId?: number; - getRoleOptions?: () => Promise; + roleOptions: Role[]; disabled?: boolean; builtinRolesDisabled?: boolean; } -export const TeamRolePicker: FC = ({ teamId, orgId, getRoleOptions, disabled, builtinRolesDisabled }) => { - const [roleOptions, setRoleOptions] = useState([]); +export const TeamRolePicker: FC = ({ teamId, orgId, roleOptions, disabled, builtinRolesDisabled }) => { const [appliedRoles, setAppliedRoles] = useState([]); const { loading } = useAsync(async () => { try { - if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { - let options = await (getRoleOptions ? getRoleOptions() : fetchRoleOptions(orgId)); - setRoleOptions(options.filter((option) => !option.name?.startsWith('managed:'))); - } else { - setRoleOptions([]); - } - const teamRoles = await fetchTeamRoles(teamId, orgId); setAppliedRoles(teamRoles); } catch (e) { // TODO handle error console.error('Error loading options'); } - }, [getRoleOptions, orgId, teamId]); + }, [orgId, teamId]); return ( void; - getRoleOptions?: () => Promise; - getBuiltinRoles?: () => Promise<{ [key: string]: Role[] }>; + roleOptions: Role[]; + builtInRoles?: { [key: string]: Role[] }; disabled?: boolean; builtinRolesDisabled?: boolean; } @@ -21,31 +21,15 @@ export const UserRolePicker: FC = ({ userId, orgId, onBuiltinRoleChange, - getRoleOptions, - getBuiltinRoles, + roleOptions, + builtInRoles, disabled, builtinRolesDisabled, }) => { - const [roleOptions, setRoleOptions] = useState([]); const [appliedRoles, setAppliedRoles] = useState([]); - const [builtInRoles, setBuiltinRoles] = useState>({}); const { loading } = useAsync(async () => { try { - if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { - let options = await (getRoleOptions ? getRoleOptions() : fetchRoleOptions(orgId)); - setRoleOptions(options.filter((option) => !option.name?.startsWith('managed:'))); - } else { - setRoleOptions([]); - } - - if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) { - const builtInRoles = await (getBuiltinRoles ? getBuiltinRoles() : fetchBuiltinRoles(orgId)); - setBuiltinRoles(builtInRoles); - } else { - setBuiltinRoles({}); - } - if (contextSrv.hasPermission(AccessControlAction.ActionUserRolesList)) { const userRoles = await fetchUserRoles(userId, orgId); setAppliedRoles(userRoles); @@ -56,7 +40,7 @@ export const UserRolePicker: FC = ({ // TODO handle error console.error('Error loading options'); } - }, [getBuiltinRoles, getRoleOptions, orgId, userId]); + }, [orgId, userId]); return ( { "name": "values", "state": Object { "displayName": "values", + "multipleFrames": false, }, "type": "number", "values": Array [ diff --git a/public/app/core/components/Upgrade/ProBadge.tsx b/public/app/core/components/Upgrade/ProBadge.tsx index b8b3ede8c8f..eb839467deb 100644 --- a/public/app/core/components/Upgrade/ProBadge.tsx +++ b/public/app/core/components/Upgrade/ProBadge.tsx @@ -2,21 +2,21 @@ import React, { HTMLAttributes, useEffect } from 'react'; import { css, cx } from '@emotion/css'; import { useStyles2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportExperimentView } from '@grafana/runtime'; export interface Props extends HTMLAttributes { text?: string; - /** Function to call when component initializes, e.g. event trackers */ - onLoad?: (...args: any[]) => void; + experimentId?: string; } -export const ProBadge = ({ text = 'PRO', className, onLoad, ...htmlProps }: Props) => { +export const ProBadge = ({ text = 'PRO', className, experimentId, ...htmlProps }: Props) => { const styles = useStyles2(getStyles); useEffect(() => { - if (onLoad) { - onLoad(); + if (experimentId) { + reportExperimentView(experimentId, 'test', ''); } - }, [onLoad]); + }, [experimentId]); return ( diff --git a/public/app/core/components/Upgrade/UpgradeBox.tsx b/public/app/core/components/Upgrade/UpgradeBox.tsx index 24bf3c1f21c..996f1588272 100644 --- a/public/app/core/components/Upgrade/UpgradeBox.tsx +++ b/public/app/core/components/Upgrade/UpgradeBox.tsx @@ -1,4 +1,4 @@ -import React, { HTMLAttributes } from 'react'; +import React, { ReactNode, HTMLAttributes } from 'react'; import { css, cx } from '@emotion/css'; import { Icon, LinkButton, useStyles2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; @@ -6,19 +6,25 @@ import { GrafanaTheme2 } from '@grafana/data'; type ComponentSize = 'sm' | 'md'; export interface Props extends HTMLAttributes { - text: string; + text?: string; size?: ComponentSize; + children?: ReactNode; + secondaryAction?: { + url: string; + text: string; + }; } -export const UpgradeBox = ({ text, className, size = 'md', ...htmlProps }: Props) => { +export const UpgradeBox = ({ text, className, children, secondaryAction, size = 'md', ...htmlProps }: Props) => { const styles = useStyles2((theme) => getUpgradeBoxStyles(theme, size)); return (
- +
-
You’ve found a Pro feature!
-

{text}

+

You’ve found a Pro feature!

+ {text &&

{text}

} + {children} Upgrade to Pro + + {secondaryAction && ( + + {secondaryAction.text} + + )}
); @@ -53,19 +72,26 @@ const getUpgradeBoxStyles = (theme: GrafanaTheme2, size: ComponentSize) => { `, text: css` margin-bottom: 0; + padding: ${theme.spacing(2, 0)}; + line-height: 1.5; `, button: css` margin-top: ${theme.spacing(2)}; + &:first-of-type { + margin-right: ${theme.spacing(1)}; + } + &:focus-visible { box-shadow: none; color: ${theme.colors.text.primary}; outline: 2px solid ${theme.colors.primary.main}; } `, + buttonSecondary: css` + color: ${theme.colors.text.secondary}; + `, icon: css` - border: 1px solid ${theme.colors.primary.shade}; - border-radius: 50%; margin: ${theme.spacing(0.5, 1, 0.5, 0.5)}; `, }; diff --git a/public/app/core/components/Upgrade/UpgradeModal.tsx b/public/app/core/components/Upgrade/UpgradeModal.tsx new file mode 100644 index 00000000000..40951c7aa67 --- /dev/null +++ b/public/app/core/components/Upgrade/UpgradeModal.tsx @@ -0,0 +1,26 @@ +import React, { useEffect } from 'react'; +import { Modal } from '@grafana/ui'; +import { reportExperimentView } from '@grafana/runtime'; +import { UpgradeBox } from './UpgradeBox'; + +export interface Props { + title: string; + text: string; + isOpen?: boolean; + onDismiss?: () => void; + experimentId?: string; +} + +export const UpgradeModal = ({ title, text, isOpen, onDismiss, experimentId }: Props) => { + useEffect(() => { + if (experimentId) { + reportExperimentView(experimentId, 'test', ''); + } + }, [experimentId]); + + return ( + + + + ); +}; diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index e3e22b60cd3..d2f66114231 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -39,7 +39,7 @@ import { } from '@grafana/data'; import { getThemeColor } from 'app/core/utils/colors'; import { SIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters'; -import { Observable, throwError, timeout } from 'rxjs'; +import { Observable } from 'rxjs'; export const LIMIT_LABEL = 'Line limit'; export const COMMON_LABELS = 'Common labels'; @@ -617,10 +617,7 @@ function aggregateFields(dataFrames: DataFrame[], config: FieldConfig): DataFram return aggregatedDataFrame; } -const LOGS_VOLUME_QUERY_DEFAULT_TIMEOUT = 60000; - type LogsVolumeQueryOptions = { - timeout?: number; extractLevel: (dataFrame: DataFrame) => LogLevel; targets: T[]; range: TimeRange; @@ -651,45 +648,36 @@ export function queryLogsVolume( data: [], }); - const subscription = (datasource.query(logsVolumeRequest) as Observable) - .pipe( - timeout({ - each: options.timeout || LOGS_VOLUME_QUERY_DEFAULT_TIMEOUT, - with: () => throwError(new Error('Request timed-out. Please make your query more specific and try again.')), - }) - ) - .subscribe({ - complete: () => { - const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume, options.extractLevel); - if (aggregatedLogsVolume[0]) { - aggregatedLogsVolume[0].meta = { - custom: { - targets: options.targets, - absoluteRange: { from: options.range.from.valueOf(), to: options.range.to.valueOf() }, - }, - }; - } - observer.next({ - state: LoadingState.Done, - error: undefined, - data: aggregatedLogsVolume, - }); - observer.complete(); - }, - next: (dataQueryResponse: DataQueryResponse) => { - rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame)); - }, - error: (error) => { - const errorMessage = error.data?.message || error.statusText || error.message; - console.error('Log volume query failed with error: ', errorMessage); - observer.next({ - state: LoadingState.Error, - error: error, - data: [], - }); - observer.error(error); - }, - }); + const subscription = (datasource.query(logsVolumeRequest) as Observable).subscribe({ + complete: () => { + const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume, options.extractLevel); + if (aggregatedLogsVolume[0]) { + aggregatedLogsVolume[0].meta = { + custom: { + targets: options.targets, + absoluteRange: { from: options.range.from.valueOf(), to: options.range.to.valueOf() }, + }, + }; + } + observer.next({ + state: LoadingState.Done, + error: undefined, + data: aggregatedLogsVolume, + }); + observer.complete(); + }, + next: (dataQueryResponse: DataQueryResponse) => { + rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame)); + }, + error: (error) => { + observer.next({ + state: LoadingState.Error, + error: error, + data: [], + }); + observer.error(error); + }, + }); return () => { subscription?.unsubscribe(); }; diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index f523fb19314..d26fcbd4905 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -20,7 +20,12 @@ import { getConfig } from 'app/core/config'; import { DashboardSearchHit } from 'app/features/search/types'; import { FolderDTO } from 'app/types'; import { ContextSrv, contextSrv } from './context_srv'; -import { parseInitFromOptions, parseResponseBody, parseUrlFromOptions } from '../utils/fetch'; +import { + isContentTypeApplicationJson, + parseInitFromOptions, + parseResponseBody, + parseUrlFromOptions, +} from '../utils/fetch'; import { isDataQuery, isLocalUrl } from '../utils/query'; import { FetchQueue } from './FetchQueue'; import { ResponseQueue } from './ResponseQueue'; @@ -187,7 +192,9 @@ export class BackendSrv implements BackendService { mergeMap(async (response) => { const { status, statusText, ok, headers, url, type, redirected } = response; - const data = await parseResponseBody(response, options.responseType); + const responseType = options.responseType ?? (isContentTypeApplicationJson(headers) ? 'json' : undefined); + + const data = await parseResponseBody(response, responseType); const fetchResponse: FetchResponse = { status, statusText, diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 262ac14abd4..b9dfcaca92a 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -1,8 +1,8 @@ import config from '../../core/config'; import { extend } from 'lodash'; import { rangeUtil, WithAccessControlMetadata } from '@grafana/data'; -import { featureEnabled } from '@grafana/runtime'; import { AccessControlAction, UserPermission } from 'app/types'; +import { featureEnabled, getBackendSrv } from '@grafana/runtime'; export class User { id: number; @@ -66,6 +66,18 @@ export class ContextSrv { this.minRefreshInterval = config.minRefreshInterval; } + async fetchUserPermissions() { + try { + if (this.accessControlEnabled()) { + this.user.permissions = await getBackendSrv().get('/api/access-control/user/permissions', { + reloadcache: true, + }); + } + } catch (e) { + console.error(e); + } + } + /** * Indicate the user has been logged out */ @@ -83,13 +95,17 @@ export class ContextSrv { } accessControlEnabled(): boolean { + return Boolean(config.featureToggles['accesscontrol']); + } + + licensedAccessControlEnabled(): boolean { return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']); } // Checks whether user has required permission hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean { // Fallback if access control disabled - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return true; } @@ -99,7 +115,7 @@ export class ContextSrv { // Checks whether user has required permission hasPermission(action: AccessControlAction | string): boolean { // Fallback if access control disabled - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return true; } @@ -126,22 +142,29 @@ export class ContextSrv { } hasAccessToExplore() { - if (config.featureToggles['accesscontrol']) { + if (this.accessControlEnabled()) { return this.hasPermission(AccessControlAction.DataSourcesExplore); } return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled; } hasAccess(action: string, fallBack: boolean) { - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return fallBack; } return this.hasPermission(action); } + hasAccessInMetadata(action: string, object: WithAccessControlMetadata, fallBack: boolean) { + if (!config.featureToggles['accesscontrol']) { + return fallBack; + } + return this.hasPermissionInMetadata(action, object); + } + // evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled evaluatePermission(fallback: () => string[], actions: string[]) { - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return fallback(); } if (actions.some((action) => this.hasPermission(action))) { diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index a25e8f75a54..97fde9b8820 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -11,6 +11,7 @@ import { getExploreUrl, GetExploreUrlArguments, getTimeRangeFromUrl, + getTimeRange, } from './explore'; import store from 'app/core/store'; import { dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data'; @@ -361,6 +362,19 @@ describe('getTimeRangeFromUrl', () => { }); }); +describe('getTimeRange', () => { + describe('should flip from and to when from is after to', () => { + const rawRange = { + from: 'now', + to: 'now-6h', + }; + + const range = getTimeRange('utc', rawRange, 0); + + expect(range.from.isBefore(range.to)).toBe(true); + }); +}); + describe('getRefIds', () => { describe('when called with a null value', () => { it('then it should return empty array', () => { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 77a29040e18..f10af2037f1 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -358,11 +358,13 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance?: DataSour }; export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange, fiscalYearStartMonth: number): TimeRange => { - return { - from: dateMath.parse(rawRange.from, false, timeZone as any, fiscalYearStartMonth)!, - to: dateMath.parse(rawRange.to, true, timeZone as any, fiscalYearStartMonth)!, - raw: rawRange, - }; + let range = rangeUtil.convertRawToRange(rawRange, timeZone, fiscalYearStartMonth); + + if (range.to.isBefore(range.from)) { + range = rangeUtil.convertRawToRange({ from: range.raw.to, to: range.raw.from }, timeZone, fiscalYearStartMonth); + } + + return range; }; const parseRawTime = (value: string | DateTime): TimeFragment | null => { diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 8cff0b30c8c..ae9b8a21405 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -20,6 +20,7 @@ import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker'; import { OrgRolePicker } from './OrgRolePicker'; import { contextSrv } from 'app/core/core'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; +import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; interface Props { orgs: UserOrg[]; @@ -133,8 +134,25 @@ class UnThemedOrgRow extends PureComponent { state = { currentRole: this.props.org.role, isChangingRole: false, + roleOptions: [], + builtInRoles: {}, }; + componentDidMount() { + if (contextSrv.licensedAccessControlEnabled()) { + if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { + fetchRoleOptions(this.props.org.orgId) + .then((roles) => this.setState({ roleOptions: roles })) + .catch((e) => console.error(e)); + } + if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) { + fetchRoleOptions(this.props.org.orgId) + .then((roles) => this.setState({ builtInRoles: roles })) + .catch((e) => console.error(e)); + } + } + } + onOrgRemove = () => { const { org } = this.props; this.props.onOrgRemove(org.orgId); @@ -176,7 +194,7 @@ class UnThemedOrgRow extends PureComponent { - {contextSrv.accessControlEnabled() ? ( + {contextSrv.licensedAccessControlEnabled() ? (
@@ -184,6 +202,8 @@ class UnThemedOrgRow extends PureComponent { userId={user?.id || 0} orgId={org.orgId} builtInRole={org.role} + roleOptions={this.state.roleOptions} + builtInRoles={this.state.builtInRoles} onBuiltinRoleChange={this.onBuiltinRoleChange} builtinRolesDisabled={rolePickerDisabled} /> diff --git a/public/app/features/alerting/unified/api/alertmanager.ts b/public/app/features/alerting/unified/api/alertmanager.ts index 0ea54d1121b..25cd0320f56 100644 --- a/public/app/features/alerting/unified/api/alertmanager.ts +++ b/public/app/features/alerting/unified/api/alertmanager.ts @@ -1,22 +1,22 @@ -import { lastValueFrom } from 'rxjs'; import { urlUtil } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; - import { AlertmanagerAlert, AlertManagerCortexConfig, AlertmanagerGroup, + AlertmanagerStatus, + ExternalAlertmanagersResponse, + Matcher, + Receiver, Silence, SilenceCreatePayload, - Matcher, - AlertmanagerStatus, - Receiver, + TestReceiversAlert, TestReceiversPayload, TestReceiversResult, - TestReceiversAlert, - ExternalAlertmanagersResponse, } from 'app/plugins/datasource/alertmanager/types'; +import { lastValueFrom } from 'rxjs'; import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { isFetchError } from '../utils/alertmanager'; // "grafana" for grafana-managed, otherwise a datasource name export async function fetchAlertManagerConfig(alertManagerSourceName: string): Promise { @@ -171,28 +171,55 @@ export async function testReceivers( receivers, alert, }; - const result = await lastValueFrom( - getBackendSrv().fetch({ - method: 'POST', - data, - url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/receivers/test`, - showErrorAlert: false, - showSuccessAlert: false, - }) - ); + try { + const result = await lastValueFrom( + getBackendSrv().fetch({ + method: 'POST', + data, + url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/receivers/test`, + showErrorAlert: false, + showSuccessAlert: false, + }) + ); - // api returns 207 if one or more receivers has failed test. Collect errors in this case - if (result.status === 207) { - throw new Error( - result.data.receivers - .flatMap((receiver) => - receiver.grafana_managed_receiver_configs - .filter((receiver) => receiver.status === 'failed') - .map((receiver) => receiver.error ?? 'Unknown error.') - ) - .join('; ') + if (receiversResponseContainsErrors(result.data)) { + throw new Error(getReceiverResultError(result.data)); + } + } catch (error) { + if (isFetchError(error) && isTestReceiversResult(error.data) && receiversResponseContainsErrors(error.data)) { + throw new Error(getReceiverResultError(error.data)); + } + + throw error; + } +} + +function receiversResponseContainsErrors(result: TestReceiversResult) { + return result.receivers.some((receiver) => + receiver.grafana_managed_receiver_configs.some((config) => config.status === 'failed') + ); +} + +function isTestReceiversResult(data: any): data is TestReceiversResult { + const receivers = data?.receivers; + + if (Array.isArray(receivers)) { + return receivers.every( + (receiver: any) => typeof receiver.name === 'string' && Array.isArray(receiver.grafana_managed_receiver_configs) ); } + + return false; +} + +function getReceiverResultError(receiversResult: TestReceiversResult) { + return receiversResult.receivers + .flatMap((receiver) => + receiver.grafana_managed_receiver_configs + .filter((receiver) => receiver.status === 'failed') + .map((receiver) => receiver.error ?? 'Unknown error.') + ) + .join('; '); } export async function addAlertManagers(alertManagers: string[]): Promise { diff --git a/public/app/features/alerting/unified/api/ruler.ts b/public/app/features/alerting/unified/api/ruler.ts index 8def1d17da4..ac3edeacf1c 100644 --- a/public/app/features/alerting/unified/api/ruler.ts +++ b/public/app/features/alerting/unified/api/ruler.ts @@ -107,15 +107,8 @@ async function rulerGetRequest(url: string, empty: T, params?: Record) { return ( - error.status === 500 && - error.data.message?.includes('unexpected content type from upstream. expected YAML, got text/html') + error.status === 404 || + (error.status === 500 && + error.data.message?.includes('unexpected content type from upstream. expected YAML, got text/html')) ); } function isCortexErrorResponse(error: FetchResponse) { - return error.data.message?.includes('group does not exist') || error.data.message?.includes('no rule groups found'); + return ( + error.status === 404 && + (error.data.message?.includes('group does not exist') || error.data.message?.includes('no rule groups found')) + ); } export async function deleteNamespace(dataSourceName: string, namespace: string): Promise { diff --git a/public/app/features/alerting/unified/components/DynamicTable.tsx b/public/app/features/alerting/unified/components/DynamicTable.tsx index 9beee9941c1..5ff067820a2 100644 --- a/public/app/features/alerting/unified/components/DynamicTable.tsx +++ b/public/app/features/alerting/unified/components/DynamicTable.tsx @@ -91,7 +91,7 @@ export const DynamicTable = ({ {items.map((item, index) => { const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id); return ( -
+
{renderPrefixCell && renderPrefixCell(item, index, items)} {isExpandable && (
diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts index af54659548d..a7fcee29adf 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts @@ -1,7 +1,7 @@ import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types'; import { FormAmRoute } from '../../types/amroutes'; import { MatcherFieldValue } from '../../types/silence-form'; -import { getFilteredRoutes } from './AmRoutesTable'; +import { deleteRoute, getFilteredRoutes, updatedRoute } from './AmRoutesTable'; const defaultAmRoute: FormAmRoute = { id: '', @@ -110,3 +110,79 @@ describe('getFilteredRoutes', () => { expect(filteredRoutes).toContain(routes[2]); }); }); + +describe('updatedRoute', () => { + it('Should update an item of the same id', () => { + // Arrange + const routes: FormAmRoute[] = [buildAmRoute({ id: '1' }), buildAmRoute({ id: '2' }), buildAmRoute({ id: '3' })]; + + const routeUpdate: FormAmRoute = { + ...routes[1], + object_matchers: [buildMatcher('severity', 'critical', MatcherOperator.equal)], + }; + + // Act + const updatedRoutes = updatedRoute(routes, routeUpdate); + + // Assert + expect(updatedRoutes).toHaveLength(3); + const changedRoute = updatedRoutes[1]; + + expect(changedRoute.object_matchers).toHaveLength(1); + expect(changedRoute.object_matchers[0].name).toBe('severity'); + expect(changedRoute.object_matchers[0].value).toBe('critical'); + expect(changedRoute.object_matchers[0].operator).toBe(MatcherOperator.equal); + }); + + it('Should not update any element when an element of matching id not found', () => { + // Arrange + const routes: FormAmRoute[] = [buildAmRoute({ id: '1' }), buildAmRoute({ id: '2' }), buildAmRoute({ id: '3' })]; + + const routeUpdate: FormAmRoute = { + ...routes[1], + id: '-1', + object_matchers: [buildMatcher('severity', 'critical', MatcherOperator.equal)], + }; + + // Act + const updatedRoutes = updatedRoute(routes, routeUpdate); + + // Assert + expect(updatedRoutes).toHaveLength(3); + + updatedRoutes.forEach((route) => { + expect(route.object_matchers).toHaveLength(0); + }); + }); +}); + +describe('deleteRoute', () => { + it('Should delete an element of the same id', () => { + // Arrange + const routes: FormAmRoute[] = [buildAmRoute({ id: '1' }), buildAmRoute({ id: '2' }), buildAmRoute({ id: '3' })]; + + const routeToDelete = routes[1]; + + // Act + const updatedRoutes = deleteRoute(routes, routeToDelete); + + // Assert + expect(updatedRoutes).toHaveLength(2); + expect(updatedRoutes[0].id).toBe('1'); + expect(updatedRoutes[1].id).toBe('3'); + }); + + it('Should not delete anything when an element of matching id not found', () => { + // Arrange + const routes: FormAmRoute[] = [buildAmRoute({ id: '1' }), buildAmRoute({ id: '2' }), buildAmRoute({ id: '3' })]; + + // Act + const updatedRoutes = deleteRoute(routes, buildAmRoute({ id: '-1' })); + + // Assert + expect(updatedRoutes).toHaveLength(3); + expect(updatedRoutes[0].id).toBe('1'); + expect(updatedRoutes[1].id).toBe('2'); + expect(updatedRoutes[2].id).toBe('3'); + }); +}); diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx index b76807394e8..e5cb1df5d46 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx @@ -44,6 +44,23 @@ export const getFilteredRoutes = (routes: FormAmRoute[], labelMatcherQuery?: str return filteredRoutes; }; +export const updatedRoute = (routes: FormAmRoute[], updatedRoute: FormAmRoute): FormAmRoute[] => { + const newRoutes = [...routes]; + const editIndex = newRoutes.findIndex((route) => route.id === updatedRoute.id); + + if (editIndex >= 0) { + newRoutes[editIndex] = { + ...newRoutes[editIndex], + ...updatedRoute, + }; + } + return newRoutes; +}; + +export const deleteRoute = (routes: FormAmRoute[], routeToRemove: FormAmRoute): FormAmRoute[] => { + return routes.filter((route) => route.id !== routeToRemove.id); +}; + export const AmRoutesTable: FC = ({ isAddMode, onCancelAdd, @@ -92,7 +109,7 @@ export const AmRoutesTable: FC = ({ id: 'actions', label: 'Actions', // eslint-disable-next-line react/display-name - renderCell: (item, index) => { + renderCell: (item) => { if (item.renderExpandedContent) { return null; } @@ -118,10 +135,7 @@ export const AmRoutesTable: FC = ({ aria-label="Delete route" name="trash-alt" onClick={() => { - const newRoutes = [...routes]; - - newRoutes.splice(index, 1); - + const newRoutes = deleteRoute(routes, item.data); onChange(newRoutes); }} type="button" @@ -144,11 +158,14 @@ export const AmRoutesTable: FC = ({ [isAddMode, routes, filteredRoutes] ); - // expand the last item when adding + // expand the last item when adding or reset when the length changed useEffect(() => { if (isAddMode && dynamicTableRoutes.length) { setExpandedId(dynamicTableRoutes[dynamicTableRoutes.length - 1].id); } + if (!isAddMode && dynamicTableRoutes.length) { + setExpandedId(undefined); + } }, [isAddMode, dynamicTableRoutes]); if (routes.length > 0 && filteredRoutes.length === 0) { @@ -168,7 +185,7 @@ export const AmRoutesTable: FC = ({ onCollapse={collapseItem} onExpand={expandItem} isExpanded={(item) => expandedId === item.id} - renderExpandedContent={(item: RouteTableItemProps, index) => + renderExpandedContent={(item: RouteTableItemProps) => isAddMode || editMode ? ( { @@ -178,12 +195,8 @@ export const AmRoutesTable: FC = ({ setEditMode(false); }} onSave={(data) => { - const newRoutes = [...routes]; + const newRoutes = updatedRoute(routes, data); - newRoutes[index] = { - ...newRoutes[index], - ...data, - }; setEditMode(false); onChange(newRoutes); }} @@ -193,13 +206,7 @@ export const AmRoutesTable: FC = ({ ) : ( { - const newRoutes = [...routes]; - - newRoutes[index] = { - ...item.data, - ...data, - }; - + const newRoutes = updatedRoute(routes, data); onChange(newRoutes); }} receivers={receivers} diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx index e2065b510aa..4a96553d037 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx @@ -5,10 +5,11 @@ import { css } from '@emotion/css'; import { RuleEditorSection } from './RuleEditorSection'; import { useFormContext } from 'react-hook-form'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; -import { RuleFolderPicker } from './RuleFolderPicker'; +import { Folder, RuleFolderPicker } from './RuleFolderPicker'; import { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; import { contextSrv } from 'app/core/services/context_srv'; import { CloudRulesSourcePicker } from './CloudRulesSourcePicker'; +import { checkForPathSeparator } from './util'; interface Props { editingExistingRule: boolean; @@ -72,6 +73,16 @@ export const AlertTypeStep: FC = ({ editingExistingRule }) => { {...register('name', { required: { value: true, message: 'Must enter an alert name' }, pattern: ruleFormType === RuleFormType.cloudRecording ? recordingRuleNameValidationPattern : undefined, + validate: { + pathSeparator: (value: string) => { + // we use the alert rule name as the "groupname" for Grafana managed alerts, so we can't allow path separators + if (ruleFormType === RuleFormType.grafana) { + return checkForPathSeparator(value); + } + + return true; + }, + }, })} autoFocus={true} /> @@ -148,6 +159,9 @@ export const AlertTypeStep: FC = ({ editingExistingRule }) => { name="folder" rules={{ required: { value: true, message: 'Please select a folder' }, + validate: { + pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title), + }, }} /> diff --git a/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx b/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx index a4b5022e6b9..16cb1fa0ac4 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx @@ -8,6 +8,7 @@ import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { SelectWithAdd } from './SelectWIthAdd'; import { Field, InputControl, useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; +import { checkForPathSeparator } from './util'; interface Props { rulesSourceName: string; @@ -75,6 +76,9 @@ export const GroupAndNamespaceFields: FC = ({ rulesSourceName }) => { control={control} rules={{ required: { value: true, message: 'Required.' }, + validate: { + pathSeparator: checkForPathSeparator, + }, }} /> @@ -87,6 +91,9 @@ export const GroupAndNamespaceFields: FC = ({ rulesSourceName }) => { control={control} rules={{ required: { value: true, message: 'Required.' }, + validate: { + pathSeparator: checkForPathSeparator, + }, }} /> diff --git a/public/app/features/alerting/unified/components/rule-editor/util.test.ts b/public/app/features/alerting/unified/components/rule-editor/util.test.ts index 7b62497f9c8..33643e09b65 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.test.ts @@ -1,6 +1,6 @@ import { ClassicCondition, ExpressionQuery } from 'app/features/expressions/types'; import { AlertQuery } from 'app/types/unified-alerting-dto'; -import { queriesWithUpdatedReferences, updateMathExpressionRefs } from './util'; +import { checkForPathSeparator, queriesWithUpdatedReferences, updateMathExpressionRefs } from './util'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; describe('rule-editor', () => { @@ -191,3 +191,17 @@ describe('rule-editor', () => { }); }); }); + +describe('checkForPathSeparator', () => { + it('should not allow strings with /', () => { + expect(checkForPathSeparator('foo / bar')).not.toBe(true); + expect(typeof checkForPathSeparator('foo / bar')).toBe('string'); + }); + it('should not allow strings with \\', () => { + expect(checkForPathSeparator('foo \\ bar')).not.toBe(true); + expect(typeof checkForPathSeparator('foo \\ bar')).toBe('string'); + }); + it('should allow anything without / or \\', () => { + expect(checkForPathSeparator('foo bar')).toBe(true); + }); +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts index 04e488b2d1f..a6921c956f6 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -1,5 +1,6 @@ import { isExpressionQuery } from 'app/features/expressions/guards'; import { AlertQuery } from 'app/types/unified-alerting-dto'; +import { ValidateResult } from 'react-hook-form'; export function queriesWithUpdatedReferences( queries: AlertQuery[], @@ -64,3 +65,16 @@ export function updateMathExpressionRefs(expression: string, previousRefId: stri return expression.replace(oldExpression, newExpression); } + +// some gateways (like Istio) will decode "/" and "\" characters – this will cause 404 errors for any API call +// that includes these values in the URL (ie. /my/path%2fto/resource -> /my/path/to/resource) +// +// see https://istio.io/latest/docs/ops/best-practices/security/#customize-your-system-on-path-normalization +export function checkForPathSeparator(value: string): ValidateResult { + const containsPathSeparator = value.includes('/') || value.includes('\\'); + if (containsPathSeparator) { + return 'Cannot contain "/" or "\\" characters'; + } + + return true; +} diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 048717794bf..ead22ca396c 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -53,7 +53,7 @@ import { isVanillaPrometheusAlertManagerDataSource, } from '../utils/datasource'; import { makeAMLink, retryWhile } from '../utils/misc'; -import { isFetchError, withAppEvents, withSerializedError } from '../utils/redux'; +import { withAppEvents, withSerializedError } from '../utils/redux'; import { formValuesToRulerRuleDTO, formValuesToRulerGrafanaRuleDTO } from '../utils/rule-form'; import { isCloudRuleIdentifier, @@ -62,10 +62,11 @@ import { isPrometheusRuleIdentifier, isRulerNotSupportedResponse, } from '../utils/rules'; -import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager'; +import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute, isFetchError } from '../utils/alertmanager'; import * as ruleId from '../utils/rule-id'; import { isEmpty } from 'lodash'; import messageFromError from 'app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError'; +import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants'; const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000; @@ -636,7 +637,8 @@ export const checkIfLotexSupportsEditingRulesAction = createAsyncThunk { }); }); - it('should parse escaped values correctly', () => { - expect(parseMatcher('foo=~"bar\\"baz\\""')).toEqual({ + // Alertmanager has some strict requirements for label values; + // we should not automatically encode or decode any values sent + // and instead let AM return any errors like (matcher value contains unescaped double quote: bar"baz") + // and allow the user to update the values to the correct format + // + // see https://github.com/prometheus/alertmanager/blob/4030e3670b359b8814aa8340ea1144f32b1f5ab3/pkg/labels/parse.go#L55-L99 + // and https://github.com/prometheus/alertmanager/blob/4030e3670b359b8814aa8340ea1144f32b1f5ab3/pkg/labels/parse.go#L101-L178 + it('should not parse escaped values', () => { + expect(parseMatcher('foo="^[a-z0-9-]{1}[a-z0-9-]{0,30}$"')).toEqual({ name: 'foo', - value: 'bar"baz"', - isRegex: true, + value: '"^[a-z0-9-]{1}[a-z0-9-]{0,30}$"', + isRegex: false, isEqual: true, }); expect(parseMatcher('foo=~bar\\"baz\\"')).toEqual({ name: 'foo', - value: 'bar"baz"', + value: 'bar\\"baz\\"', isRegex: true, isEqual: true, }); diff --git a/public/app/features/alerting/unified/utils/alertmanager.ts b/public/app/features/alerting/unified/utils/alertmanager.ts index 4b2c27ad119..cfa9a963f44 100644 --- a/public/app/features/alerting/unified/utils/alertmanager.ts +++ b/public/app/features/alerting/unified/utils/alertmanager.ts @@ -11,6 +11,7 @@ import { MatcherFieldValue } from '../types/silence-form'; import { SelectableValue } from '@grafana/data'; import { getAllDataSources } from './config'; import { DataSourceType } from './datasource'; +import { FetchError } from '@grafana/runtime'; export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig { // add default receiver if it does not exist @@ -117,14 +118,6 @@ const matcherOperators = [ MatcherOperator.equal, ]; -function unescapeMatcherValue(value: string) { - let trimmed = value.trim().replace(/\\"/g, '"'); - if (trimmed.startsWith('"') && trimmed.endsWith('"') && !trimmed.endsWith('\\"')) { - trimmed = trimmed.substr(1, trimmed.length - 2); - } - return trimmed.replace(/\\"/g, '"'); -} - export function parseMatcher(matcher: string): Matcher { const trimmed = matcher.trim(); if (trimmed.startsWith('{') && trimmed.endsWith('}')) { @@ -140,7 +133,7 @@ export function parseMatcher(matcher: string): Matcher { } const [operator, idx] = operatorsFound[0]; const name = trimmed.substr(0, idx).trim(); - const value = unescapeMatcherValue(trimmed.substr(idx + operator.length).trim()); + const value = trimmed.substr(idx + operator.length).trim(); if (!name) { throw new Error(`Invalid matcher: ${trimmed}`); } @@ -254,3 +247,7 @@ export function getMonthsString(months?: string[]): string { export function getYearsString(years?: string[]): string { return 'Years: ' + (years?.join(', ') ?? 'All'); } + +export function isFetchError(e: unknown): e is FetchError { + return typeof e === 'object' && e !== null && 'status' in e && 'data' in e; +} diff --git a/public/app/features/alerting/unified/utils/redux.ts b/public/app/features/alerting/unified/utils/redux.ts index 4bc6cb1819f..39a3d2ed992 100644 --- a/public/app/features/alerting/unified/utils/redux.ts +++ b/public/app/features/alerting/unified/utils/redux.ts @@ -4,6 +4,7 @@ import { FetchError } from '@grafana/runtime'; import { AppEvents } from '@grafana/data'; import { appEvents } from 'app/core/core'; +import { isFetchError } from './alertmanager'; export interface AsyncRequestState { result?: T; @@ -138,10 +139,6 @@ export function withAppEvents( }); } -export function isFetchError(e: unknown): e is FetchError { - return typeof e === 'object' && e !== null && 'status' in e && 'data' in e; -} - export function messageFromError(e: Error | FetchError | SerializedError): string { if (isFetchError(e)) { if (e.data?.message) { @@ -161,3 +158,19 @@ export function messageFromError(e: Error | FetchError | SerializedError): strin } return (e as Error)?.message || String(e); } + +export function isAsyncRequestMapSliceFulfilled(slice: AsyncRequestMapSlice): boolean { + return Object.values(slice).every(isAsyncRequestStateFulfilled); +} + +export function isAsyncRequestStateFulfilled(state: AsyncRequestState): boolean { + return state.dispatched && !state.loading && !state.error; +} + +export function isAsyncRequestMapSlicePending(slice: AsyncRequestMapSlice): boolean { + return Object.values(slice).some(isAsyncRequestStatePending); +} + +export function isAsyncRequestStatePending(state: AsyncRequestState): boolean { + return state.dispatched && state.loading; +} diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index b708cd53dfe..8896e91d561 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -50,7 +50,7 @@ export function alertInstanceKey(alert: Alert): string { } export function isRulerNotSupportedResponse(resp: AsyncRequestState) { - return resp.error && resp.error?.message === RULER_NOT_SUPPORTED_MSG; + return resp.error && resp.error?.message?.includes(RULER_NOT_SUPPORTED_MSG); } export function isGrafanaRuleIdentifier(identifier: RuleIdentifier): identifier is GrafanaRuleIdentifier { diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx index 85029438a59..24be2e0fd98 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { QueryGroup } from 'app/features/query/components/QueryGroup'; import { PanelModel } from '../../state'; -import { getLocationSrv } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { QueryGroupDataSource, QueryGroupOptions } from 'app/types'; import { DataQuery } from '@grafana/data'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -47,9 +47,9 @@ export class PanelEditorQueries extends PureComponent { }; onOpenQueryInspector = () => { - getLocationSrv().update({ - query: { inspect: this.props.panel.id, inspectTab: 'query' }, - partial: true, + locationService.partial({ + inspect: this.props.panel.id, + inspectTab: 'query', }); }; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index 1c04cfc2473..8c903e3fbfa 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -41,7 +41,7 @@ export const PanelEditorTabs: FC = React.memo(({ panel, da {tabs.map((tab) => { if (tab.id === PanelEditorTabId.Alert) { - renderAlertTab(tab, panel, dashboard, onChangeTab); + return renderAlertTab(tab, panel, dashboard, onChangeTab); } return ( void ) { - if (!config.alertingEnabled || !config.unifiedAlertingEnabled) { + const alertingDisabled = !config.alertingEnabled && !config.unifiedAlertingEnabled; + + if (alertingDisabled) { return null; - } else if (config.unifiedAlertingEnabled) { + } + + if (config.unifiedAlertingEnabled) { return ( ); - } else if (config.alertingEnabled) { + } + + if (config.alertingEnabled) { return ( { * Open the Panel Inspector when we click on an error */ onClickError = () => { - getLocationSrv().update({ partial: true, query: { inspect: this.props.panel.id, inspectTab: InspectTab.Error } }); + locationService.partial({ + inspect: this.props.panel.id, + inspectTab: InspectTab.Error, + }); }; renderCornerType(infoMode: InfoMode, content: PopoverContent, onClick?: () => void) { diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index 25c88ae6935..15be676a44f 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -6,9 +6,11 @@ import { AccessControlAction } from 'app/types'; import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import { GenericDataSourcePlugin } from '../settings/PluginSettings'; +const loadingDSType = 'Loading'; + export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDataSourcePlugin): NavModelItem { const pluginMeta = plugin.meta; - + const highlightsEnabled = config.featureToggles.featureHighlights; const navModel: NavModelItem = { img: pluginMeta.info.logos.large, id: 'datasource-' + dataSource.uid, @@ -49,23 +51,25 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat }); } + const isLoadingNav = dataSource.type === loadingDSType; + const dsPermissions = { active: false, icon: 'lock', - id: `datasource-permissions-${dataSource.id}`, + id: `datasource-permissions-${dataSource.uid}`, text: 'Permissions', - url: `datasources/edit/${dataSource.id}/permissions`, + url: `datasources/edit/${dataSource.uid}/permissions`, }; if (featureEnabled('dspermissions')) { if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) { navModel.children!.push(dsPermissions); } - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled && !isLoadingNav) { navModel.children!.push({ ...dsPermissions, url: dsPermissions.url + '/upgrade', - tabSuffix: ProBadge, + tabSuffix: () => ProBadge({ experimentId: 'feature-highlights-data-source-permissions-badge' }), }); } @@ -79,11 +83,11 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat if (featureEnabled('analytics')) { navModel.children!.push(analytics); - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled && !isLoadingNav) { navModel.children!.push({ ...analytics, url: analytics.url + '/upgrade', - tabSuffix: ProBadge, + tabSuffix: () => ProBadge({ experimentId: 'feature-highlights-data-source-insights-badge' }), }); } @@ -98,11 +102,11 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat if (featureEnabled('caching')) { navModel.children!.push(caching); - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled && !isLoadingNav) { navModel.children!.push({ ...caching, url: caching.url + '/upgrade', - tabSuffix: ProBadge, + tabSuffix: () => ProBadge({ experimentId: 'feature-highlights-query-caching-badge' }), }); } @@ -110,7 +114,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat } export function getDataSourceNav(main: NavModelItem, pageName: string): NavModel { - let node: NavModelItem; + let node: NavModelItem = { text: '' }; // find active page for (const child of main.children!) { @@ -143,8 +147,8 @@ export function getDataSourceLoadingNav(pageName: string): NavModel { orgId: 1, password: '', readOnly: false, - type: 'Loading', - typeName: 'Loading', + type: loadingDSType, + typeName: loadingDSType, typeLogoUrl: 'public/img/icn-datasource.svg', url: '', user: '', diff --git a/public/app/features/dimensions/editors/ResourcePickerPopover.tsx b/public/app/features/dimensions/editors/ResourcePickerPopover.tsx index 29e25dd9cef..c38df5788f3 100644 --- a/public/app/features/dimensions/editors/ResourcePickerPopover.tsx +++ b/public/app/features/dimensions/editors/ResourcePickerPopover.tsx @@ -3,6 +3,7 @@ import { css } from '@emotion/css'; import { Button, ButtonGroup, useStyles2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; import { FocusScope } from '@react-aria/focus'; +import { useDialog } from '@react-aria/dialog'; import { useOverlay } from '@react-aria/overlays'; import { MediaType, PickerTabType, ResourceFolderName } from '../types'; @@ -25,6 +26,7 @@ export const ResourcePickerPopover = (props: Props) => { }; const ref = createRef(); + const { dialogProps } = useDialog({}, ref); const { overlayProps } = useOverlay({ onClose, isDismissable: true, isOpen: true }, ref); const [newValue, setNewValue] = useState(value ?? ''); @@ -59,7 +61,7 @@ export const ResourcePickerPopover = (props: Props) => { return ( -
+
+ ) : ( + message + )} + + ); +} + export function LogsVolumePanel(props: Props) { const { width, logsVolumeData, absoluteRange, timeZone, splitOpen, onUpdateTimeRange, onLoadLogsVolume } = props; const theme = useTheme2(); @@ -26,11 +55,7 @@ export function LogsVolumePanel(props: Props) { if (!logsVolumeData) { return null; } else if (logsVolumeData?.error) { - return ( - - Please check console logs for more details. - - ); + return ; } else if (logsVolumeData?.state === LoadingState.Loading) { LogsVolumePanelContent = Log volume is loading...; } else if (logsVolumeData?.data) { diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx index 89518be6e2d..1c964abef98 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/Wrapper.test.tsx @@ -314,12 +314,20 @@ describe('Wrapper', () => { store.dispatch(splitOpen({ datasourceUid: 'elastic', query: { expr: 'error' } }) as any); await waitFor(() => expect(document.title).toEqual('Explore - loki | elastic - Grafana')); }); + + it('removes `from` and `to` parameters from url when first mounted', () => { + setup({ searchParams: 'from=1&to=2&orgId=1' }); + + expect(locationService.getSearchObject()).toEqual(expect.not.objectContaining({ from: '1', to: '2' })); + expect(locationService.getSearchObject()).toEqual(expect.objectContaining({ orgId: '1' })); + }); }); type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; type SetupOptions = { datasources?: DatasourceSetup[]; query?: any; + searchParams?: string; }; function setup(options?: SetupOptions): { datasources: { [name: string]: DataSourceApi }; store: EnhancedStore } { @@ -368,7 +376,7 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou }, }; - locationService.push({ pathname: '/explore' }); + locationService.push({ pathname: '/explore', search: options?.searchParams }); if (options?.query) { locationService.partial(options.query); diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index abc725c59fb..f38a2e6616e 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -10,6 +10,7 @@ import { Branding } from '../../core/components/Branding/Branding'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from 'app/types'; +import { locationService } from '@grafana/runtime'; interface RouteProps extends GrafanaRouteComponentProps<{}, ExploreQueryParams> {} interface OwnProps {} @@ -38,6 +39,20 @@ class WrapperUnconnected extends PureComponent { lastSavedUrl.left = undefined; lastSavedUrl.right = undefined; + // timeSrv (which is used internally) on init reads `from` and `to` param from the URL and updates itself + // using those value regardless of what is passed to the init method. + // The updated value is then used by Explore to get the range for each pane. + // This means that if `from` and `to` parameters are present in the URL, + // it would be impossible to change the time range in Explore. + // We are only doing this on mount for 2 reasons: + // 1: Doing it on update means we'll enter a render loop. + // 2: when parsing time in Explore (before feeding it to timeSrv) we make sure `from` is before `to` inside + // each pane state in order to not trigger un URL update from timeSrv. + const searchParams = locationService.getSearchObject(); + if (searchParams.from || searchParams.to) { + locationService.partial({ from: undefined, to: undefined }, true); + } + const richHistory = getRichHistory(); this.props.richHistoryUpdatedAction({ richHistory }); } diff --git a/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx b/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx index fb4b4aa9f81..c357de03032 100644 --- a/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx +++ b/public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx @@ -112,7 +112,6 @@ const getStyles = (theme: GrafanaTheme2) => { name: css` text-overflow: ellipsis; overflow: hidden; - white-space: nowrap; font-size: ${theme.typography.size.sm}; font-weight: ${theme.typography.fontWeightMedium}; width: 100%; @@ -120,7 +119,6 @@ const getStyles = (theme: GrafanaTheme2) => { description: css` text-overflow: ellipsis; overflow: hidden; - white-space: nowrap; color: ${theme.colors.text.secondary}; font-size: ${theme.typography.bodySmall.fontSize}; font-weight: ${theme.typography.fontWeightLight}; diff --git a/public/app/features/panel/state/actions.ts b/public/app/features/panel/state/actions.ts index 12b58f90903..84e12c00199 100644 --- a/public/app/features/panel/state/actions.ts +++ b/public/app/features/panel/state/actions.ts @@ -93,7 +93,7 @@ export function changeToLibraryPanel(panel: PanelModel, libraryPanel: LibraryEle ...libraryPanel.model, gridPos: panel.gridPos, id: panel.id, - libraryPanel: toPanelModelLibraryPanel(libraryPanel.model), + libraryPanel: toPanelModelLibraryPanel(libraryPanel), }); // a new library panel usually means new queries, clear any current result diff --git a/public/app/features/panel/state/util.ts b/public/app/features/panel/state/util.ts index 5db12bc1534..a12a748400a 100644 --- a/public/app/features/panel/state/util.ts +++ b/public/app/features/panel/state/util.ts @@ -1,4 +1,4 @@ -import { PanelPluginMeta, PluginState } from '@grafana/data'; +import { PanelPluginMeta, PluginState, unEscapeStringFromRegex } from '@grafana/data'; import { config } from 'app/core/config'; export function getAllPanelPluginMeta(): PanelPluginMeta[] { @@ -12,7 +12,7 @@ export function getAllPanelPluginMeta(): PanelPluginMeta[] { export function filterPluginList( pluginsList: PanelPluginMeta[], - searchQuery: string, + searchQuery: string, // Note: this will be an escaped regex string as it comes from `FilterInput` current: PanelPluginMeta ): PanelPluginMeta[] { if (!searchQuery.length) { @@ -24,7 +24,7 @@ export function filterPluginList( }); } - const query = searchQuery.toLowerCase(); + const query = unEscapeStringFromRegex(searchQuery).toLowerCase(); const first: PanelPluginMeta[] = []; const match: PanelPluginMeta[] = []; const isGraphQuery = 'graph'.startsWith(query); diff --git a/public/app/features/panel/state/utils.test.ts b/public/app/features/panel/state/utils.test.ts index bc664e0f572..6735aeb9a98 100644 --- a/public/app/features/panel/state/utils.test.ts +++ b/public/app/features/panel/state/utils.test.ts @@ -1,4 +1,4 @@ -import { PanelPluginMeta } from '@grafana/data'; +import { PanelPluginMeta, escapeStringForRegex } from '@grafana/data'; import { filterPluginList } from './util'; describe('panel state utils', () => { @@ -8,7 +8,18 @@ describe('panel state utils', () => { { id: 'timeseries', name: 'Graph (old)' }, { id: 'timeline', name: 'Timeline' }, ]; - const found = filterPluginList(pluginsList, 'gra', { id: 'xyz' } as any); + const found = filterPluginList(pluginsList, escapeStringForRegex('gra'), { id: 'xyz' } as any); expect(found.map((v) => v.id)).toEqual(['graph', 'timeseries']); }); + + it('should handle escaped regex characters in the search query (e.g. -)', async () => { + const pluginsList: PanelPluginMeta[] = [ + { id: 'graph', name: 'Graph (old)' } as any, + { id: 'timeseries', name: 'Graph (old)' }, + { id: 'timeline', name: 'Timeline' }, + { id: 'panelwithdashes', name: 'Panel-With-Dashes' }, + ]; + const found = filterPluginList(pluginsList, escapeStringForRegex('panel-'), { id: 'xyz' } as any); + expect(found.map((v) => v.id)).toEqual(['panelwithdashes']); + }); }); diff --git a/public/app/features/plugins/admin/hooks/useHistory.tsx b/public/app/features/plugins/admin/hooks/useHistory.tsx index a33f49298d3..5b1c501c39f 100644 --- a/public/app/features/plugins/admin/hooks/useHistory.tsx +++ b/public/app/features/plugins/admin/hooks/useHistory.tsx @@ -1,13 +1,9 @@ -import { getLocationSrv } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; export const useHistory = () => { return { push: ({ query }: any) => { - getLocationSrv().update({ - partial: true, - replace: false, - query, - }); + locationService.partial(query); }, }; }; diff --git a/public/app/features/query/components/QueryEditorRow.test.ts b/public/app/features/query/components/QueryEditorRow.test.ts index b754fa5710a..6117923d66c 100644 --- a/public/app/features/query/components/QueryEditorRow.test.ts +++ b/public/app/features/query/components/QueryEditorRow.test.ts @@ -54,10 +54,41 @@ describe('filterPanelDataToQuery', () => { const panelData = filterPanelDataToQuery(withError, 'B'); expect(panelData).toBeDefined(); + expect(panelData?.state).toBe(LoadingState.Error); + expect(panelData?.error).toBe(withError.error); + }); - // @ts-ignore typescript doesn't understand that panelData can't be undefined here - expect(panelData.state).toBe(LoadingState.Error); - // @ts-ignore typescript doesn't understand that panelData can't be undefined here - expect(panelData.error).toBe(withError.error); + it('should set the state to done if the frame has no errors', () => { + const withError = { + ...data, + }; + withError.state = LoadingState.Error; + + const panelDataB = filterPanelDataToQuery(withError, 'B'); + expect(panelDataB?.series.length).toBe(3); + expect(panelDataB?.series[0].refId).toBe('B'); + expect(panelDataB?.state).toBe(LoadingState.Error); + + const panelDataA = filterPanelDataToQuery(withError, 'A'); + expect(panelDataA?.series.length).toBe(1); + expect(panelDataA?.series[0].refId).toBe('A'); + expect(panelDataA?.state).toBe(LoadingState.Done); + }); + + it('should not set the state to done if the frame is loading and has no errors', () => { + const loadingData: PanelData = { + state: LoadingState.Loading, + series: [ + toDataFrame({ refId: 'A', fields: [{ name: 'AAA' }], meta: {} }), + toDataFrame({ refId: 'B', fields: [{ name: 'B111' }], meta: {} }), + ], + timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } }, + }; + + const panelDataB = filterPanelDataToQuery(loadingData, 'B'); + expect(panelDataB?.state).toBe(LoadingState.Loading); + + const panelDataA = filterPanelDataToQuery(loadingData, 'A'); + expect(panelDataA?.state).toBe(LoadingState.Loading); }); }); diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 5dab18c9886..fb7a32444c3 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -480,6 +480,8 @@ export function filterPanelDataToQuery(data: PanelData, refId: string): PanelDat const error = data.error && data.error.refId === refId ? data.error : undefined; if (error) { state = LoadingState.Error; + } else if (!error && data.state === LoadingState.Error) { + state = LoadingState.Done; } const timeRange = data.timeRange; diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 5786b0149e5..575569fc59c 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -11,8 +11,8 @@ import { PanelData, } from '@grafana/data'; import { QueryEditorRow } from './QueryEditorRow'; -import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; -import { getDataSourceSrv } from '@grafana/runtime'; +import { DragDropContext, DragStart, Droppable, DropResult } from 'react-beautiful-dnd'; +import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; interface Props { // The query configuration @@ -81,8 +81,18 @@ export class QueryEditorRows extends PureComponent { ); } + onDragStart = (result: DragStart) => { + const { queries, dsSettings } = this.props; + + reportInteraction('query_row_reorder_started', { + startIndex: result.source.index, + numberOfQueries: queries.length, + datasourceType: dsSettings.type, + }); + }; + onDragEnd = (result: DropResult) => { - const { queries, onQueriesChange } = this.props; + const { queries, onQueriesChange, dsSettings } = this.props; if (!result || !result.destination) { return; @@ -91,6 +101,12 @@ export class QueryEditorRows extends PureComponent { const startIndex = result.source.index; const endIndex = result.destination.index; if (startIndex === endIndex) { + reportInteraction('query_row_reorder_canceled', { + startIndex, + endIndex, + numberOfQueries: queries.length, + datasourceType: dsSettings.type, + }); return; } @@ -98,13 +114,20 @@ export class QueryEditorRows extends PureComponent { const [removed] = update.splice(startIndex, 1); update.splice(endIndex, 0, removed); onQueriesChange(update); + + reportInteraction('query_row_reorder_ended', { + startIndex, + endIndex, + numberOfQueries: queries.length, + datasourceType: dsSettings.type, + }); }; render() { const { dsSettings, data, queries, app, history, eventBus } = this.props; return ( - + {(provided) => { return ( diff --git a/public/app/features/search/components/ConfirmDeleteModal.tsx b/public/app/features/search/components/ConfirmDeleteModal.tsx index 34a9dad4722..16ba770452d 100644 --- a/public/app/features/search/components/ConfirmDeleteModal.tsx +++ b/public/app/features/search/components/ConfirmDeleteModal.tsx @@ -2,7 +2,7 @@ import React, { FC } from 'react'; import { css } from '@emotion/css'; import { GrafanaTheme } from '@grafana/data'; import { ConfirmModal, stylesFactory, useTheme } from '@grafana/ui'; -import { getLocationSrv } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { DashboardSection, OnDeleteItems } from '../types'; import { getCheckedUids } from '../utils'; import { deleteFoldersAndDashboards } from 'app/features/manage-dashboards/state/actions'; @@ -41,7 +41,7 @@ export const ConfirmDeleteModal: FC = ({ results, onDeleteItems, isOpen, deleteFoldersAndDashboards(folders, dashboards).then(() => { onDismiss(); // Redirect to /dashboard in case folder was deleted from f/:folder.uid - getLocationSrv().update({ path: '/dashboards' }); + locationService.push('/dashboards'); onDeleteItems(folders, dashboards); }); }; diff --git a/public/app/features/search/hooks/useDashboardSearch.ts b/public/app/features/search/hooks/useDashboardSearch.ts index 6b267d9b743..6fbcb220ede 100644 --- a/public/app/features/search/hooks/useDashboardSearch.ts +++ b/public/app/features/search/hooks/useDashboardSearch.ts @@ -1,5 +1,5 @@ import { KeyboardEvent, useReducer } from 'react'; -import { getLocationSrv } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { DashboardQuery, DashboardSearchItemType, DashboardSection } from '../types'; import { MOVE_SELECTION_DOWN, MOVE_SELECTION_UP } from '../reducers/actionTypes'; import { dashboardsSearchState, DashboardsSearchState, searchReducer } from '../reducers/dashboardSearch'; @@ -58,9 +58,7 @@ export const useDashboardSearch = (query: DashboardQuery, onCloseSearch: () => v if (selectedItem.type === DashboardSearchItemType.DashFolder) { onToggleSection(selectedItem as DashboardSection); } else { - getLocationSrv().update({ - path: locationUtil.stripBaseFromUrl(selectedItem.url), - }); + locationService.push(locationUtil.stripBaseFromUrl(selectedItem.url)); // Delay closing to prevent current page flicker setTimeout(onCloseSearch, 0); } diff --git a/public/app/features/serviceaccounts/ServiceAccountsTable.tsx b/public/app/features/serviceaccounts/ServiceAccountsTable.tsx index c602db7b1c0..9e35adef38e 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsTable.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsTable.tsx @@ -28,10 +28,15 @@ const ServiceAccountsTable: FC = (props) => { useEffect(() => { async function fetchOptions() { try { - let options = await fetchRoleOptions(orgId); - setRoleOptions(options); - const builtInRoles = await fetchBuiltinRoles(orgId); - setBuiltinRoles(builtInRoles); + if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { + let options = await fetchRoleOptions(orgId); + setRoleOptions(options); + } + + if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) { + const builtInRoles = await fetchBuiltinRoles(orgId); + setBuiltinRoles(builtInRoles); + } } catch (e) { console.error('Error loading options'); } @@ -41,9 +46,6 @@ const ServiceAccountsTable: FC = (props) => { } }, [orgId]); - const getRoleOptions = async () => roleOptions; - const getBuiltinRoles = async () => builtinRoles; - return ( <> @@ -89,8 +91,8 @@ const ServiceAccountsTable: FC = (props) => { orgId={orgId} builtInRole={serviceAccount.role} onBuiltinRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)} - getRoleOptions={getRoleOptions} - getBuiltinRoles={getBuiltinRoles} + roleOptions={roleOptions} + builtInRoles={builtinRoles} disabled={rolePickerDisabled} /> ) : ( diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index 77505494cce..47373c38d76 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -6,6 +6,7 @@ import { getBackendSrv, locationService } from '@grafana/runtime'; import { connect } from 'react-redux'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; +import { contextSrv } from 'app/core/core'; export interface Props { navModel: NavModel; @@ -20,6 +21,7 @@ export class CreateTeam extends PureComponent { create = async (formModel: TeamDTO) => { const result = await getBackendSrv().post('/api/teams', formModel); if (result.teamId) { + await contextSrv.fetchUserPermissions(); locationService.push(`/org/teams/edit/${result.teamId}`); } }; diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index f3deb62c77b..6ba6f050278 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -6,6 +6,7 @@ import { getMockTeamGroups } from './__mocks__/teamMocks'; const setup = (propOverrides?: object) => { const props: Props = { + isReadOnly: false, groups: [] as TeamGroup[], loadTeamGroups: jest.fn(), addTeamGroup: jest.fn(), diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index aeedfb12f16..a550b0cd9df 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -23,13 +23,17 @@ const mapDispatchToProps = { removeTeamGroup, }; +interface OwnProps { + isReadOnly: boolean; +} + interface State { isAdding: boolean; newGroupId: string; } const connector = connect(mapStateToProps, mapDispatchToProps); -export type Props = ConnectedProps; +export type Props = OwnProps & ConnectedProps; const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; @@ -70,11 +74,12 @@ export class TeamGroupSync extends PureComponent { } renderGroup(group: TeamGroup) { + const { isReadOnly } = this.props; return ( @@ -84,7 +89,7 @@ export class TeamGroupSync extends PureComponent { render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.groups; + const { groups, isReadOnly } = this.props; return (
@@ -95,7 +100,7 @@ export class TeamGroupSync extends PureComponent {
{groups.length > 0 && ( - )} @@ -113,11 +118,12 @@ export class TeamGroupSync extends PureComponent { value={newGroupId} onChange={this.onNewGroupIdChanged} placeholder="cn=ops,ou=groups,dc=grafana,dc=org" + disabled={isReadOnly} />
-
@@ -135,6 +141,7 @@ export class TeamGroupSync extends PureComponent { proTipLinkTitle="Learn more" proTipLink="http://docs.grafana.org/auth/enhanced_ldap/" proTipTarget="_blank" + buttonDisabled={isReadOnly} /> )} diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 27c6af5be0b..f044e4681d7 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -43,7 +43,7 @@ export class TeamList extends PureComponent { componentDidMount() { this.fetchTeams(); - if (contextSrv.accessControlEnabled()) { + if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { this.fetchRoleOptions(); } } @@ -69,29 +69,55 @@ export class TeamList extends PureComponent { const { editorsCanAdmin, signedInUser } = this.props; const permission = team.permission; const teamUrl = `org/teams/edit/${team.id}`; - const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }); + const isTeamAdmin = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }); + const canDelete = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsDelete, team, isTeamAdmin); + const canReadTeam = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRead, team, isTeamAdmin); + const canSeeTeamRoles = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRolesList, team, false); + const canUpdateTeamRoles = + contextSrv.hasAccess(AccessControlAction.ActionTeamsRolesAdd, false) || + contextSrv.hasAccess(AccessControlAction.ActionTeamsRolesRemove, false); + const displayRolePicker = + contextSrv.licensedAccessControlEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) && + contextSrv.hasPermission(AccessControlAction.ActionRolesList); return (
- {contextSrv.accessControlEnabled() && ( + {displayRolePicker && ( )} - {contextSrv.accessControlEnabled() && } + {displayRolePicker && } diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 8f5fa7ec666..c4f852b2bf6 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -13,6 +13,7 @@ jest.mock('@grafana/runtime/src/config', () => ({ licenseInfo: { enabledFeatures: { teamsync: true }, }, + featureToggles: { accesscontrol: false }, }, })); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 400dc0079c5..7da89e296be 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -4,16 +4,17 @@ import { includes } from 'lodash'; import config from 'app/core/config'; import Page from 'app/core/components/Page/Page'; import TeamMembers from './TeamMembers'; +import TeamPermissions from './TeamPermissions'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { StoreState } from 'app/types'; +import { AccessControlAction, StoreState } from 'app/types'; import { loadTeam, loadTeamMembers } from './state/actions'; import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors'; import { getTeamLoadingNav } from './state/navModel'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; import { NavModel } from '@grafana/data'; -import { featureEnabled } from '@grafana/runtime'; +import { featureEnabled, reportExperimentView } from '@grafana/runtime'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { UpgradeBox } from 'app/core/components/Upgrade/UpgradeBox'; @@ -37,10 +38,17 @@ enum PageTypes { function mapStateToProps(state: StoreState, props: OwnProps) { const teamId = parseInt(props.match.params.id, 10); - const pageName = props.match.params.page ?? 'members'; + const team = getTeam(state.team, teamId); + let defaultPage = 'members'; + if (contextSrv.accessControlEnabled()) { + // With FGAC the settings page will always be available + if (!team || !contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team)) { + defaultPage = 'settings'; + } + } + const pageName = props.match.params.page ?? defaultPage; const teamLoadingNav = getTeamLoadingNav(pageName as string); const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav); - const team = getTeam(state.team, teamId); const members = getTeamMembers(state.team); return { @@ -75,13 +83,23 @@ export class TeamPages extends PureComponent { async componentDidMount() { await this.fetchTeam(); + + const { isSyncEnabled } = this.state; + const currentPage = this.getCurrentPage(); + + if (currentPage === PageTypes.GroupSync && !isSyncEnabled && config.featureToggles.featureHighlights) { + reportExperimentView('feature-highlights-team-sync', 'test', ''); + } } async fetchTeam() { const { loadTeam, teamId } = this.props; this.setState({ isLoading: true }); const team = await loadTeam(teamId); - await this.props.loadTeamMembers(); + // With accesscontrol, the TeamPermissions will fetch team members + if (!contextSrv.accessControlEnabled()) { + await this.props.loadTeamMembers(); + } this.setState({ isLoading: false }); return team; } @@ -105,6 +123,10 @@ export class TeamPages extends PureComponent { }; hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => { + if (contextSrv.accessControlEnabled()) { + return navModel; + } + if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) { navModel.main.children .filter((navItem) => !this.textsAreEqual(navItem.text, PageTypes.Members)) @@ -121,16 +143,37 @@ export class TeamPages extends PureComponent { const { members, team } = this.props; const currentPage = this.getCurrentPage(); + const canReadTeam = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsRead, + team!, + isSignedInUserTeamAdmin + ); + const canReadTeamPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsPermissionsRead, + team!, + isSignedInUserTeamAdmin + ); + const canWriteTeamPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsPermissionsWrite, + team!, + isSignedInUserTeamAdmin + ); + switch (currentPage) { case PageTypes.Members: - return ; - + if (contextSrv.accessControlEnabled()) { + return ; + } else { + return ; + } case PageTypes.Settings: - return isSignedInUserTeamAdmin && ; + return canReadTeam && ; case PageTypes.GroupSync: - if (isSignedInUserTeamAdmin && isSyncEnabled) { - return ; - } else if (config.featureHighlights.enabled) { + if (isSyncEnabled) { + if (canReadTeamPermissions) { + return ; + } + } else if (config.featureToggles.featureHighlights) { return ( { + const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); + const canSetPermissions = contextSrv.hasPermissionInMetadata( + AccessControlAction.ActionTeamsPermissionsWrite, + props.team + ); + + return ( + + ); +}; + +export default TeamPermissions; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index b261116718e..4d4cfc0e83c 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -3,6 +3,12 @@ import { shallow } from 'enzyme'; import { Props, TeamSettings } from './TeamSettings'; import { getMockTeam } from './__mocks__/teamMocks'; +jest.mock('app/core/core', () => ({ + contextSrv: { + hasPermissionInMetadata: () => true, + }, +})); + const setup = (propOverrides?: object) => { const props: Props = { team: getMockTeam(), diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 8ef7dd288f5..ede17d8da8f 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -4,7 +4,8 @@ import { Input, Field, Form, Button, FieldSet, VerticalGroup } from '@grafana/ui import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; -import { Team } from 'app/types'; +import { AccessControlAction, Team } from 'app/types'; +import { contextSrv } from 'app/core/core'; const mapDispatchToProps = { updateTeam, @@ -18,6 +19,8 @@ interface OwnProps { export type Props = ConnectedProps & OwnProps; export const TeamSettings: FC = ({ team, updateTeam }) => { + const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team); + return (
@@ -26,25 +29,29 @@ export const TeamSettings: FC = ({ team, updateTeam }) => { onSubmit={(formTeam: Team) => { updateTeam(formTeam.name, formTeam.email); }} + disabled={!canWriteTeamSettings} > {({ register }) => ( <> - + - + )}
- +
); }; diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap index 35719d0908d..3abd26f7778 100644 --- a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -44,6 +44,7 @@ exports[`Render should render component 1`] = ` >
+
{group.groupId} -
- + {canReadTeam ? ( + + Team avatar + + ) : ( Team avatar - + )} - {team.name} + {canReadTeam ? {team.name} :
{team.name}
}
- 0 ? undefined : 'Empty email cell'}> - {team.email} - + {canReadTeam ? ( + 0 ? undefined : 'Empty email cell'}> + {team.email} + + ) : ( +
0 ? undefined : 'Empty email cell'}> + {team.email} +
+ )}
- {team.memberCount} + {canReadTeam ? ( + {team.memberCount} + ) : ( +
{team.memberCount}
+ )}
- this.state.roleOptions} /> + {canSeeTeamRoles && ( + + )} @@ -113,6 +139,7 @@ export class TeamList extends PureComponent { buttonIcon="users-alt" buttonLink="org/teams/new" buttonTitle=" New team" + buttonDisabled={!contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate)} proTip="Assign folder and dashboard permissions to teams instead of users to ease administration." proTipLink="" proTipLinkTitle="" @@ -130,6 +157,10 @@ export class TeamList extends PureComponent { const { teams, searchQuery, editorsCanAdmin, searchPage, setTeamsSearchPage } = this.props; const teamAdmin = contextSrv.hasRole('Admin') || (editorsCanAdmin && contextSrv.hasRole('Editor')); const canCreate = contextSrv.hasAccess(AccessControlAction.ActionTeamsCreate, teamAdmin); + const displayRolePicker = + contextSrv.licensedAccessControlEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) && + contextSrv.hasPermission(AccessControlAction.ActionRolesList); const newTeamHref = canCreate ? 'org/teams/new' : '#'; const paginatedTeams = this.getPaginatedTeams(teams); const totalPages = Math.ceil(teams.length / pageLimit); @@ -155,7 +186,7 @@ export class TeamList extends PureComponent { Name Email MembersRolesRoles
- test-1 - + - test-1@test.com - + - 1 - + - - Team avatar - + Team avatar - test-1 - + - test-1@test.com - + - 1 - + - + `; diff --git a/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap index f460cc9ead9..a7e227d3f80 100644 --- a/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap @@ -16,12 +16,14 @@ exports[`Render should render component 1`] = ` "permission": 0, } } + disabled={false} onSubmit={[Function]} > diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 13fe284e826..df695929dc1 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,20 +1,31 @@ import { getBackendSrv } from '@grafana/runtime'; -import { TeamMember, ThunkResult } from 'app/types'; +import { AccessControlAction, TeamMember, ThunkResult } from 'app/types'; import { updateNavIndex } from 'app/core/actions'; import { buildNavModel } from './navModel'; import { teamGroupsLoaded, teamLoaded, teamMembersLoaded, teamsLoaded } from './reducers'; +import { accessControlQueryParam } from 'app/core/utils/accessControl'; +import { contextSrv } from 'app/core/core'; export function loadTeams(): ThunkResult { return async (dispatch) => { - const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); + // Early return if the user cannot list teams + if (!contextSrv.hasPermission(AccessControlAction.ActionTeamsRead)) { + dispatch(teamsLoaded([])); + return; + } + + const response = await getBackendSrv().get( + '/api/teams/search', + accessControlQueryParam({ perpage: 1000, page: 1 }) + ); dispatch(teamsLoaded(response.teams)); }; } export function loadTeam(id: number): ThunkResult { return async (dispatch) => { - const response = await getBackendSrv().get(`/api/teams/${id}`); + const response = await getBackendSrv().get(`/api/teams/${id}`, accessControlQueryParam()); dispatch(teamLoaded(response)); dispatch(updateNavIndex(buildNavModel(response))); }; @@ -79,6 +90,8 @@ export function removeTeamGroup(groupId: string): ThunkResult { export function deleteTeam(id: number): ThunkResult { return async (dispatch) => { await getBackendSrv().delete(`/api/teams/${id}`); + // Update users permissions in case they lost teams.read with the deletion + await contextSrv.fetchUserPermissions(); dispatch(loadTeams()); }; } diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts index b8560c41127..9631b59f9b6 100644 --- a/public/app/features/teams/state/navModel.ts +++ b/public/app/features/teams/state/navModel.ts @@ -1,9 +1,19 @@ -import { Team, TeamPermissionLevel } from 'app/types'; +import { AccessControlAction, Team, TeamPermissionLevel } from 'app/types'; import { featureEnabled } from '@grafana/runtime'; import { NavModelItem, NavModel } from '@grafana/data'; import config from 'app/core/config'; +import { contextSrv } from 'app/core/services/context_srv'; import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; +const loadingTeam = { + avatarUrl: 'public/img/user_profile.png', + id: 1, + name: 'Loading', + email: 'loading', + memberCount: 0, + permission: TeamPermissionLevel.Member, +}; + export function buildNavModel(team: Team): NavModelItem { const navModel: NavModelItem = { img: team.avatarUrl, @@ -13,13 +23,8 @@ export function buildNavModel(team: Team): NavModelItem { text: team.name, breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], children: [ - { - active: false, - icon: 'users-alt', - id: `team-members-${team.id}`, - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, + // With FGAC this tab will always be available (but not always editable) + // With Legacy it will be hidden by hideTabsFromNonTeamAdmin should the user not be allowed to see it { active: false, icon: 'sliders-v-alt', @@ -30,6 +35,22 @@ export function buildNavModel(team: Team): NavModelItem { ], }; + // While team is loading we leave the members tab + // With FGAC the Members tab is available when user has ActionTeamsPermissionsRead for this team + // With Legacy it will always be present + if ( + team === loadingTeam || + contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team) + ) { + navModel.children!.unshift({ + active: false, + icon: 'users-alt', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }); + } + const teamGroupSync = { active: false, icon: 'sync', @@ -38,24 +59,27 @@ export function buildNavModel(team: Team): NavModelItem { url: `org/teams/edit/${team.id}/groupsync`, }; + const isLoadingTeam = team === loadingTeam; + + // With both Legacy and FGAC the tab is protected being featureEnabled + // While team is loading we leave the teamsync tab + // With FGAC the External Group Sync tab is available when user has ActionTeamsPermissionsRead for this team if (featureEnabled('teamsync')) { - navModel.children!.push(teamGroupSync); - } else if (config.featureHighlights.enabled) { - navModel.children!.push({ ...teamGroupSync, tabSuffix: ProBadge }); + if (isLoadingTeam || contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team)) { + navModel.children!.push(teamGroupSync); + } + } else if (config.featureToggles.featureHighlights) { + navModel.children!.push({ + ...teamGroupSync, + tabSuffix: () => ProBadge({ experimentId: isLoadingTeam ? '' : 'feature-highlights-team-sync-badge' }), + }); } return navModel; } export function getTeamLoadingNav(pageName: string): NavModel { - const main = buildNavModel({ - avatarUrl: 'public/img/user_profile.png', - id: 1, - name: 'Loading', - email: 'loading', - memberCount: 0, - permission: TeamPermissionLevel.Member, - }); + const main = buildNavModel(loadingTeam); let node: NavModelItem; diff --git a/public/app/features/users/UsersTable.test.tsx b/public/app/features/users/UsersTable.test.tsx index 68bba275f15..f71e0517eb7 100644 --- a/public/app/features/users/UsersTable.test.tsx +++ b/public/app/features/users/UsersTable.test.tsx @@ -9,7 +9,7 @@ jest.mock('app/core/core', () => ({ contextSrv: { hasPermission: () => true, hasPermissionInMetadata: () => true, - accessControlEnabled: () => false, + licensedAccessControlEnabled: () => false, }, })); diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index 427a32ef53a..c5c05f1faf2 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -26,28 +26,21 @@ const UsersTable: FC = (props) => { if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { let options = await fetchRoleOptions(orgId); setRoleOptions(options); - } else { - setRoleOptions([]); } if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) { const builtInRoles = await fetchBuiltinRoles(orgId); setBuiltinRoles(builtInRoles); - } else { - setBuiltinRoles({}); } } catch (e) { console.error('Error loading options'); } } - if (contextSrv.accessControlEnabled()) { + if (contextSrv.licensedAccessControlEnabled()) { fetchOptions(); } }, [orgId]); - const getRoleOptions = async () => roleOptions; - const getBuiltinRoles = async () => builtinRoles; - return ( <> @@ -88,14 +81,14 @@ const UsersTable: FC = (props) => { +
{user.lastSeenAtAge} - {contextSrv.accessControlEnabled() ? ( + {contextSrv.licensedAccessControlEnabled() ? ( onRoleChange(newRole, user)} - getRoleOptions={getRoleOptions} - getBuiltinRoles={getBuiltinRoles} + roleOptions={roleOptions} + builtInRoles={builtinRoles} disabled={!contextSrv.hasPermissionInMetadata(AccessControlAction.OrgUsersRoleUpdate, user)} /> ) : ( diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index 1ef41d32b79..ea3b649bb92 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -247,7 +247,7 @@ interface TestReceiversResultGrafanaReceiverConfig { name: string; uid?: string; error?: string; - status: 'failed'; + status: 'ok' | 'failed'; } interface TestReceiversResultReceiver { diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index ecf94527947..591e95dcee6 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -225,6 +225,36 @@ describe('datasource', () => { expect(response.data.length).toEqual(2); }); }); + + it('sets fields.config.interval based on period', async () => { + const { datasource } = setupMockedDataSource({ + data: { + results: { + a: { + refId: 'a', + series: [{ name: 'cpu', points: [1, 2], meta: { custom: { period: 60 } } }], + }, + b: { + refId: 'b', + series: [{ name: 'cpu', points: [1, 2], meta: { custom: { period: 120 } } }], + }, + }, + }, + }); + + const observable = datasource.performTimeSeriesQuery( + { + queries: [{ datasourceId: 1, refId: 'a' }], + } as any, + { from: dateTime(), to: dateTime() } as any + ); + + await expect(observable).toEmitValuesWith((received) => { + const response = received[0]; + expect(response.data[0].fields[0].config.interval).toEqual(60000); + expect(response.data[1].fields[0].config.interval).toEqual(120000); + }); + }); }); describe('describeLogGroup', () => { diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 53f13d523cd..3567dc58e9f 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -13,6 +13,7 @@ import { DataSourceInstanceSettings, DataSourceWithLogsContextSupport, dateMath, + FieldType, LoadingState, LogRowModel, rangeUtil, @@ -504,6 +505,15 @@ export class CloudWatchDatasource const lastError = findLast(res.results, (v) => !!v.error); + dataframes.forEach((frame) => { + frame.fields.forEach((field) => { + if (field.type === FieldType.time) { + // field.config.interval is populated in order for Grafana to fill in null values at frame intervals + field.config.interval = frame.meta?.custom?.period * 1000; + } + }); + }); + return { data: dataframes, error: lastError ? { message: lastError.error } : null, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts index 51a82ab43b9..54ac2692360 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts @@ -42,7 +42,7 @@ export default class ResourcePickerData extends DataSourceWithBackend; -export function LokiExploreQueryEditor(props: Props) { +export const LokiExploreQueryEditor = memo((props: Props) => { const { query, data, datasource, history, onChange, onRunQuery, range } = props; return ( @@ -23,6 +23,7 @@ export function LokiExploreQueryEditor(props: Props) { history={history} data={data} range={range} + data-testid={testIds.editor} ExtraFieldElement={ ); -} +}); -export default memo(LokiExploreQueryEditor); +LokiExploreQueryEditor.displayName = 'LokiExploreQueryEditor'; + +export const testIds = { + editor: 'loki-editor-explore', +}; diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx index 1bac9ffcfa0..65e2bdd558e 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx @@ -5,7 +5,8 @@ import { noop } from 'lodash'; import { LokiDatasource } from '../datasource'; import { testIds as alertingTestIds } from './LokiQueryEditorForAlerting'; import { testIds as regularTestIds } from './LokiQueryEditor'; -import LokiQueryEditorByApp from './LokiQueryEditorByApp'; +import { testIds as exploreTestIds } from './LokiExploreQueryEditor'; +import { LokiQueryEditorByApp } from './LokiQueryEditorByApp'; function setup(app: CoreApp): RenderResult { const dataSource = { @@ -43,10 +44,10 @@ describe('LokiQueryEditorByApp', () => { expect(queryByTestId(alertingTestIds.editor)).toBeNull(); }); - it('should render regular query editor for explore', () => { + it('should render expore query editor for explore', () => { const { getByTestId, queryByTestId } = setup(CoreApp.Explore); - expect(getByTestId(regularTestIds.editor)).toBeInTheDocument(); + expect(getByTestId(exploreTestIds.editor)).toBeInTheDocument(); expect(queryByTestId(alertingTestIds.editor)).toBeNull(); }); diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.tsx index 7dffe3c21d0..e49db26d3e1 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.tsx @@ -3,6 +3,7 @@ import { CoreApp } from '@grafana/data'; import { LokiQueryEditorProps } from './types'; import { LokiQueryEditor } from './LokiQueryEditor'; import { LokiQueryEditorForAlerting } from './LokiQueryEditorForAlerting'; +import { LokiExploreQueryEditor } from './LokiExploreQueryEditor'; import { LokiQueryEditorSelector } from '../querybuilder/components/LokiQueryEditorSelector'; import { config } from '@grafana/runtime'; @@ -12,6 +13,11 @@ export function LokiQueryEditorByApp(props: LokiQueryEditorProps) { switch (app) { case CoreApp.CloudAlerting: return ; + case CoreApp.Explore: + if (config.featureToggles.lokiQueryBuilder) { + return ; + } + return ; default: if (config.featureToggles.lokiQueryBuilder) { return ; diff --git a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap index c7c3761cb88..abb0e6dfa97 100644 --- a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap @@ -51,6 +51,7 @@ exports[`LokiExploreQueryEditor should render component 1`] = ` }, } } + data-testid="loki-editor-explore" datasource={ Object { "getTimeRangeParams": [Function], diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index da697baefa1..2c75f36b5da 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -10,7 +10,6 @@ import { FieldType, LogRowModel, MutableDataFrame, - TimeSeries, toUtc, } from '@grafana/data'; import { BackendSrvRequest, FetchResponse, config } from '@grafana/runtime'; @@ -334,7 +333,7 @@ describe('LokiDatasource', () => { expect(ds.runRangeQuery).toBeCalled(); }); - it('should return series data for metrics range queries', async () => { + it('should return dataframe data for metrics range queries', async () => { const ds = createLokiDSForTests(); const options = getQueryOptions({ targets: [{ expr: metricsQuery, refId: 'B', range: true }], @@ -345,11 +344,19 @@ describe('LokiDatasource', () => { await expect(ds.query(options)).toEmitValuesWith((received) => { const result = received[0]; - const timeSeries = result.data[0] as TimeSeries; + const frame = result.data[0] as DataFrame; - expect(timeSeries.meta?.preferredVisualisationType).toBe('graph'); - expect(timeSeries.refId).toBe('B'); - expect(timeSeries.datapoints[0]).toEqual([1.1, 1605715380000]); + expect(frame.meta?.preferredVisualisationType).toBe('graph'); + expect(frame.refId).toBe('B'); + frame.fields.forEach((field) => { + const value = field.values.get(0); + + if (field.type === FieldType.time) { + expect(value).toBe(1605715380000); + } else { + expect(value).toBe(1.1); + } + }); }); }); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index cc9aebcfeda..eebcb3c9ec1 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -70,12 +70,6 @@ export const DEFAULT_MAX_LINES = 1000; export const LOKI_ENDPOINT = '/loki/api/v1'; const NS_IN_MS = 1000000; -/** - * Loki's logs volume query may be expensive as it requires counting all logs in the selected range. If such query - * takes too much time it may need be made more specific to limit number of logs processed under the hood. - */ -const LOGS_VOLUME_TIMEOUT = 10000; - const RANGE_QUERY_ENDPOINT = `${LOKI_ENDPOINT}/query_range`; const INSTANT_QUERY_ENDPOINT = `${LOKI_ENDPOINT}/query`; @@ -150,7 +144,6 @@ export class LokiDatasource }); return queryLogsVolume(this, logsVolumeRequest, { - timeout: LOGS_VOLUME_TIMEOUT, extractLevel, range: request.range, targets: request.targets, diff --git a/public/app/plugins/datasource/loki/module.ts b/public/app/plugins/datasource/loki/module.ts index 524838580c7..6a6ad395313 100644 --- a/public/app/plugins/datasource/loki/module.ts +++ b/public/app/plugins/datasource/loki/module.ts @@ -9,6 +9,5 @@ import { ConfigEditor } from './configuration/ConfigEditor'; export const plugin = new DataSourcePlugin(Datasource) .setQueryEditor(LokiQueryEditorByApp) .setConfigEditor(ConfigEditor) - .setExploreQueryField(LokiQueryEditorByApp) .setQueryEditorHelp(LokiCheatSheet) .setAnnotationQueryCtrl(LokiAnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index b2fb3682d4e..d9d0e0b99b8 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -290,8 +290,6 @@ describe('enhanceDataFrame', () => { * NOTE on time parameters: * - Input time series data has timestamps in sec (like Prometheus) * - Output time series has timestamps in ms (as expected for the chart lib) - * - Start/end parameters are in ns (as expected for Loki) - * - Step is in sec (like in Prometheus) */ const data: Array<[number, string]> = [ [1, '1'], @@ -300,12 +298,10 @@ describe('enhanceDataFrame', () => { ]; it('returns data as is if step, start, and end align', () => { - const options: Partial = { start: 1 * 1e9, end: 4 * 1e9, step: 1 }; - const result = ResultTransformer.lokiPointsToTimeseriesPoints(data, options as TransformerOptions); + const result = ResultTransformer.lokiPointsToTimeseriesPoints(data); expect(result).toEqual([ [1, 1000], [0, 2000], - [null, 3000], [1, 4000], ]); }); diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 970cb1ddc45..ae7ef9ce46c 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -17,6 +17,7 @@ import { QueryResultMeta, TimeSeriesValue, ScopedVars, + toDataFrame, } from '@grafana/data'; import { getTemplateSrv, getDataSourceSrv } from '@grafana/runtime'; @@ -184,21 +185,16 @@ function lokiMatrixToTimeSeries(matrixResult: LokiMatrixResult, options: Transfo return { target: name, title: name, - datapoints: lokiPointsToTimeseriesPoints(matrixResult.values, options), + datapoints: lokiPointsToTimeseriesPoints(matrixResult.values), tags: matrixResult.metric, meta: options.meta, refId: options.refId, }; } -export function lokiPointsToTimeseriesPoints( - data: Array<[number, string]>, - options: TransformerOptions -): TimeSeriesValue[][] { - const stepMs = options.step * 1000; +export function lokiPointsToTimeseriesPoints(data: Array<[number, string]>): TimeSeriesValue[][] { const datapoints: TimeSeriesValue[][] = []; - let baseTimestampMs = options.start / 1e6; for (const [time, value] of data) { let datapointValue: TimeSeriesValue = parseFloat(value); @@ -207,19 +203,10 @@ export function lokiPointsToTimeseriesPoints( } const timestamp = time * 1000; - for (let t = baseTimestampMs; t < timestamp; t += stepMs) { - datapoints.push([null, t]); - } - baseTimestampMs = timestamp + stepMs; datapoints.push([datapointValue, timestamp]); } - const endTimestamp = options.end / 1e6; - for (let t = baseTimestampMs; t <= endTimestamp; t += stepMs) { - datapoints.push([null, t]); - } - return datapoints; } @@ -454,7 +441,7 @@ function fieldFromDerivedFieldConfig(derivedFieldConfigs: DerivedFieldConfig[]): }; } -export function rangeQueryResponseToTimeSeries( +function rangeQueryResponseToTimeSeries( response: LokiResponse, query: LokiRangeQueryRequest, target: LokiQuery, @@ -491,6 +478,33 @@ export function rangeQueryResponseToTimeSeries( } } +export function rangeQueryResponseToDataFrames( + response: LokiResponse, + query: LokiRangeQueryRequest, + target: LokiQuery, + responseListLength: number, + scopedVars: ScopedVars +): DataFrame[] { + const series = rangeQueryResponseToTimeSeries(response, query, target, responseListLength, scopedVars); + const frames = series.map((s) => toDataFrame(s)); + + const { step } = query; + + if (step != null) { + const intervalMs = step * 1000; + + frames.forEach((frame) => { + frame.fields.forEach((field) => { + if (field.type === FieldType.time) { + field.config.interval = intervalMs; + } + }); + }); + } + + return frames; +} + export function processRangeQueryResponse( response: LokiResponse, target: LokiQuery, @@ -511,7 +525,7 @@ export function processRangeQueryResponse( case LokiResultType.Vector: case LokiResultType.Matrix: return of({ - data: rangeQueryResponseToTimeSeries( + data: rangeQueryResponseToDataFrames( response, query, { diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 6a3c81b4a27..d533515091a 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -2,12 +2,11 @@ import { map as _map } from 'lodash'; import { lastValueFrom, of } from 'rxjs'; import { catchError, map, mapTo } from 'rxjs/operators'; import { BackendDataSourceResponse, DataSourceWithBackend, FetchResponse, getBackendSrv } from '@grafana/runtime'; -import { AnnotationEvent, DataSourceInstanceSettings, MetricFindValue, ScopedVars } from '@grafana/data'; +import { AnnotationEvent, DataSourceInstanceSettings, MetricFindValue, ScopedVars, TimeRange } from '@grafana/data'; import ResponseParser from './response_parser'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { MssqlOptions, MssqlQuery, MssqlQueryForInterpolation } from './types'; -import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; export class MssqlDatasource extends DataSourceWithBackend { @@ -18,8 +17,7 @@ export class MssqlDatasource extends DataSourceWithBackend, - private readonly templateSrv: TemplateSrv = getTemplateSrv(), - private readonly timeSrv: TimeSrv = getTimeSrv() + private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); this.name = instanceSettings.name; @@ -123,7 +121,7 @@ export class MssqlDatasource extends DataSourceWithBackend ({ ...(jest.requireActual('@grafana/runtime') as unknown as object), @@ -17,15 +16,13 @@ describe('MSSQLDatasource', () => { const templateSrv: TemplateSrv = new TemplateSrv(); const fetchMock = jest.spyOn(backendSrv, 'fetch'); - const ctx: any = { - timeSrv: new TimeSrvStub(), - }; + const ctx: any = {}; beforeEach(() => { jest.clearAllMocks(); ctx.instanceSettings = { name: 'mssql' }; - ctx.ds = new MssqlDatasource(ctx.instanceSettings, templateSrv, ctx.timeSrv); + ctx.ds = new MssqlDatasource(ctx.instanceSettings, templateSrv); }); describe('When performing annotationQuery', () => { @@ -255,7 +252,6 @@ describe('MSSQLDatasource', () => { }; beforeEach(() => { - ctx.timeSrv.setTime(time); fetchMock.mockImplementation(() => of(createFetchResponse(response))); return ctx.ds.metricFindQuery(query, { range: time }); diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 7865155d6e1..a2e77ec4941 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -9,7 +9,6 @@ import ResponseParser from './response_parser'; import { MySQLOptions, MySQLQuery, MysqlQueryForInterpolation } from './types'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { getSearchFilterScopedVar } from '../../../features/variables/utils'; -import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; export class MysqlDatasource extends DataSourceWithBackend { @@ -21,8 +20,7 @@ export class MysqlDatasource extends DataSourceWithBackend, - private readonly templateSrv: TemplateSrv = getTemplateSrv(), - private readonly timeSrv: TimeSrv = getTimeSrv() + private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); this.name = instanceSettings.name; @@ -140,7 +138,7 @@ export class MysqlDatasource extends DataSourceWithBackend { } as unknown as DataSourceInstanceSettings; const templateSrv: TemplateSrv = new TemplateSrv(); const variable = { ...initialCustomVariableModelState }; - const raw = { - from: toUtc('2018-04-25 10:00'), - to: toUtc('2018-04-25 11:00'), - }; - const timeSrvMock: any = { - timeRange: () => ({ - from: raw.from, - to: raw.to, - raw: raw, - }), - }; fetchMock.mockImplementation((options) => of(createFetchResponse(response))); - const ds = new MysqlDatasource(instanceSettings, templateSrv, timeSrvMock); + const ds = new MysqlDatasource(instanceSettings, templateSrv); return { ds, variable, templateSrv, fetchMock }; }; diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 2b4b448e03e..6d6605740d3 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -2,12 +2,11 @@ import { map as _map } from 'lodash'; import { lastValueFrom, of } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; import { BackendDataSourceResponse, DataSourceWithBackend, FetchResponse, getBackendSrv } from '@grafana/runtime'; -import { AnnotationEvent, DataSourceInstanceSettings, MetricFindValue, ScopedVars } from '@grafana/data'; +import { AnnotationEvent, DataSourceInstanceSettings, MetricFindValue, ScopedVars, TimeRange } from '@grafana/data'; import ResponseParser from './response_parser'; import PostgresQueryModel from 'app/plugins/datasource/postgres/postgres_query_model'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; -import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; //Types import { PostgresOptions, PostgresQuery, PostgresQueryForInterpolation } from './types'; import { getSearchFilterScopedVar } from '../../../features/variables/utils'; @@ -23,8 +22,7 @@ export class PostgresDatasource extends DataSourceWithBackend, - private readonly templateSrv: TemplateSrv = getTemplateSrv(), - private readonly timeSrv: TimeSrv = getTimeSrv() + private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); this.name = instanceSettings.name; @@ -142,7 +140,7 @@ export class PostgresDatasource extends DataSourceWithBackend ({ @@ -43,21 +41,10 @@ describe('PostgreSQLDatasource', () => { }, } as unknown as DataSourceInstanceSettings; const templateSrv: TemplateSrv = new TemplateSrv(); - const raw = { - from: toUtc('2018-04-25 10:00'), - to: toUtc('2018-04-25 11:00'), - }; - const timeSrvMock = { - timeRange: () => ({ - from: raw.from, - to: raw.to, - raw: raw, - }), - } as unknown as TimeSrv; const variable = { ...initialCustomVariableModelState }; - const ds = new PostgresDatasource(instanceSettings, templateSrv, timeSrvMock); + const ds = new PostgresDatasource(instanceSettings, templateSrv); - return { ds, templateSrv, timeSrvMock, variable }; + return { ds, templateSrv, variable }; }; // https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing diff --git a/public/app/plugins/datasource/prometheus/module.test.ts b/public/app/plugins/datasource/prometheus/module.test.ts index f12182e4fc9..2b0473a3791 100644 --- a/public/app/plugins/datasource/prometheus/module.test.ts +++ b/public/app/plugins/datasource/prometheus/module.test.ts @@ -3,7 +3,6 @@ import { ANNOTATION_QUERY_STEP_DEFAULT } from './datasource'; describe('module', () => { it('should have metrics query field in panels and Explore', () => { - expect(PrometheusDatasourcePlugin.components.ExploreMetricsQueryField).toBeDefined(); expect(PrometheusDatasourcePlugin.components.QueryEditor).toBeDefined(); }); it('should have stepDefaultValuePlaceholder set in annotations ctrl', () => { diff --git a/public/app/plugins/datasource/prometheus/module.ts b/public/app/plugins/datasource/prometheus/module.ts index f4711890cba..f570f7c5150 100644 --- a/public/app/plugins/datasource/prometheus/module.ts +++ b/public/app/plugins/datasource/prometheus/module.ts @@ -14,6 +14,5 @@ class PrometheusAnnotationsQueryCtrl { export const plugin = new DataSourcePlugin(PrometheusDatasource) .setQueryEditor(PromQueryEditorByApp) .setConfigEditor(ConfigEditor) - .setExploreMetricsQueryField(PromQueryEditorByApp) .setAnnotationQueryCtrl(PrometheusAnnotationsQueryCtrl) .setQueryEditorHelp(PromCheatSheet); diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 9e7e00c7d4e..99c08ac73f5 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -347,7 +347,7 @@ const serviceGraphLinks = [ title: 'Request rate', internal: { query: { - expr: 'rate(traces_service_graph_request_total{server="${__data.fields.id}"}[$__interval])', + expr: 'rate(traces_service_graph_request_total{server="${__data.fields.id}"}[$__rate_interval])', }, datasourceUid: 'prom', datasourceName: 'Prometheus', @@ -358,7 +358,7 @@ const serviceGraphLinks = [ title: 'Request histogram', internal: { query: { - expr: 'histogram_quantile(0.9, rate(traces_service_graph_request_server_seconds_bucket{server="${__data.fields.id}"}[$__interval]))', + expr: 'histogram_quantile(0.9, sum(rate(traces_service_graph_request_server_seconds_bucket{server="${__data.fields.id}"}[$__rate_interval])) by (le, client, server))', }, datasourceUid: 'prom', datasourceName: 'Prometheus', @@ -369,7 +369,7 @@ const serviceGraphLinks = [ title: 'Failed request rate', internal: { query: { - expr: 'rate(traces_service_graph_request_failed_total{server="${__data.fields.id}"}[$__interval])', + expr: 'rate(traces_service_graph_request_failed_total{server="${__data.fields.id}"}[$__rate_interval])', }, datasourceUid: 'prom', datasourceName: 'Prometheus', diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 028e177bbe1..e6b4470cd2c 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -325,17 +325,17 @@ function serviceMapQuery(request: DataQueryRequest, datasourceUid: s links: [ makePromLink( 'Request rate', - `rate(${totalsMetric}{server="\${__data.fields.id}"}[$__interval])`, + `rate(${totalsMetric}{server="\${__data.fields.id}"}[$__rate_interval])`, datasourceUid ), makePromLink( 'Request histogram', - `histogram_quantile(0.9, rate(${histogramMetric}{server="\${__data.fields.id}"}[$__interval]))`, + `histogram_quantile(0.9, sum(rate(${histogramMetric}{server="\${__data.fields.id}"}[$__rate_interval])) by (le, client, server))`, datasourceUid ), makePromLink( 'Failed request rate', - `rate(${failedMetric}{server="\${__data.fields.id}"}[$__interval])`, + `rate(${failedMetric}{server="\${__data.fields.id}"}[$__rate_interval])`, datasourceUid ), ], diff --git a/public/app/plugins/panel/alertlist/AlertInstances.tsx b/public/app/plugins/panel/alertlist/AlertInstances.tsx index 1b5a049b1d7..269a3ca0a92 100644 --- a/public/app/plugins/panel/alertlist/AlertInstances.tsx +++ b/public/app/plugins/panel/alertlist/AlertInstances.tsx @@ -1,67 +1,47 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import pluralize from 'pluralize'; import { Icon, useStyles2 } from '@grafana/ui'; -import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting'; +import { Alert } from 'app/types/unified-alerting'; import { GrafanaTheme2, PanelProps } from '@grafana/data'; import { css } from '@emotion/css'; -import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; -import { UnifiedAlertListOptions } from './types'; +import { GroupMode, UnifiedAlertListOptions } from './types'; import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable'; import { sortAlerts } from 'app/features/alerting/unified/utils/misc'; +import { filterAlerts } from './util'; interface Props { - ruleWithLocation: PromRuleWithLocation; + alerts: Alert[]; options: PanelProps['options']; } -export const AlertInstances = ({ ruleWithLocation, options }: Props) => { - const { rule } = ruleWithLocation; - const [displayInstances, setDisplayInstances] = useState(options.showInstances); +export const AlertInstances: FC = ({ alerts, options }) => { + // when custom grouping is enabled, we will always uncollapse the list of alert instances + const defaultShowInstances = options.groupMode === GroupMode.Custom ? true : options.showInstances; + const [displayInstances, setDisplayInstances] = useState(defaultShowInstances); const styles = useStyles2(getStyles); - useEffect(() => { - setDisplayInstances(options.showInstances); - }, [options.showInstances]); + const toggleDisplayInstances = useCallback(() => { + setDisplayInstances((display) => !display); + }, []); - const alerts = useMemo( - (): Alert[] => (displayInstances ? filterAlerts(options, sortAlerts(options.sortOrder, rule.alerts)) : []), - [rule, options, displayInstances] + const filteredAlerts = useMemo( + (): Alert[] => filterAlerts(options, sortAlerts(options.sortOrder, alerts)) ?? [], + [alerts, options] ); return (
- {rule.state !== PromAlertingRuleState.Inactive && ( -
setDisplayInstances(!displayInstances)}> + {options.groupMode === GroupMode.Default && ( +
toggleDisplayInstances()}> - {`${rule.alerts.length} ${pluralize('instance', rule.alerts.length)}`} + {`${filteredAlerts.length} ${pluralize('instance', filteredAlerts.length)}`}
)} - - {!!alerts.length && } + {displayInstances && }
); }; -function filterAlerts(options: PanelProps['options'], alerts: Alert[]): Alert[] { - const hasAlertState = Object.values(options.stateFilter).some((value) => value); - let filteredAlerts = [...alerts]; - if (hasAlertState) { - filteredAlerts = filteredAlerts.filter((alert) => { - return ( - (options.stateFilter.firing && - (alert.state === GrafanaAlertState.Alerting || alert.state === PromAlertingRuleState.Firing)) || - (options.stateFilter.pending && - (alert.state === GrafanaAlertState.Pending || alert.state === PromAlertingRuleState.Pending)) || - (options.stateFilter.noData && alert.state === GrafanaAlertState.NoData) || - (options.stateFilter.normal && alert.state === GrafanaAlertState.Normal) || - (options.stateFilter.error && alert.state === GrafanaAlertState.Error) || - (options.stateFilter.inactive && alert.state === PromAlertingRuleState.Inactive) - ); - }); - } - return filteredAlerts; -} - const getStyles = (_: GrafanaTheme2) => ({ instance: css` cursor: pointer; diff --git a/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx new file mode 100644 index 00000000000..8a84dcaa55d --- /dev/null +++ b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx @@ -0,0 +1,75 @@ +import React, { FC, useEffect, useMemo } from 'react'; +import { isEmpty, uniq } from 'lodash'; +import { Icon, MultiSelect } from '@grafana/ui'; +import { SelectableValue } from '@grafana/data'; +import { useDispatch } from 'react-redux'; +import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions'; +import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector'; +import { getAllRulesSourceNames } from 'app/features/alerting/unified/utils/datasource'; +import { PromRuleType } from 'app/types/unified-alerting-dto'; +import { AlertingRule } from 'app/types/unified-alerting'; +import { isPrivateLabel } from './util'; +import { + isAsyncRequestMapSliceFulfilled, + isAsyncRequestMapSlicePending, +} from 'app/features/alerting/unified/utils/redux'; + +interface Props { + id: string; + defaultValue: SelectableValue; + onChange: (keys: string[]) => void; +} + +export const GroupBy: FC = (props) => { + const { onChange, id, defaultValue } = props; + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchAllPromRulesAction()); + }, [dispatch]); + + const promRulesByDatasource = useUnifiedAlertingSelector((state) => state.promRules); + const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []); + + const allRequestsReady = isAsyncRequestMapSliceFulfilled(promRulesByDatasource); + const loading = isAsyncRequestMapSlicePending(promRulesByDatasource); + + const labels = useMemo(() => { + if (isEmpty(promRulesByDatasource)) { + return []; + } + + if (!allRequestsReady) { + return []; + } + + const allLabels = rulesDataSourceNames + .flatMap((datasource) => promRulesByDatasource[datasource].result ?? []) + .flatMap((rules) => rules.groups) + .flatMap((group) => group.rules.filter((rule): rule is AlertingRule => rule.type === PromRuleType.Alerting)) + .flatMap((rule) => rule.alerts ?? []) + .map((alert) => Object.keys(alert.labels ?? {})) + .flatMap((labels) => labels.filter(isPrivateLabel)); + + return uniq(allLabels); + }, [allRequestsReady, promRulesByDatasource, rulesDataSourceNames]); + + return ( + + id={id} + isLoading={loading} + defaultValue={defaultValue} + aria-label={'group by label keys'} + placeholder="Group by" + prefix={} + onChange={(items) => { + onChange(items.map((item) => item.value ?? '')); + }} + options={labels.map((key) => ({ + label: key, + value: key, + }))} + menuShouldPortal={true} + /> + ); +}; diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index e532878c896..6d8488d8629 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -1,15 +1,14 @@ import React, { useEffect, useMemo } from 'react'; import { sortBy } from 'lodash'; import { useDispatch } from 'react-redux'; -import { GrafanaTheme, GrafanaTheme2, intervalToAbbreviatedDurationString, PanelProps } from '@grafana/data'; -import { CustomScrollbar, Icon, IconName, LoadingPlaceholder, useStyles, useStyles2 } from '@grafana/ui'; +import { GrafanaTheme2, PanelProps } from '@grafana/data'; +import { CustomScrollbar, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; -import { AlertInstances } from './AlertInstances'; import alertDef from 'app/features/alerting/state/alertDef'; -import { SortOrder, UnifiedAlertListOptions } from './types'; +import { GroupMode, SortOrder, UnifiedAlertListOptions } from './types'; -import { flattenRules, alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules'; +import { flattenRules, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules'; import { PromRuleWithLocation } from 'app/types/unified-alerting'; import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions'; import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector'; @@ -22,6 +21,8 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { Annotation, RULE_LIST_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; +import UngroupedModeView from './unified-alerting/UngroupedView'; +import GroupedModeView from './unified-alerting/GroupedView'; export function UnifiedAlertList(props: PanelProps) { const dispatch = useDispatch(); @@ -43,8 +44,7 @@ export function UnifiedAlertList(props: PanelProps) { (name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error ); - const styles = useStyles(getStyles); - const stateStyle = useStyles2(getStateTagStyles); + const styles = useStyles2(getStyles); const rules = useMemo( () => @@ -58,8 +58,6 @@ export function UnifiedAlertList(props: PanelProps) { [props.options, promRulesRequests] ); - const rulesToDisplay = rules.length <= props.options.maxItems ? rules : rules.slice(0, props.options.maxItems); - const noAlertsMessage = rules.length ? '' : 'No alerts'; return ( @@ -68,49 +66,12 @@ export function UnifiedAlertList(props: PanelProps) { {dispatched && loading && !haveResults && } {noAlertsMessage &&
{noAlertsMessage}
}
-
    - {haveResults && - rulesToDisplay.map((ruleWithLocation, index) => { - const { rule, namespaceName, groupName } = ruleWithLocation; - const firstActiveAt = getFirstActiveAt(rule); - return ( -
  1. -
    - -
    -
    -
    -
    - {rule.name} -
    -
    - {rule.state.toUpperCase()}{' '} - {firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && ( - <> - for{' '} - - {intervalToAbbreviatedDurationString({ - start: firstActiveAt, - end: Date.now(), - })} - - - )} -
    -
    - -
    -
  2. - ); - })} -
+ {props.options.groupMode === GroupMode.Custom && haveResults && ( + + )} + {props.options.groupMode === GroupMode.Default && haveResults && ( + + )}
@@ -160,7 +121,7 @@ function filterRules(options: PanelProps['options'], ru const matchers = parseMatchers(options.alertInstanceLabelFilter); // Reduce rules and instances to only those that match filteredRules = filteredRules.reduce((rules, rule) => { - const filteredAlerts = rule.rule.alerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers)); + const filteredAlerts = (rule.rule.alerts ?? []).filter(({ labels }) => labelsMatchMatchers(labels, matchers)); if (filteredAlerts.length) { rules.push({ ...rule, rule: { ...rule.rule, alerts: filteredAlerts } }); } @@ -185,10 +146,10 @@ function filterRules(options: PanelProps['options'], ru return filteredRules; } -const getStyles = (theme: GrafanaTheme) => ({ +export const getStyles = (theme: GrafanaTheme2) => ({ cardContainer: css` - padding: ${theme.spacing.xs} 0 ${theme.spacing.xxs} 0; - line-height: ${theme.typography.lineHeight.md}; + padding: ${theme.v1.spacing.xs} 0 ${theme.v1.spacing.xxs} 0; + line-height: ${theme.v1.typography.lineHeight.md}; margin-bottom: 0px; `, container: css` @@ -206,29 +167,34 @@ const getStyles = (theme: GrafanaTheme) => ({ align-items: center; width: 100%; height: 100%; - background: ${theme.colors.bg2}; - padding: ${theme.spacing.xs} ${theme.spacing.sm}; - border-radius: ${theme.border.radius.md}; - margin-bottom: ${theme.spacing.xs}; + background: ${theme.v1.colors.bg2}; + padding: ${theme.v1.spacing.xs} ${theme.v1.spacing.sm}; + border-radius: ${theme.v1.border.radius.md}; + margin-bottom: ${theme.v1.spacing.xs}; & > * { - margin-right: ${theme.spacing.sm}; + margin-right: ${theme.v1.spacing.sm}; } `, alertName: css` - font-size: ${theme.typography.size.md}; - font-weight: ${theme.typography.weight.bold}; + font-size: ${theme.v1.typography.size.md}; + font-weight: ${theme.v1.typography.weight.bold}; + `, + alertLabels: css` + > * { + margin-right: ${theme.v1.spacing.xs}; + } `, alertDuration: css` - font-size: ${theme.typography.size.sm}; + font-size: ${theme.v1.typography.size.sm}; `, alertRuleItemText: css` - font-weight: ${theme.typography.weight.bold}; - font-size: ${theme.typography.size.sm}; + font-weight: ${theme.v1.typography.weight.bold}; + font-size: ${theme.v1.typography.size.sm}; margin: 0; `, alertRuleItemTime: css` - color: ${theme.colors.textWeak}; + color: ${theme.v1.colors.textWeak}; font-weight: normal; white-space: nowrap; `, @@ -246,7 +212,7 @@ const getStyles = (theme: GrafanaTheme) => ({ height: 100%; `, alertIcon: css` - margin-right: ${theme.spacing.xs}; + margin-right: ${theme.v1.spacing.xs}; `, instanceDetails: css` min-width: 1px; @@ -254,68 +220,7 @@ const getStyles = (theme: GrafanaTheme) => ({ overflow: hidden; text-overflow: ellipsis; `, -}); - -const getStateTagStyles = (theme: GrafanaTheme2) => ({ - common: css` - width: 70px; - text-align: center; - align-self: stretch; - - display: inline-block; - color: white; - border-radius: ${theme.shape.borderRadius()}; - font-size: ${theme.typography.size.sm}; - /* padding: ${theme.spacing(2, 0)}; */ - text-transform: capitalize; - line-height: 1.2; - flex-shrink: 0; - - display: flex; - flex-direction: column; - justify-content: center; - `, - icon: css` - margin-top: ${theme.spacing(2.5)}; - align-self: flex-start; - `, - // good: css` - // background-color: ${theme.colors.success.main}; - // border: solid 1px ${theme.colors.success.main}; - // color: ${theme.colors.success.contrastText}; - // `, - // warning: css` - // background-color: ${theme.colors.warning.main}; - // border: solid 1px ${theme.colors.warning.main}; - // color: ${theme.colors.warning.contrastText}; - // `, - // bad: css` - // background-color: ${theme.colors.error.main}; - // border: solid 1px ${theme.colors.error.main}; - // color: ${theme.colors.error.contrastText}; - // `, - // neutral: css` - // background-color: ${theme.colors.secondary.main}; - // border: solid 1px ${theme.colors.secondary.main}; - // `, - // info: css` - // background-color: ${theme.colors.primary.main}; - // border: solid 1px ${theme.colors.primary.main}; - // color: ${theme.colors.primary.contrastText}; - // `, - good: css` - color: ${theme.colors.success.main}; - `, - bad: css` - color: ${theme.colors.error.main}; - `, - warning: css` - color: ${theme.colors.warning.main}; - `, - neutral: css` - color: ${theme.colors.secondary.main}; - `, - info: css` - color: ${theme.colors.primary.main}; + customGroupDetails: css` + margin-bottom: ${theme.v1.spacing.xs}; `, }); diff --git a/public/app/plugins/panel/alertlist/module.tsx b/public/app/plugins/panel/alertlist/module.tsx index 823495fd48e..30e504971ea 100644 --- a/public/app/plugins/panel/alertlist/module.tsx +++ b/public/app/plugins/panel/alertlist/module.tsx @@ -3,7 +3,7 @@ import { PanelPlugin } from '@grafana/data'; import { TagsInput } from '@grafana/ui'; import { AlertList } from './AlertList'; import { UnifiedAlertList } from './UnifiedAlertList'; -import { AlertListOptions, ShowOption, SortOrder, UnifiedAlertListOptions } from './types'; +import { AlertListOptions, GroupMode, ShowOption, SortOrder, UnifiedAlertListOptions } from './types'; import { alertListPanelMigrationHandler } from './AlertListMigrationHandler'; import { config, DataSourcePicker } from '@grafana/runtime'; import { RuleFolderPicker } from 'app/features/alerting/unified/components/rule-editor/RuleFolderPicker'; @@ -13,6 +13,7 @@ import { ReadonlyFolderPicker, } from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker'; import { AlertListSuggestionsSupplier } from './suggestions'; +import { GroupBy } from './GroupByWithLoading'; function showIfCurrentState(options: AlertListOptions) { return options.showOptions === ShowOption.Current; @@ -151,6 +152,37 @@ const alertList = new PanelPlugin(AlertList) const unifiedAlertList = new PanelPlugin(UnifiedAlertList).setPanelOptions((builder) => { builder + .addRadio({ + path: 'groupMode', + name: 'Group mode', + description: 'How alert instances should be grouped', + defaultValue: GroupMode.Default, + settings: { + options: [ + { value: GroupMode.Default, label: 'Default grouping' }, + { value: GroupMode.Custom, label: 'Custom grouping' }, + ], + }, + category: ['Options'], + }) + .addCustomEditor({ + path: 'groupBy', + name: 'Group by', + description: 'Filter alerts using label querying', + id: 'groupBy', + defaultValue: [], + showIf: (options) => options.groupMode === GroupMode.Custom, + category: ['Options'], + editor: (props) => { + return ( + ({ label: value, value }))} + onChange={props.onChange} + /> + ); + }, + }) .addNumberInput({ name: 'Max items', path: 'maxItems', @@ -181,13 +213,6 @@ const unifiedAlertList = new PanelPlugin(UnifiedAlertLi defaultValue: false, category: ['Options'], }) - .addBooleanSwitch({ - path: 'showInstances', - name: 'Show alert instances', - description: 'Show individual alert instances for multi-dimensional rules', - defaultValue: false, - category: ['Options'], - }) .addTextInput({ path: 'alertName', name: 'Alert name', diff --git a/public/app/plugins/panel/alertlist/types.ts b/public/app/plugins/panel/alertlist/types.ts index 4b2d313bcb3..76983e8922a 100644 --- a/public/app/plugins/panel/alertlist/types.ts +++ b/public/app/plugins/panel/alertlist/types.ts @@ -1,3 +1,5 @@ +import { Alert } from 'app/types/unified-alerting'; + export enum SortOrder { AlphaAsc = 1, AlphaDesc, @@ -11,6 +13,11 @@ export enum ShowOption { RecentChanges = 'changes', } +export enum GroupMode { + Default = 'default', + Custom = 'custom', +} + export interface AlertListOptions { showOptions: ShowOption; maxItems: number; @@ -43,6 +50,8 @@ export interface UnifiedAlertListOptions { maxItems: number; sortOrder: SortOrder; dashboardAlerts: boolean; + groupMode: GroupMode; + groupBy: string[]; alertName: string; showInstances: boolean; folder: { id: number; title: string }; @@ -50,3 +59,5 @@ export interface UnifiedAlertListOptions { alertInstanceLabelFilter: string; datasource: string; } + +export type GroupedRules = Map; diff --git a/public/app/plugins/panel/alertlist/unified-alerting/GroupedView.tsx b/public/app/plugins/panel/alertlist/unified-alerting/GroupedView.tsx new file mode 100644 index 00000000000..fc2995361e1 --- /dev/null +++ b/public/app/plugins/panel/alertlist/unified-alerting/GroupedView.tsx @@ -0,0 +1,70 @@ +import React, { FC, useMemo } from 'react'; +import { useStyles2 } from '@grafana/ui'; +import { AlertLabel } from 'app/features/alerting/unified/components/AlertLabel'; +import { AlertInstances } from '../AlertInstances'; +import { GroupedRules, UnifiedAlertListOptions } from '../types'; +import { getStyles } from '../UnifiedAlertList'; +import { PromRuleWithLocation } from 'app/types/unified-alerting'; + +type GroupedModeProps = { + rules: PromRuleWithLocation[]; + options: UnifiedAlertListOptions; +}; + +const GroupedModeView: FC = ({ rules, options }) => { + const styles = useStyles2(getStyles); + + const groupBy = options.groupBy; + + const groupedRules = useMemo(() => { + const groupedRules = new Map(); + + const hasInstancesWithMatchingLabels = (rule: PromRuleWithLocation) => + groupBy ? alertHasEveryLabel(rule, groupBy) : true; + + const matchingRules = rules.filter(hasInstancesWithMatchingLabels); + matchingRules.forEach((rule: PromRuleWithLocation) => { + (rule.rule.alerts ?? []).forEach((alert) => { + const mapKey = createMapKey(groupBy, alert.labels); + const existingAlerts = groupedRules.get(mapKey) ?? []; + groupedRules.set(mapKey, [...existingAlerts, alert]); + }); + }); + + return groupedRules; + }, [groupBy, rules]); + + return ( + <> + {Array.from(groupedRules).map(([key, alerts]) => ( +
  • +
    +
    +
    + {key && parseMapKey(key).map(([key, value]) => )} + {!key && 'No grouping'} +
    +
    + +
    +
  • + ))} + + ); +}; + +function createMapKey(groupBy: string[], labels: Record): string { + return new URLSearchParams(groupBy.map((key) => [key, labels[key]])).toString(); +} + +function parseMapKey(key: string): Array<[string, string]> { + return [...new URLSearchParams(key)]; +} + +function alertHasEveryLabel(rule: PromRuleWithLocation, groupByKeys: string[]) { + return groupByKeys.every((key) => { + return (rule.rule.alerts ?? []).some((alert) => alert.labels[key]); + }); +} + +export default GroupedModeView; diff --git a/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx b/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx new file mode 100644 index 00000000000..8588ec7062f --- /dev/null +++ b/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx @@ -0,0 +1,108 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data'; +import { Icon, IconName, useStyles2 } from '@grafana/ui'; +import alertDef from 'app/features/alerting/state/alertDef'; +import { alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules'; +import { PromRuleWithLocation } from 'app/types/unified-alerting'; +import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +import React, { FC } from 'react'; +import { AlertInstances } from '../AlertInstances'; +import { UnifiedAlertListOptions } from '../types'; +import { getStyles } from '../UnifiedAlertList'; + +type UngroupedModeProps = { + rules: PromRuleWithLocation[]; + options: UnifiedAlertListOptions; +}; + +const UngroupedModeView: FC = ({ rules, options }) => { + const styles = useStyles2(getStyles); + const stateStyle = useStyles2(getStateTagStyles); + + const rulesToDisplay = rules.length <= options.maxItems ? rules : rules.slice(0, options.maxItems); + + return ( + <> +
      + {rulesToDisplay.map((ruleWithLocation, index) => { + const { rule, namespaceName, groupName } = ruleWithLocation; + const firstActiveAt = getFirstActiveAt(rule); + return ( +
    1. +
      + +
      +
      +
      +
      + {rule.name} +
      +
      + {rule.state.toUpperCase()}{' '} + {firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && ( + <> + for{' '} + + {intervalToAbbreviatedDurationString({ + start: firstActiveAt, + end: Date.now(), + })} + + + )} +
      +
      + +
      +
    2. + ); + })} +
    + + ); +}; + +const getStateTagStyles = (theme: GrafanaTheme2) => ({ + common: css` + width: 70px; + text-align: center; + align-self: stretch; + + display: inline-block; + color: white; + border-radius: ${theme.shape.borderRadius()}; + font-size: ${theme.v1.typography.size.sm}; + text-transform: capitalize; + line-height: 1.2; + flex-shrink: 0; + + display: flex; + flex-direction: column; + justify-content: center; + `, + icon: css` + margin-top: ${theme.spacing(2.5)}; + align-self: flex-start; + `, + good: css` + color: ${theme.colors.success.main}; + `, + bad: css` + color: ${theme.colors.error.main}; + `, + warning: css` + color: ${theme.colors.warning.main}; + `, + neutral: css` + color: ${theme.colors.secondary.main}; + `, + info: css` + color: ${theme.colors.primary.main}; + `, +}); + +export default UngroupedModeView; diff --git a/public/app/plugins/panel/alertlist/util.ts b/public/app/plugins/panel/alertlist/util.ts new file mode 100644 index 00000000000..7d00f2237a3 --- /dev/null +++ b/public/app/plugins/panel/alertlist/util.ts @@ -0,0 +1,30 @@ +import { PanelProps } from '@grafana/data'; +import { Alert } from 'app/types/unified-alerting'; +import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +import { isEmpty } from 'lodash'; +import { UnifiedAlertListOptions } from './types'; + +export function filterAlerts(options: PanelProps['options'], alerts: Alert[]): Alert[] { + const { stateFilter } = options; + + if (isEmpty(stateFilter)) { + return alerts; + } + + return alerts.filter((alert) => { + return ( + (stateFilter.firing && + (alert.state === GrafanaAlertState.Alerting || alert.state === PromAlertingRuleState.Firing)) || + (stateFilter.pending && + (alert.state === GrafanaAlertState.Pending || alert.state === PromAlertingRuleState.Pending)) || + (stateFilter.noData && alert.state === GrafanaAlertState.NoData) || + (stateFilter.normal && alert.state === GrafanaAlertState.Normal) || + (stateFilter.error && alert.state === GrafanaAlertState.Error) || + (stateFilter.inactive && alert.state === PromAlertingRuleState.Inactive) + ); + }); +} + +export function isPrivateLabel(label: string) { + return !(label.startsWith('__') && label.endsWith('__')); +} diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 3b1e6cdb9ac..8820d009485 100755 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -1,9 +1,12 @@ -import React, { useMemo, useRef } from 'react'; -import { TooltipDisplayMode, StackingMode, LegendDisplayMode } from '@grafana/schema'; +import React, { useMemo, useRef, useState } from 'react'; +import { css } from '@emotion/css'; +import { LegendDisplayMode } from '@grafana/schema'; import { + CartesianCoords2D, compareDataFrameStructures, DataFrame, getFieldDisplayName, + GrafanaTheme2, PanelProps, TimeRange, VizOrientation, @@ -13,20 +16,27 @@ import { GraphNGProps, measureText, PlotLegend, - TooltipPlugin, + Portal, UPlotConfigBuilder, UPLOT_AXIS_FONT_SIZE, usePanelContext, + useStyles2, useTheme2, VizLayout, VizLegend, + VizTooltipContainer, } from '@grafana/ui'; +import { PanelDataErrorView } from '@grafana/runtime'; +import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG'; + import { PanelOptions } from './models.gen'; import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils'; -import { PanelDataErrorView } from '@grafana/runtime'; import { DataHoverView } from '../geomap/components/DataHoverView'; import { getFieldLegendItem } from '../state-timeline/utils'; -import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG'; +import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; +import { HoverEvent, setupConfig } from './config'; + +const TOOLTIP_OFFSET = 10; /** * @alpha @@ -55,8 +65,31 @@ interface Props extends PanelProps {} export const BarChartPanel: React.FunctionComponent = ({ data, options, width, height, timeZone, id }) => { const theme = useTheme2(); + const styles = useStyles2(getStyles); const { eventBus } = usePanelContext(); + const oldConfig = useRef(undefined); + const isToolTipOpen = useRef(false); + + const [hover, setHover] = useState(undefined); + const [coords, setCoords] = useState(null); + const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); + const [focusedPointIdx, setFocusedPointIdx] = useState(null); + const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(false); + + const onCloseToolTip = () => { + isToolTipOpen.current = false; + setCoords(null); + setShouldDisplayCloseButton(false); + }; + + const onUPlotClick = () => { + isToolTipOpen.current = !isToolTipOpen.current; + + // Linking into useState required to re-render tooltip + setShouldDisplayCloseButton(isToolTipOpen.current); + }; + const frame0Ref = useRef(); const info = useMemo(() => prepareBarChartDisplayValues(data?.series, theme, options), [data, theme, options]); const structureRef = useRef(10000); @@ -66,7 +99,7 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w }, [options]); // change every time the options object changes (while editing) const structureRev = useMemo(() => { - const f0 = info.viz; + const f0 = info.viz[0]; const f1 = frame0Ref.current; if (!(f0 && f1 && compareDataFrameStructures(f0, f1, true))) { structureRef.current++; @@ -86,7 +119,7 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w // If no max length is set, limit the number of characters to a length where it will use a maximum of half of the height of the viz. if (!options.xTickLabelMaxLength) { const rotationAngle = options.xTickLabelRotation; - const textSize = measureText('M', UPLOT_AXIS_FONT_SIZE).width; // M is usually the widest character so let's use that as an aproximation. + const textSize = measureText('M', UPLOT_AXIS_FONT_SIZE).width; // M is usually the widest character so let's use that as an approximation. const maxHeightForValues = height / 2; return ( @@ -99,15 +132,7 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w } }, [height, options.xTickLabelRotation, options.xTickLabelMaxLength]); - // Force 'multi' tooltip setting or stacking mode - const tooltip = useMemo(() => { - if (options.stacking === StackingMode.Normal || options.stacking === StackingMode.Percent) { - return { ...options.tooltip, mode: TooltipDisplayMode.Multi }; - } - return options.tooltip; - }, [options.tooltip, options.stacking]); - - if (!info.viz?.fields.length) { + if (!info.viz[0]?.fields.length) { return ; } @@ -119,12 +144,20 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w } return ( - + <> + {shouldDisplayCloseButton && ( + <> + +
    + + )} + + ); }; @@ -145,7 +178,7 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w } } - return ; + return ; }; const rawValue = (seriesIdx: number, valueIdx: number) => { @@ -200,14 +233,14 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w rawValue, getColor, fillOpacity, - allFrames: [info.viz], + allFrames: info.viz, }); }; return ( f[0]} // already processed in by the panel above! @@ -219,17 +252,39 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w width={width} height={height} > - {(config, alignedFrame) => { + {(config) => { + if (oldConfig.current !== config) { + oldConfig.current = setupConfig({ + config, + onUPlotClick, + setFocusedSeriesIdx, + setFocusedPointIdx, + setCoords, + setHover, + isToolTipOpen, + }); + } + return ( - + + {hover && coords && ( + + {renderTooltip(info.aligned, focusedSeriesIdx, focusedPointIdx)} + + )} + ); }} ); }; + +const getStyles = (theme: GrafanaTheme2) => ({ + closeButtonSpacer: css` + margin-bottom: 15px; + `, +}); diff --git a/public/app/plugins/panel/barchart/config.ts b/public/app/plugins/panel/barchart/config.ts new file mode 100644 index 00000000000..44652526236 --- /dev/null +++ b/public/app/plugins/panel/barchart/config.ts @@ -0,0 +1,107 @@ +import { Dispatch, MutableRefObject, SetStateAction } from 'react'; +import { UPlotConfigBuilder } from '@grafana/ui'; +import { positionTooltip } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin'; +import { CartesianCoords2D } from '@grafana/data'; + +export type HoverEvent = { + xIndex: number; + yIndex: number; + pageX: number; + pageY: number; +}; + +type SetupConfigParams = { + config: UPlotConfigBuilder; + onUPlotClick: () => void; + setFocusedSeriesIdx: Dispatch>; + setFocusedPointIdx: Dispatch>; + setCoords: Dispatch>; + setHover: Dispatch>; + isToolTipOpen: MutableRefObject; +}; + +// This applies config hooks to setup tooltip listener. Ideally this could happen in the same `prepConfig` function +// however the GraphNG structures do not allow access to the `setHover` callback +export const setupConfig = ({ + config, + onUPlotClick, + setFocusedSeriesIdx, + setFocusedPointIdx, + setCoords, + setHover, + isToolTipOpen, +}: SetupConfigParams): UPlotConfigBuilder => { + config.addHook('init', (u) => { + u.root.parentElement?.addEventListener('click', onUPlotClick); + u.over.addEventListener('mouseleave', () => { + if (!isToolTipOpen.current) { + setCoords(null); + } + }); + }); + + let rect: DOMRect; + // rect of .u-over (grid area) + config.addHook('syncRect', (u, r) => { + rect = r; + }); + + const tooltipInterpolator = config.getTooltipInterpolator(); + if (tooltipInterpolator) { + config.addHook('setCursor', (u) => { + tooltipInterpolator( + setFocusedSeriesIdx, + setFocusedPointIdx, + (clear) => { + if (clear && !isToolTipOpen.current) { + setCoords(null); + return; + } + + if (!rect) { + return; + } + + const { x, y } = positionTooltip(u, rect); + if (x !== undefined && y !== undefined && !isToolTipOpen.current) { + setCoords({ x, y }); + } + }, + u + ); + }); + } + + config.addHook('setLegend', (u) => { + if (!isToolTipOpen.current) { + setFocusedPointIdx(u.legend.idx!); + } + if (u.cursor.idxs != null) { + for (let i = 0; i < u.cursor.idxs.length; i++) { + const sel = u.cursor.idxs[i]; + if (sel != null) { + const hover: HoverEvent = { + xIndex: sel, + yIndex: 0, + pageX: rect.left + u.cursor.left!, + pageY: rect.top + u.cursor.top!, + }; + + if (!isToolTipOpen.current || !hover) { + setHover(hover); + } + + return; // only show the first one + } + } + } + }); + + config.addHook('setSeries', (_, idx) => { + if (!isToolTipOpen.current) { + setFocusedSeriesIdx(idx); + } + }); + + return config; +}; diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 3efced85fb8..3dc4e97dbbb 100755 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -69,9 +69,10 @@ export const plugin = new PanelPlugin(BarChar .setPanelOptions((builder, context) => { const disp = prepareBarChartDisplayValues(context.data, config.theme2, context.options ?? ({} as any)); let xaxisPlaceholder = 'First string or time field'; - if (disp.viz?.fields?.length) { - const first = disp.viz.fields[0]; - xaxisPlaceholder += ` (${getFieldDisplayName(first, disp.viz)})`; + const viz = disp.viz ? disp.viz[0] : undefined; + if (viz?.fields?.length) { + const first = viz.fields[0]; + xaxisPlaceholder += ` (${getFieldDisplayName(first, viz)})`; } builder diff --git a/public/app/plugins/panel/barchart/types.ts b/public/app/plugins/panel/barchart/types.ts index ba8cb9f69b9..c91940d7c09 100644 --- a/public/app/plugins/panel/barchart/types.ts +++ b/public/app/plugins/panel/barchart/types.ts @@ -7,8 +7,11 @@ export interface BarChartDisplayValues { /** All fields joined */ aligned: DataFrame; - /** The fields we can display, first field is X axis */ - viz: DataFrame; + /** + * The fields we can display, first field is X axis. + * This needs to be an array to avoid extra re-initialization in GraphNG + */ + viz: [DataFrame]; /** Potentialy color by a field value */ colorByField?: Field; diff --git a/public/app/plugins/panel/barchart/utils.test.ts b/public/app/plugins/panel/barchart/utils.test.ts index 482c9c09f47..6fdfbbbd107 100644 --- a/public/app/plugins/panel/barchart/utils.test.ts +++ b/public/app/plugins/panel/barchart/utils.test.ts @@ -185,7 +185,7 @@ describe('BarChart utils', () => { }); const result = prepareBarChartDisplayValues([df], createTheme(), { stacking: StackingMode.None } as any); - const field = result.viz.fields[1]; + const field = result.viz[0].fields[1]; expect(field!.values.toArray()).toMatchInlineSnapshot(` Array [ -10, @@ -209,19 +209,19 @@ describe('BarChart utils', () => { const resultAsc = prepareBarChartDisplayValues([frame], createTheme(), { legend: { sortBy: 'Min', sortDesc: false }, - } as any); - expect(resultAsc.viz.fields[0].type).toBe(FieldType.string); - expect(resultAsc.viz.fields[1].name).toBe('a'); - expect(resultAsc.viz.fields[2].name).toBe('c'); - expect(resultAsc.viz.fields[3].name).toBe('b'); + } as any).viz[0]; + expect(resultAsc.fields[0].type).toBe(FieldType.string); + expect(resultAsc.fields[1].name).toBe('a'); + expect(resultAsc.fields[2].name).toBe('c'); + expect(resultAsc.fields[3].name).toBe('b'); const resultDesc = prepareBarChartDisplayValues([frame], createTheme(), { legend: { sortBy: 'Min', sortDesc: true }, - } as any); - expect(resultDesc.viz.fields[0].type).toBe(FieldType.string); - expect(resultDesc.viz.fields[1].name).toBe('b'); - expect(resultDesc.viz.fields[2].name).toBe('c'); - expect(resultDesc.viz.fields[3].name).toBe('a'); + } as any).viz[0]; + expect(resultDesc.fields[0].type).toBe(FieldType.string); + expect(resultDesc.fields[1].name).toBe('b'); + expect(resultDesc.fields[2].name).toBe('c'); + expect(resultDesc.fields[3].name).toBe('a'); }); }); }); diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 0e33b97acf9..992138a0753 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -434,10 +434,12 @@ export function prepareBarChartDisplayValues( return { aligned: frame, colorByField, - viz: { - length: firstField.values.length, - fields: fields, // ideally: fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.viz)), - }, + viz: [ + { + length: firstField.values.length, + fields: fields, // ideally: fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.viz)), + }, + ], }; } diff --git a/public/app/plugins/panel/geomap/GeomapTooltip.tsx b/public/app/plugins/panel/geomap/GeomapTooltip.tsx index 652b9023424..6dd9578e2d5 100644 --- a/public/app/plugins/panel/geomap/GeomapTooltip.tsx +++ b/public/app/plugins/panel/geomap/GeomapTooltip.tsx @@ -1,5 +1,6 @@ import React, { createRef } from 'react'; import { VizTooltipContainer } from '@grafana/ui'; +import { useDialog } from '@react-aria/dialog'; import { useOverlay } from '@react-aria/overlays'; import { ComplexDataHoverView } from './components/ComplexDataHoverView'; @@ -14,13 +15,14 @@ interface Props { export const GeomapTooltip = ({ ttip, onClose, isOpen }: Props) => { const ref = createRef(); const { overlayProps } = useOverlay({ onClose, isDismissable: true, isOpen }, ref); + const { dialogProps } = useDialog({}, ref); return ( <> {ttip && ttip.layers && ( -
    - +
    +
    )} diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index fdc09d85613..37d0f433dba 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -19,7 +19,7 @@ import { graphPanelMigrationHandler } from './GraphMigrations'; import { DataWarning, GraphFieldConfig, GraphPanelOptions } from './types'; import { auto } from 'angular'; -import { getLocationSrv } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { getDataTimeRange } from './utils'; import { changePanelPlugin } from 'app/features/panel/state/actions'; import { dispatch } from 'app/store/store'; @@ -266,12 +266,9 @@ export class GraphCtrl extends MetricsPanelCtrl { if (range) { dataWarning.actionText = 'Zoom to data'; dataWarning.action = () => { - getLocationSrv().update({ - partial: true, - query: { - from: range.from, - to: range.to, - }, + locationService.partial({ + from: range.from, + to: range.to, }); }; } diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index e45c66fceab..2aed7c0165b 100755 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -1,10 +1,17 @@ -import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; +import { + FieldColorModeId, + FieldConfigProperty, + FieldType, + identityOverrideProcessor, + PanelPlugin, +} from '@grafana/data'; import { StateTimelinePanel } from './StateTimelinePanel'; import { TimelineOptions, TimelineFieldConfig, defaultPanelOptions, defaultTimelineFieldConfig } from './types'; import { VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { timelinePanelChangedHandler } from './migrations'; import { StatTimelineSuggestionsSupplier } from './suggestions'; +import { SpanNullsEditor } from '../timeseries/SpanNullsEditor'; export const plugin = new PanelPlugin(StateTimelinePanel) .setPanelChangeHandler(timelinePanelChangedHandler) @@ -40,6 +47,16 @@ export const plugin = new PanelPlugin(Stat max: 100, step: 1, }, + }) + .addCustomEditor({ + id: 'spanNulls', + path: 'spanNulls', + name: 'Connect null values', + defaultValue: false, + editor: SpanNullsEditor, + override: SpanNullsEditor, + shouldApply: (f) => f.type !== FieldType.time, + process: identityOverrideProcessor, }); }, }) diff --git a/public/app/plugins/panel/state-timeline/timeline.ts b/public/app/plugins/panel/state-timeline/timeline.ts index b82ca170a22..d81d2384db8 100644 --- a/public/app/plugins/panel/state-timeline/timeline.ts +++ b/public/app/plugins/panel/state-timeline/timeline.ts @@ -41,6 +41,7 @@ export interface TimelineCoreOptions { colWidth?: number; theme: GrafanaTheme2; showValue: VisibilityMode; + mergeValues?: boolean; isDiscrete: (seriesIdx: number) => boolean; getValueColor: (seriesIdx: number, value: any) => string; label: (seriesIdx: number) => string; @@ -62,6 +63,7 @@ export function getConfig(opts: TimelineCoreOptions) { rowHeight = 0, colWidth = 0, showValue, + mergeValues = false, theme, label, formatValue, @@ -212,11 +214,16 @@ export function getConfig(opts: TimelineCoreOptions) { walk(rowHeight, sidx - 1, numSeries, yDim, (iy, y0, height) => { if (mode === TimelineMode.Changes) { for (let ix = 0; ix < dataY.length; ix++) { - if (dataY[ix] != null) { + let yVal = dataY[ix]; + + if (yVal != null) { let left = Math.round(valToPosX(dataX[ix], scaleX, xDim, xOff)); let nextIx = ix; - while (dataY[++nextIx] === undefined && nextIx < dataY.length) {} + while ( + ++nextIx < dataY.length && + (dataY[nextIx] === undefined || (mergeValues && dataY[nextIx] === yVal)) + ) {} // to now (not to end of chart) let right = @@ -236,7 +243,7 @@ export function getConfig(opts: TimelineCoreOptions) { strokeWidth, iy, ix, - dataY[ix], + yVal, discrete ); diff --git a/public/app/plugins/panel/state-timeline/utils.test.ts b/public/app/plugins/panel/state-timeline/utils.test.ts index 8e7fb9f4a90..0382bf21bd0 100644 --- a/public/app/plugins/panel/state-timeline/utils.test.ts +++ b/public/app/plugins/panel/state-timeline/utils.test.ts @@ -54,12 +54,12 @@ describe('prepare timeline graph', () => { const field = out.fields.find((f) => f.name === 'b'); expect(field?.values.toArray()).toMatchInlineSnapshot(` Array [ + 1, 1, undefined, - undefined, - undefined, + 1, + 2, 2, - undefined, null, 2, 3, diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index f5cbefd4e67..1b5a7ff3036 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -69,6 +69,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ colWidth, showValue, alignValue, + mergeValues, }) => { const builder = new UPlotConfigBuilder(timeZone); @@ -98,6 +99,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ mode: mode!, numSeries: frame.fields.length - 1, isDiscrete: (seriesIdx) => isDiscrete(frame.fields[seriesIdx]), + mergeValues, rowHeight: rowHeight!, colWidth: colWidth, showValue: showValue!, @@ -308,6 +310,15 @@ export function unsetSameFutureValues(values: any[]): any[] | undefined { return clone; } +function getSpanNulls(field: Field) { + let spanNulls = field.config.custom?.spanNulls; + + // magic value for join() to leave nulls alone instead of expanding null ranges + // should be set to -1 when spanNulls = null|undefined|false|0, which is "retain nulls, without expanding" + // Infinity is not optimal here since it causes spanNulls to be more expensive than simply removing all nulls unconditionally + return !spanNulls ? -1 : spanNulls === true ? Infinity : spanNulls; +} + /** * Merge values by the threshold */ @@ -329,7 +340,6 @@ export function mergeThresholdValues(field: Field, theme: GrafanaTheme2): Field textToColor.set(items[i].label, items[i].color!); } - let prev: Threshold | undefined = undefined; let input = field.values.toArray(); const vals = new Array(field.values.length); if (thresholds.mode === ThresholdsMode.Percentage) { @@ -347,19 +357,20 @@ export function mergeThresholdValues(field: Field, theme: GrafanaTheme2): Field const v = input[i]; if (v == null) { vals[i] = v; - prev = undefined; - } - const active = getActiveThreshold(v, thresholds.steps); - if (active === prev) { - vals[i] = undefined; } else { - vals[i] = thresholdToText.get(active); + vals[i] = thresholdToText.get(getActiveThreshold(v, thresholds.steps)); } - prev = active; } return { ...field, + config: { + ...field.config, + custom: { + ...field.config.custom, + spanNulls: getSpanNulls(field), + }, + }, type: FieldType.string, values: new ArrayVector(vals), display: (value: string) => ({ @@ -410,23 +421,10 @@ export function prepareTimelineFields( ...field.config, custom: { ...field.config.custom, - // magic value for join() to leave nulls alone - spanNulls: -1, + spanNulls: getSpanNulls(field), }, }, }; - - if (mergeValues) { - let merged = unsetSameFutureValues(field.values.toArray()); - if (merged) { - fields.push({ - ...field, - values: new ArrayVector(merged), - }); - changed = true; - continue; - } - } fields.push(field); break; default: diff --git a/public/app/plugins/panel/timeseries/config.ts b/public/app/plugins/panel/timeseries/config.ts index 073784bf461..dae74c592a2 100644 --- a/public/app/plugins/panel/timeseries/config.ts +++ b/public/app/plugins/panel/timeseries/config.ts @@ -16,6 +16,7 @@ import { VisibilityMode, StackingMode, GraphTresholdsStyleMode, + GraphTransform, } from '@grafana/schema'; import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui'; @@ -181,6 +182,29 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption }); commonOptionsBuilder.addStackingConfig(builder, cfg.stacking, categoryStyles); + + builder.addSelect({ + category: categoryStyles, + name: 'Transform', + path: 'transform', + settings: { + options: [ + { + label: 'Constant', + value: GraphTransform.Constant, + description: 'The first value will be shown as a constant line', + }, + { + label: 'Negative Y', + value: GraphTransform.NegativeY, + description: 'Flip the results to negative values on the y axis', + }, + ], + isClearable: true, + }, + hideFromDefaults: true, + }); + commonOptionsBuilder.addAxisConfig(builder, cfg); commonOptionsBuilder.addHideFrom(builder); diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index e1256bea07e..43f9e0bb861 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -207,18 +207,29 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/org/teams', - roles: () => (config.editorsCanAdmin ? [] : ['Editor', 'Admin']), + roles: () => + contextSrv.evaluatePermission( + () => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']), + [AccessControlAction.ActionTeamsRead, AccessControlAction.ActionTeamsCreate] + ), component: SafeDynamicImport(() => import(/* webpackChunkName: "TeamList" */ 'app/features/teams/TeamList')), }, { path: '/org/teams/new', - - roles: () => (config.editorsCanAdmin ? [] : ['Admin']), + roles: () => + contextSrv.evaluatePermission( + () => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']), + [AccessControlAction.ActionTeamsCreate] + ), component: SafeDynamicImport(() => import(/* webpackChunkName: "CreateTeam" */ 'app/features/teams/CreateTeam')), }, { path: '/org/teams/edit/:id/:page?', - roles: () => (config.editorsCanAdmin ? [] : ['Admin']), + roles: () => + contextSrv.evaluatePermission( + () => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']), + [AccessControlAction.ActionTeamsRead, AccessControlAction.ActionTeamsCreate] + ), component: SafeDynamicImport(() => import(/* webpackChunkName: "TeamPages" */ 'app/features/teams/TeamPages')), }, { diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index 11c84d7ad97..b542ed44bdb 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -50,9 +50,17 @@ export enum AccessControlAction { ActionServerStatsRead = 'server.stats:read', ActionTeamsCreate = 'teams:create', + ActionTeamsDelete = 'teams:delete', + ActionTeamsRead = 'teams:read', + ActionTeamsWrite = 'teams:write', + ActionTeamsPermissionsRead = 'teams.permissions:read', + ActionTeamsPermissionsWrite = 'teams.permissions:write', ActionRolesList = 'roles:list', ActionBuiltinRolesList = 'roles.builtin:list', + ActionTeamsRolesList = 'teams.roles:list', + ActionTeamsRolesAdd = 'teams.roles:add', + ActionTeamsRolesRemove = 'teams.roles:remove', ActionUserRolesList = 'users.roles:list', } diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index 1bf8605522e..92c83b436a9 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -1,6 +1,7 @@ +import { WithAccessControlMetadata } from '@grafana/data'; import { TeamPermissionLevel } from './acl'; -export interface Team { +export interface Team extends WithAccessControlMetadata { id: number; name: string; avatarUrl: string; diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts index 2ee1d9213ed..50c4c5f4c51 100644 --- a/public/app/types/unified-alerting.ts +++ b/public/app/types/unified-alerting.ts @@ -28,7 +28,7 @@ interface RuleBase { } export interface AlertingRule extends RuleBase { - alerts: Alert[]; + alerts?: Alert[]; labels: { [key: string]: string; }; diff --git a/public/emails/ng_alert_notification.html b/public/emails/ng_alert_notification.html index a873e1299b7..8f0f2828b8a 100644 --- a/public/emails/ng_alert_notification.html +++ b/public/emails/ng_alert_notification.html @@ -183,7 +183,7 @@ text-decoration: underline; @@ -200,18 +200,21 @@ text-decoration: underline;
    - +
    - + - - + + + {{ if gt (len .Annotations.SortedPairs) 0 }}
    - {{Subject .Subject "{{.Title}}"}} + +
    + +{{Subject .Subject "{{.Title}}"}} {{ define "alert" }} -
    - Value: {{ .ValueString }} -
    + Value: {{ .ValueString }} +
    @@ -233,25 +236,25 @@ text-decoration: underline; {{ if .SilenceURL }} - + Silence {{ end }} {{ if .Annotations.runbook_url }} - + View Runbook {{ end }} {{ if .DashboardURL}} - + Go to Dashboard {{ end }} {{ if .PanelURL}} - + Go to Panel {{ end }} @@ -277,62 +280,62 @@ text-decoration: underline;
    - {{ if gt (len .Alerts.Firing) 0 }} - - - - {{ range .Alerts.Firing }} + {{ if gt (len .Alerts.Firing) 0 }} + + + + {{ range .Alerts.Firing }} + + + + + {{ template "alert" . }} + {{ end }} + {{ end }} + {{ if gt (len .Alerts.Resolved) 0 }} + + + + {{ range .Alerts.Resolved }} + + + + + {{ template "alert" . }} + {{ end }} + {{ end }} - - - {{ template "alert" . }} - {{ end }} - {{ end }} - {{ if gt (len .Alerts.Resolved) 0 }} - - - - {{ range .Alerts.Resolved }} - - - - - {{ template "alert" . }} - {{ end }} - {{ end }} - - -
    - Firing: {{ .Alerts.Firing | len }} alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for - {{ range .GroupLabels.SortedPairs }} - {{ .Name }}={{ .Value }} - {{ end }}{{ end }} -
    + Firing: {{ .Alerts.Firing | len }} alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for + {{ range .GroupLabels.SortedPairs }} + {{ .Name }}={{ .Value }} + {{ end }}{{ end }} +
    + Firing + + {{ .Labels.alertname }} +
    + Resolved: {{ .Alerts.Resolved | len }} alert{{ if gt (len .Alerts.Resolved) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for + {{ range .GroupLabels.SortedPairs }} + {{ .Name }}={{ .Value }} + {{ end }}{{ end }} +
    + Resolved + + {{ .Labels.alertname }} +
    - Firing - - {{ .Labels.alertname }} + + Go to alerts page
    - Resolved: {{ .Alerts.Resolved | len }} alert{{ if gt (len .Alerts.Resolved) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for - {{ range .GroupLabels.SortedPairs }} - {{ .Name }}={{ .Value }} - {{ end }}{{ end }} -
    - Resolved - - {{ .Labels.alertname }} -
    - Go to alerts page -
    -
    - - +
    {{ end }} + -
    @@ -341,7 +344,7 @@ text-decoration: underline; -
    +

    Sent by Grafana v{{.BuildVersion}} @@ -355,9 +358,9 @@ text-decoration: underline;

    - - - - + + + + diff --git a/public/sass/_variables.dark.generated.scss b/public/sass/_variables.dark.generated.scss index d485dab7aad..474f9c119de 100644 --- a/public/sass/_variables.dark.generated.scss +++ b/public/sass/_variables.dark.generated.scss @@ -237,7 +237,7 @@ $dropdownBackground: #181b1f; $dropdownBorder: rgba(204, 204, 220, 0.07); $dropdownDividerTop: rgba(204, 204, 220, 0.07); $dropdownDividerBottom: rgba(204, 204, 220, 0.07); -$dropdownShadow: 0px 10px 20px rgb(20,20,20); +$dropdownShadow: 0px 8px 24px rgb(1,4,9); $dropdownLinkColor: $link-color; $dropdownLinkColorHover: $white; @@ -267,7 +267,7 @@ $side-menu-header-color: rgb(204, 204, 220); // ------------------------- $menu-dropdown-bg: #181b1f; $menu-dropdown-hover-bg: rgba(204, 204, 220, 0.16); -$menu-dropdown-shadow: 0px 10px 20px rgb(20,20,20); +$menu-dropdown-shadow: 0px 8px 24px rgb(1,4,9); // Tabs // ------------------------- @@ -301,7 +301,7 @@ $popover-bg: #181b1f; $popover-color: rgb(204, 204, 220); $popover-border-color: rgba(204, 204, 220, 0.07); $popover-header-bg: #22252b; -$popover-shadow: 0px 10px 20px rgb(20,20,20); +$popover-shadow: 0px 8px 24px rgb(1,4,9); $popover-help-bg: $tooltipBackground; $popover-help-color: $text-color; diff --git a/public/views/swagger.html b/public/views/swagger.html new file mode 100644 index 00000000000..c12cc817aca --- /dev/null +++ b/public/views/swagger.html @@ -0,0 +1,63 @@ + + + + + + Swagger UI + + + + + + + +
    + + + + + + diff --git a/scripts/check-breaking-changes.sh b/scripts/check-breaking-changes.sh index a76e3a2626b..94834b59f07 100755 --- a/scripts/check-breaking-changes.sh +++ b/scripts/check-breaking-changes.sh @@ -12,7 +12,7 @@ while IFS=" " read -r -a package; do PACKAGE_PATH=$(basename "$package") # Calculate current and previous package paths / names - PREV="./main/packages/$PACKAGE_PATH/dist/" + PREV="./base/packages/$PACKAGE_PATH/dist/" CURRENT="./pr/packages/$PACKAGE_PATH/dist/" # Temporarily skipping these packages as they don't have any exposed static typing diff --git a/scripts/drone/pipelines/main.star b/scripts/drone/pipelines/main.star index 9c65e7a0bc9..583f3854160 100644 --- a/scripts/drone/pipelines/main.star +++ b/scripts/drone/pipelines/main.star @@ -21,6 +21,7 @@ load( 'build_frontend_docs_step', 'copy_packages_for_docker_step', 'build_docker_images_step', + 'publish_images_step', 'postgres_integration_tests_step', 'mysql_integration_tests_step', 'redis_integration_tests_step', @@ -111,8 +112,10 @@ def get_steps(edition, is_downstream=False): frontend_metrics_step(edition=edition), build_frontend_docs_step(edition=edition), copy_packages_for_docker_step(), - build_docker_images_step(edition=edition, ver_mode=ver_mode, publish=publish), - build_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=publish), + build_docker_images_step(edition=edition, ver_mode=ver_mode, publish=False), + build_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=False), + publish_images_step(edition=edition, ver_mode=ver_mode, mode='', docker_repo='grafana', ubuntu=False), + publish_images_step(edition=edition, ver_mode=ver_mode, mode='', docker_repo='grafana-oss', ubuntu=True) ]) if include_enterprise2: diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star index 1c592e9d730..237f903e005 100644 --- a/scripts/drone/pipelines/release.star +++ b/scripts/drone/pipelines/release.star @@ -23,7 +23,7 @@ load( 'e2e_tests_artifacts', 'build_storybook_step', 'copy_packages_for_docker_step', - 'package_docker_images_step', + 'build_docker_images_step', 'postgres_integration_tests_step', 'mysql_integration_tests_step', 'redis_integration_tests_step', @@ -36,7 +36,8 @@ load( 'store_packages_step', 'upload_cdn_step', 'validate_scuemata_step', - 'ensure_cuetsified_step' + 'ensure_cuetsified_step', + 'publish_images_step' ) load( @@ -117,27 +118,6 @@ def release_npm_packages_step(): ], } -def publish_images_step(edition, mode, docker_repo): - if mode == 'security': - mode = '--{} '.format(mode) - else: - mode = '' - return { - 'name': 'publish-images-{}'.format(docker_repo), - 'image': 'google/cloud-sdk', - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'DOCKER_USER': from_secret('docker_username'), - 'DOCKER_PASSWORD': from_secret('docker_password'), - }, - 'commands': ['./bin/grabpl artifacts docker publish {}--version-tag ${{TAG}} --dockerhub-repo {} --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7'.format(mode, docker_repo)], - 'depends_on': ['fetch-images-{}'.format(edition)], - 'volumes': [{ - 'name': 'docker', - 'path': '/var/run/docker.sock' - }], - } - def fetch_images_step(edition): return { 'name': 'fetch-images-{}'.format(edition), @@ -159,10 +139,10 @@ def publish_image_steps(version, mode, docker_repo, additional_docker_repo=""): steps = [ download_grabpl_step(), fetch_images_step(version), - publish_images_step(version, mode, docker_repo), + publish_images_step(version, 'release', mode, docker_repo), ] if additional_docker_repo != "": - steps.extend([publish_images_step(version, mode, additional_docker_repo)]) + steps.extend([publish_images_step(version, 'release', mode, additional_docker_repo)]) return steps @@ -224,8 +204,8 @@ def get_steps(edition, ver_mode): build_steps.extend([ package_step(edition=edition, ver_mode=ver_mode, include_enterprise2=include_enterprise2), copy_packages_for_docker_step(), - package_docker_images_step(edition=edition, ver_mode=ver_mode, publish=should_publish), - package_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=should_publish), + build_docker_images_step(edition=edition, ver_mode=ver_mode, publish=True), + build_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=True), grafana_server_step(edition=edition), ]) @@ -285,7 +265,7 @@ def get_oss_pipelines(trigger, ver_mode): ) pipelines = [ pipeline( - name='oss-build-publish{}-{}'.format(get_e2e_suffix(), ver_mode), edition=edition, trigger=trigger, services=[], + name='oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), edition=edition, trigger=trigger, services=[], steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + build_steps + package_steps + publish_steps, volumes=volumes, @@ -308,7 +288,7 @@ def get_oss_pipelines(trigger, ver_mode): ]) deps = { 'depends_on': [ - 'oss-build-publish{}-{}'.format(get_e2e_suffix(), ver_mode), + 'oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), 'oss-test-{}'.format(ver_mode), 'oss-integration-tests-{}'.format(ver_mode) ] diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 9c900fa4c64..ceac7d349df 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,9 +1,8 @@ load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket') -grabpl_version = 'v2.8.7' +grabpl_version = 'v2.9.0' build_image = 'grafana/build-container:1.4.9' publish_image = 'grafana/grafana-ci-deploy:1.3.1' -grafana_docker_image = 'grafana/drone-grafana-docker:0.3.2' deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image' alpine_image = 'alpine:3.15' curl_image = 'byrnedo/alpine-curl:0.1.8' @@ -317,8 +316,10 @@ def e2e_tests_artifacts(edition): def upload_cdn_step(edition, ver_mode): + src_dir = '' if ver_mode == "release": - bucket = "$${PRERELEASE_BUCKET}/artifacts/static-assets" + bucket = "$${PRERELEASE_BUCKET}" + src_dir = " --src-dir artifacts/static-assets" else: bucket = "grafana-static-assets" @@ -337,11 +338,11 @@ def upload_cdn_step(edition, ver_mode): 'image': publish_image, 'depends_on': deps, 'environment': { - 'GCP_GRAFANA_UPLOAD_KEY': from_secret('gcp_key'), + 'GCP_KEY': from_secret('gcp_key'), 'PRERELEASE_BUCKET': from_secret(prerelease_bucket) }, 'commands': [ - './bin/grabpl upload-cdn --edition {} --bucket "{}"'.format(edition, bucket), + './bin/grabpl upload-cdn --edition {} --src-bucket "{}"{}'.format(edition, bucket, src_dir), ], } @@ -729,8 +730,11 @@ def copy_packages_for_docker_step(): } -def package_docker_images_step(edition, ver_mode, archs=None, ubuntu=False, publish=False): - cmd = './bin/grabpl build-docker --edition {} --shouldSave'.format(edition) +def build_docker_images_step(edition, ver_mode, archs=None, ubuntu=False, publish=False): + cmd = './bin/grabpl build-docker --edition {}'.format(edition) + if publish: + cmd += ' --shouldSave' + ubuntu_sfx = '' if ubuntu: ubuntu_sfx = '-ubuntu' @@ -740,12 +744,10 @@ def package_docker_images_step(edition, ver_mode, archs=None, ubuntu=False, publ cmd += ' -archs {}'.format(','.join(archs)) return { - 'name': 'package-docker-images' + ubuntu_sfx, + 'name': 'build-docker-images' + ubuntu_sfx, 'image': 'google/cloud-sdk', 'depends_on': ['copy-packages-for-docker'], 'commands': [ - 'printenv GCP_KEY | base64 -d > /tmp/gcpkey.json', - 'gcloud auth activate-service-account --key-file=/tmp/gcpkey.json', cmd ], 'volumes': [{ @@ -757,27 +759,34 @@ def package_docker_images_step(edition, ver_mode, archs=None, ubuntu=False, publ }, } -def build_docker_images_step(edition, ver_mode, archs=None, ubuntu=False, publish=False): - ubuntu_sfx = '' - if ubuntu: - ubuntu_sfx = '-ubuntu' +def publish_images_step(edition, ver_mode, mode, docker_repo, ubuntu=False): + if mode == 'security': + mode = '--{} '.format(mode) + else: + mode = '' - settings = { - 'dry_run': not publish, - 'edition': edition, - 'ubuntu': ubuntu, - } + cmd = './bin/grabpl artifacts docker publish {}--dockerhub-repo {} --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7'.format(mode, docker_repo) + + if ver_mode == 'release': + deps = ['fetch-images-{}'.format(edition)] + cmd += ' --version-tag ${TAG}' + else: + deps = ['build-docker-images', 'build-docker-images-ubuntu'] - if publish: - settings['username'] = from_secret('docker_user') - settings['password'] = from_secret('docker_password') - if archs: - settings['archs'] = ','.join(archs) return { - 'name': 'build-docker-images' + ubuntu_sfx, - 'image': grafana_docker_image, - 'depends_on': ['copy-packages-for-docker'], - 'settings': settings, + 'name': 'publish-images-{}'.format(docker_repo), + 'image': 'google/cloud-sdk', + 'environment': { + 'GCP_KEY': from_secret('gcp_key'), + 'DOCKER_USER': from_secret('docker_username'), + 'DOCKER_PASSWORD': from_secret('docker_password'), + }, + 'commands': [cmd], + 'depends_on': deps, + 'volumes': [{ + 'name': 'docker', + 'path': '/var/run/docker.sock' + }], } @@ -923,7 +932,7 @@ def upload_packages_step(edition, ver_mode, is_downstream=False): 'image': publish_image, 'depends_on': deps, 'environment': { - 'GCP_GRAFANA_UPLOAD_KEY': from_secret('gcp_key'), + 'GCP_KEY': from_secret('gcp_key'), 'PRERELEASE_BUCKET': from_secret('prerelease_bucket'), }, 'commands': [cmd, ], @@ -960,7 +969,6 @@ def store_packages_step(edition, ver_mode, is_downstream=False): 'GPG_KEY_PASSWORD': from_secret('gpg_key_password'), }, 'commands': [ - 'printenv GCP_KEY | base64 -d > /tmp/gcpkey.json', cmd, ], } diff --git a/yarn.lock b/yarn.lock index 63b7649b880..b6e58e555e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,15 +5,6 @@ __metadata: version: 5 cacheKey: 8 -"@babel/code-frame@npm:7.10.4": - version: 7.10.4 - resolution: "@babel/code-frame@npm:7.10.4" - dependencies: - "@babel/highlight": ^7.10.4 - checksum: feb4543c8a509fe30f0f6e8d7aa84f82b41148b963b826cd330e34986f649a85cb63b2f13dd4effdf434ac555d16f14940b8ea5f4433297c2f5ff85486ded019 - languageName: node - linkType: hard - "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -3737,7 +3728,7 @@ __metadata: tinycolor2: 1.4.2 tslib: 2.3.1 typescript: 4.4.4 - uplot: 1.6.18 + uplot: 1.6.19 xss: 1.0.10 languageName: unknown linkType: soft @@ -3989,7 +3980,7 @@ __metadata: postcss-loader: 3.0.0 postcss-preset-env: 6.7.0 prettier: 2.5.1 - react-dev-utils: ^11.0.4 + react-dev-utils: ^12.0.0 replace-in-file: ^4.1.0 replace-in-file-webpack-plugin: ^1.0.6 rimraf: ^3.0.0 @@ -4042,6 +4033,7 @@ __metadata: "@monaco-editor/react": 4.3.1 "@popperjs/core": 2.11.2 "@react-aria/button": 3.3.4 + "@react-aria/dialog": 3.1.4 "@react-aria/focus": 3.5.0 "@react-aria/menu": 3.3.0 "@react-aria/overlays": 3.7.3 @@ -4050,20 +4042,20 @@ __metadata: "@rollup/plugin-image": 2.1.1 "@rollup/plugin-node-resolve": 13.1.3 "@sentry/browser": 6.17.2 - "@storybook/addon-a11y": 6.4.13 - "@storybook/addon-actions": 6.4.13 - "@storybook/addon-docs": 6.4.13 - "@storybook/addon-essentials": 6.4.13 + "@storybook/addon-a11y": 6.4.15 + "@storybook/addon-actions": 6.4.15 + "@storybook/addon-docs": 6.4.15 + "@storybook/addon-essentials": 6.4.15 "@storybook/addon-knobs": 6.4.0 - "@storybook/addon-storysource": 6.4.13 - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/builder-webpack5": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 - "@storybook/manager-webpack5": 6.4.13 - "@storybook/react": 6.4.13 - "@storybook/theming": 6.4.13 + "@storybook/addon-storysource": 6.4.15 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/builder-webpack5": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 + "@storybook/manager-webpack5": 6.4.15 + "@storybook/react": 6.4.15 + "@storybook/theming": 6.4.15 "@swc/helpers": 0.3.2 "@testing-library/dom": 8.11.1 "@testing-library/jest-dom": 5.16.1 @@ -4168,7 +4160,7 @@ __metadata: ts-loader: 8.0.11 tslib: 2.3.1 typescript: 4.4.4 - uplot: 1.6.18 + uplot: 1.6.19 uuid: 8.3.2 webpack: 5.68.0 webpack-filter-warnings-plugin: 1.2.1 @@ -6520,7 +6512,22 @@ __metadata: languageName: node linkType: hard -"@react-aria/focus@npm:3.5.0, @react-aria/focus@npm:^3.5.0": +"@react-aria/dialog@npm:3.1.4": + version: 3.1.4 + resolution: "@react-aria/dialog@npm:3.1.4" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.4.1 + "@react-aria/utils": ^3.8.2 + "@react-stately/overlays": ^3.1.3 + "@react-types/dialog": ^3.3.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: 968328a9b22e545ea79084cca2714f3bc7954c02eb0dc7cab78a1094d4c5dbd19520a0cc28e3dcbb7cb8dd7c505d6b9cb77fbb0fb4735fdf2e99908d481af3ab + languageName: node + linkType: hard + +"@react-aria/focus@npm:3.5.0, @react-aria/focus@npm:^3.4.1, @react-aria/focus@npm:^3.5.0": version: 3.5.0 resolution: "@react-aria/focus@npm:3.5.0" dependencies: @@ -6835,6 +6842,18 @@ __metadata: languageName: node linkType: hard +"@react-types/dialog@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-types/dialog@npm:3.3.1" + dependencies: + "@react-types/overlays": ^3.5.1 + "@react-types/shared": ^3.8.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: 6b0260e7018f26387c1d16585a048af5617f8b146d42d28439f0d4fd0fb041b1f004ca1a0a021a4e07a200a0045e34da8b2e0a3ff235272426b6815994e1c291 + languageName: node + linkType: hard + "@react-types/menu@npm:3.4.1, @react-types/menu@npm:^3.3.0": version: 3.4.1 resolution: "@react-types/menu@npm:3.4.1" @@ -7221,18 +7240,18 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-a11y@npm:6.4.13" +"@storybook/addon-a11y@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-a11y@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/channels": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/channels": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/theming": 6.4.13 + "@storybook/theming": 6.4.15 axe-core: ^4.2.0 core-js: ^3.8.2 global: ^4.4.0 @@ -7249,20 +7268,20 @@ __metadata: optional: true react-dom: optional: true - checksum: f353e0f2e6eead2e9bf8ed69557fe5c0f1104250b2a5c7117c9ff4a5713b7765f9eb05a74a00416f160c90baa27bb02dee80a1fa035a153e913bd1e87ee0f0c8 + checksum: 996081f2279001e2d855f950eece21ea098ac765f9b3749c5f9c109585bd2b293d1e35459d4ea9dc9fb6abccb115414fcd44fdcb4b6878d546f14d7778561eed languageName: node linkType: hard -"@storybook/addon-actions@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-actions@npm:6.4.13" +"@storybook/addon-actions@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-actions@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/theming": 6.4.13 + "@storybook/theming": 6.4.15 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -7283,21 +7302,21 @@ __metadata: optional: true react-dom: optional: true - checksum: ec6fd6faac6d66ba70dca8954ae980f4e40b8aa8e41a25a2e8d03daee89ac9dea7719deac7dd92ee891f50be86526361cf9c25d7488633be53e8a5f931902754 + checksum: 311e082bebc9a93372c90feb03ccf3ab0fb591acb69e580e1845d0f76f8d007de12b5667ea35dc83e6ed891ac3f5cf40871cc4902980e4079ba2cd11015dc99f languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-backgrounds@npm:6.4.13" +"@storybook/addon-backgrounds@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-backgrounds@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/theming": 6.4.13 + "@storybook/theming": 6.4.15 core-js: ^3.8.2 global: ^4.4.0 memoizerific: ^1.11.3 @@ -7312,23 +7331,23 @@ __metadata: optional: true react-dom: optional: true - checksum: e742199136e8a753e0853450b2c66c2d4715f979a28b2f284f5d044a453477f90dca51660e89f8529bfd1886df5fed665a3accdd22f54bd471b7073c1b9f1084 + checksum: 71ca7b45d0e3cc042394043959e45bce5f61dc0bfc003c01343b2153c84098abfab32853959c24ed393548bba65d4edc06f764f690855ed832dd30eba169e90b languageName: node linkType: hard -"@storybook/addon-controls@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-controls@npm:6.4.13" +"@storybook/addon-controls@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-controls@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-common": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-common": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/node-logger": 6.4.13 - "@storybook/store": 6.4.13 - "@storybook/theming": 6.4.13 + "@storybook/node-logger": 6.4.15 + "@storybook/store": 6.4.15 + "@storybook/theming": 6.4.15 core-js: ^3.8.2 lodash: ^4.17.21 ts-dedent: ^2.0.0 @@ -7340,13 +7359,13 @@ __metadata: optional: true react-dom: optional: true - checksum: e11985b2ddc857283f7e607ec4536dabf2ce4c14612e3576f22e0a27b92845cc252c4116ee72dd48fed9a26d7eabff6bb921509fc7ed24c0d6fb5d6967d29c52 + checksum: d7daa8cdd199d0c1b0d68c027977efa4b60e75aed25e0b60e79e0d4631afc41b6f2df4a6ffd880a014789e0e8b889eea81658e8b8ddaa89abfeba671ccc0f4fc languageName: node linkType: hard -"@storybook/addon-docs@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-docs@npm:6.4.13" +"@storybook/addon-docs@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-docs@npm:6.4.15" dependencies: "@babel/core": ^7.12.10 "@babel/generator": ^7.12.11 @@ -7357,21 +7376,21 @@ __metadata: "@mdx-js/loader": ^1.6.22 "@mdx-js/mdx": ^1.6.22 "@mdx-js/react": ^1.6.22 - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/builder-webpack4": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/builder-webpack4": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/csf-tools": 6.4.13 - "@storybook/node-logger": 6.4.13 - "@storybook/postinstall": 6.4.13 - "@storybook/preview-web": 6.4.13 - "@storybook/source-loader": 6.4.13 - "@storybook/store": 6.4.13 - "@storybook/theming": 6.4.13 + "@storybook/csf-tools": 6.4.15 + "@storybook/node-logger": 6.4.15 + "@storybook/postinstall": 6.4.15 + "@storybook/preview-web": 6.4.15 + "@storybook/source-loader": 6.4.15 + "@storybook/store": 6.4.15 + "@storybook/theming": 6.4.15 acorn: ^7.4.1 acorn-jsx: ^5.3.1 acorn-walk: ^7.2.0 @@ -7386,7 +7405,7 @@ __metadata: lodash: ^4.17.21 nanoid: ^3.1.23 p-limit: ^3.1.0 - prettier: <=2.3.0 + prettier: ">=2.2.1 <=2.3.0" prop-types: ^15.7.2 react-element-to-jsx-string: ^14.3.4 regenerator-runtime: ^0.13.7 @@ -7395,12 +7414,12 @@ __metadata: ts-dedent: ^2.0.0 util-deprecate: ^1.0.2 peerDependencies: - "@storybook/angular": 6.4.13 - "@storybook/html": 6.4.13 - "@storybook/react": 6.4.13 - "@storybook/vue": 6.4.13 - "@storybook/vue3": 6.4.13 - "@storybook/web-components": 6.4.13 + "@storybook/angular": 6.4.15 + "@storybook/html": 6.4.15 + "@storybook/react": 6.4.15 + "@storybook/vue": 6.4.15 + "@storybook/vue3": 6.4.15 + "@storybook/web-components": 6.4.15 lit: ^2.0.0 lit-html: ^1.4.1 || ^2.0.0 react: ^16.8.0 || ^17.0.0 @@ -7438,32 +7457,32 @@ __metadata: optional: true webpack: optional: true - checksum: dabf5b3dbf7ad53b11984caeb19fefe41e4ba17283bef08e07b9b556f7ff3e60301081281bc4031b44775bff06eb4d22bc5052bd4bc246983abeb1eb72a55c28 + checksum: a4245258c9497c50b025bf6c39d60e18c1ca5af736ac24f312f88a6376cb965d1fcc301d507eb859b0ef8b0761264fb0adcf288030ec1d889bf9a5c0f287a8a7 languageName: node linkType: hard -"@storybook/addon-essentials@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-essentials@npm:6.4.13" +"@storybook/addon-essentials@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-essentials@npm:6.4.15" dependencies: - "@storybook/addon-actions": 6.4.13 - "@storybook/addon-backgrounds": 6.4.13 - "@storybook/addon-controls": 6.4.13 - "@storybook/addon-docs": 6.4.13 - "@storybook/addon-measure": 6.4.13 - "@storybook/addon-outline": 6.4.13 - "@storybook/addon-toolbars": 6.4.13 - "@storybook/addon-viewport": 6.4.13 - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/node-logger": 6.4.13 + "@storybook/addon-actions": 6.4.15 + "@storybook/addon-backgrounds": 6.4.15 + "@storybook/addon-controls": 6.4.15 + "@storybook/addon-docs": 6.4.15 + "@storybook/addon-measure": 6.4.15 + "@storybook/addon-outline": 6.4.15 + "@storybook/addon-toolbars": 6.4.15 + "@storybook/addon-viewport": 6.4.15 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/node-logger": 6.4.15 core-js: ^3.8.2 regenerator-runtime: ^0.13.7 ts-dedent: ^2.0.0 peerDependencies: "@babel/core": ^7.9.6 - "@storybook/vue": 6.4.13 - "@storybook/web-components": 6.4.13 + "@storybook/vue": 6.4.15 + "@storybook/web-components": 6.4.15 babel-loader: ^8.0.0 lit-html: ^1.4.1 || ^2.0.0-rc.3 react: ^16.8.0 || ^17.0.0 @@ -7482,7 +7501,7 @@ __metadata: optional: true webpack: optional: true - checksum: a3d7e3be930fa878c2b5ec130934f5a88edbaf00aeaf6c8635d28df8790f3fdb8b8350ff09a27375d8103560a29cf9cd9f62f5a63a5b52ebcd72ec45aeed1b48 + checksum: 2e05c01380393b870898bca8f5965c9b236d9e31b27f748369620b4d732e6303f930147bd47240bce2416cc7740aa7679808eeba9e2caa41fbbbb1b55563dcae languageName: node linkType: hard @@ -7518,15 +7537,15 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-measure@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-measure@npm:6.4.13" +"@storybook/addon-measure@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-measure@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 core-js: ^3.8.2 global: ^4.4.0 @@ -7538,19 +7557,19 @@ __metadata: optional: true react-dom: optional: true - checksum: 9a0f7a86161d341924361df3073af1390b6a8eb267a6aa6fb9fa6d3712fd1b77f9dad729199c565fab18e9ed58049b9ffb704fc00085f81e29b88a61cc8db838 + checksum: fe19eeca1ed3957268c6e2d42c4f4e6ce58f513f097a7b5dc34576a4c2fdaa82584365d428119227f4180a7b3a10b3bdc4e88181675526403461c4a5371d9bf1 languageName: node linkType: hard -"@storybook/addon-outline@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-outline@npm:6.4.13" +"@storybook/addon-outline@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-outline@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 core-js: ^3.8.2 global: ^4.4.0 @@ -7564,25 +7583,25 @@ __metadata: optional: true react-dom: optional: true - checksum: fac6a3f366670e0bb6e41994bd63d612e77d09253e2e441d8cc931136c49dd4f8f8bb34c4a256bd93b693d0565b397a81218863bf959f90382eca66178d0161b + checksum: 6bbfd68754ddecd975f0ea720790973e79f91e9e01f9442fb1199c1adf049330ddd1c01806c0c6c3399a2cda6a9933b49e56b831d8842ce746e41ea70ff5a738 languageName: node linkType: hard -"@storybook/addon-storysource@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-storysource@npm:6.4.13" +"@storybook/addon-storysource@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-storysource@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/router": 6.4.13 - "@storybook/source-loader": 6.4.13 - "@storybook/theming": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/router": 6.4.15 + "@storybook/source-loader": 6.4.15 + "@storybook/theming": 6.4.15 core-js: ^3.8.2 estraverse: ^5.2.0 loader-utils: ^2.0.0 - prettier: <=2.3.0 + prettier: ">=2.2.1 <=2.3.0" prop-types: ^15.7.2 react-syntax-highlighter: ^13.5.3 regenerator-runtime: ^0.13.7 @@ -7594,18 +7613,18 @@ __metadata: optional: true react-dom: optional: true - checksum: 0d73dd01cecf718b7f828efc582ae5feaae8432855e279c9225855cdc829d6bf5550e3534dbab3abd7e16c5064218cc6d1c7366b39efd5be8477b50f57b0908a + checksum: d87b3bbd79c815a97e794b177267569cb2005b74b3f892bca324e560c433360986371b2c9156baf00586a06838845b2b0fe0503f5393830f9801c9456dbea749 languageName: node linkType: hard -"@storybook/addon-toolbars@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-toolbars@npm:6.4.13" +"@storybook/addon-toolbars@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-toolbars@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/theming": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/theming": 6.4.15 core-js: ^3.8.2 regenerator-runtime: ^0.13.7 peerDependencies: @@ -7616,20 +7635,20 @@ __metadata: optional: true react-dom: optional: true - checksum: d8b9bddfc1048a43d53ae12df56fb4193ad3102f74a065b66e2e8e26c7815c8f8f881c27c845f184264ebbc1f14286bed31acfb3c830eb5d517251f7c733529a + checksum: bdccfc40e9e6763c3a0f0787fb274c1b6c5c1b1375717d6637539df663575fe84df3e980148c9cc5ce0123e05c3b2635e151460f9989af0a7d46eed1a452e372 languageName: node linkType: hard -"@storybook/addon-viewport@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addon-viewport@npm:6.4.13" +"@storybook/addon-viewport@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addon-viewport@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 - "@storybook/theming": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 + "@storybook/theming": 6.4.15 core-js: ^3.8.2 global: ^4.4.0 memoizerific: ^1.11.3 @@ -7643,21 +7662,21 @@ __metadata: optional: true react-dom: optional: true - checksum: a7d78716f51778fb1b6a4df0e7f43c452f1db35396b0915c719d4b37c27c186de7ee29c67e09e1b64b7e17b557f09363f8b6aea4d94ed7b557c241667ed4a54a + checksum: 44fe3b03a17fd358bb1e47e37e483c4942adf03e55a0cb38b21a7070631bc20728443657250fc0ccf573722d77a28b9738d8a2c58ce3d8b5c1cfab308cfa6a78 languageName: node linkType: hard -"@storybook/addons@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/addons@npm:6.4.13" +"@storybook/addons@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/addons@npm:6.4.15" dependencies: - "@storybook/api": 6.4.13 - "@storybook/channels": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/api": 6.4.15 + "@storybook/channels": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/router": 6.4.13 - "@storybook/theming": 6.4.13 + "@storybook/router": 6.4.15 + "@storybook/theming": 6.4.15 "@types/webpack-env": ^1.16.0 core-js: ^3.8.2 global: ^4.4.0 @@ -7665,21 +7684,21 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 0df8b5b9c88e67edf1ec2d29a462d6acced43157a380bc90f399ab3df776a9310765340aa8bac755eeb2f592922b175f101a3b4540fdd9edeb67c7ba50eee4b2 + checksum: c853099f08ca2ac98826609709a05ba169634f357d93ba033a28820e063fd3a49fb2520d1ec63f87f5f9e7427b94487f57453bbb2fb579528d6742769dcee7e9 languageName: node linkType: hard -"@storybook/api@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/api@npm:6.4.13" +"@storybook/api@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/api@npm:6.4.15" dependencies: - "@storybook/channels": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/channels": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/router": 6.4.13 + "@storybook/router": 6.4.15 "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.4.13 + "@storybook/theming": 6.4.15 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -7693,13 +7712,13 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 8ba66efe619b4301a1b452f4f7cd58aaf648d99b7777838294b6ea623ce91043ebc2e1c63ed1ee78dfcca04edc1dec460402dda889773c157bcd2a79e5b0b1cc + checksum: 1f071611f5e0c42fa161deaacdf3606004cfdf1f444eddb31ec78afb803915d09b6964f4c8aba5416d2dd0ef0b24ed694107264ec80a1649544b7b37d033cb5e languageName: node linkType: hard -"@storybook/builder-webpack4@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/builder-webpack4@npm:6.4.13" +"@storybook/builder-webpack4@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/builder-webpack4@npm:6.4.15" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-proposal-class-properties": ^7.12.1 @@ -7722,22 +7741,22 @@ __metadata: "@babel/preset-env": ^7.12.11 "@babel/preset-react": ^7.12.10 "@babel/preset-typescript": ^7.12.7 - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/channel-postmessage": 6.4.13 - "@storybook/channels": 6.4.13 - "@storybook/client-api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-common": 6.4.13 - "@storybook/core-events": 6.4.13 - "@storybook/node-logger": 6.4.13 - "@storybook/preview-web": 6.4.13 - "@storybook/router": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/channel-postmessage": 6.4.15 + "@storybook/channels": 6.4.15 + "@storybook/client-api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-common": 6.4.15 + "@storybook/core-events": 6.4.15 + "@storybook/node-logger": 6.4.15 + "@storybook/preview-web": 6.4.15 + "@storybook/router": 6.4.15 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.4.13 - "@storybook/theming": 6.4.13 - "@storybook/ui": 6.4.13 + "@storybook/store": 6.4.15 + "@storybook/theming": 6.4.15 + "@storybook/ui": 6.4.15 "@types/node": ^14.0.10 "@types/webpack": ^4.41.26 autoprefixer: ^9.8.6 @@ -7776,13 +7795,13 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 4601c53d80f65be64ab231453c673a6e34f6e8f407f96fc0d161ffc123b939878ba2f36ef75e15df15000c238a93385eaa920ec497f9b4e2eedc03a39eaa9a14 + checksum: 43f1e999d1ac86887cf0d53862352d95aa26239807c010c46c890b02840b8fb7a7a446c77fa8c34524241665e8e432fbc74b73f4693536239a0d9ec6604e3a1b languageName: node linkType: hard -"@storybook/builder-webpack5@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/builder-webpack5@npm:6.4.13" +"@storybook/builder-webpack5@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/builder-webpack5@npm:6.4.15" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-proposal-class-properties": ^7.12.1 @@ -7804,21 +7823,21 @@ __metadata: "@babel/preset-env": ^7.12.11 "@babel/preset-react": ^7.12.10 "@babel/preset-typescript": ^7.12.7 - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/channel-postmessage": 6.4.13 - "@storybook/channels": 6.4.13 - "@storybook/client-api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-common": 6.4.13 - "@storybook/core-events": 6.4.13 - "@storybook/node-logger": 6.4.13 - "@storybook/preview-web": 6.4.13 - "@storybook/router": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/channel-postmessage": 6.4.15 + "@storybook/channels": 6.4.15 + "@storybook/client-api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-common": 6.4.15 + "@storybook/core-events": 6.4.15 + "@storybook/node-logger": 6.4.15 + "@storybook/preview-web": 6.4.15 + "@storybook/router": 6.4.15 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.4.13 - "@storybook/theming": 6.4.13 + "@storybook/store": 6.4.15 + "@storybook/theming": 6.4.15 "@types/node": ^14.0.10 babel-loader: ^8.0.0 babel-plugin-macros: ^3.0.1 @@ -7831,6 +7850,7 @@ __metadata: glob-promise: ^3.4.0 html-webpack-plugin: ^5.0.0 path-browserify: ^1.0.1 + process: ^0.11.10 stable: ^0.1.8 style-loader: ^2.0.0 terser-webpack-plugin: ^5.0.3 @@ -7846,60 +7866,60 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 44268548431b5dea86553ce4eb17073575203353fe78ad71f0fd184891f6d66d10fc7f30a087fcc638380978987e3a74201ed96648026532e85dfbe4c428db77 + checksum: b9be6ed8f093cbcb77bbbaccbe5af5e544e837f480500ed3ee4a3c595a14963cd7b4364d815dc965d5a01adeeb20f8a7fc6912be00b17199f944768cf1a2ec61 languageName: node linkType: hard -"@storybook/channel-postmessage@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/channel-postmessage@npm:6.4.13" +"@storybook/channel-postmessage@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/channel-postmessage@npm:6.4.15" dependencies: - "@storybook/channels": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/channels": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/core-events": 6.4.15 core-js: ^3.8.2 global: ^4.4.0 qs: ^6.10.0 telejson: ^5.3.2 - checksum: 459df7a9033ae89edb5bbdce1d096e0a508b130776c01f58fc8229112874487862c3c66de53a577a74e9aee5c552c88a85460730c86151bdf418cfba71388fb2 + checksum: f47439ca301cc29dbf55dd13166cb9c7fe44084b73f65cc729bd2beb3f4ca5af517f6b789de676793f1b2ee0fd902deb5a4fa619632874b277bc74b180f65f60 languageName: node linkType: hard -"@storybook/channel-websocket@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/channel-websocket@npm:6.4.13" +"@storybook/channel-websocket@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/channel-websocket@npm:6.4.15" dependencies: - "@storybook/channels": 6.4.13 - "@storybook/client-logger": 6.4.13 + "@storybook/channels": 6.4.15 + "@storybook/client-logger": 6.4.15 core-js: ^3.8.2 global: ^4.4.0 telejson: ^5.3.2 - checksum: 112421a3cd877d1f467ffbe33c9addfb5423d849203280457cb0adcab15970d029189e7402374ca830f8aa92ce143e0eb946181f89b3a50422c6ba556a2bef7d + checksum: d74ff19d83de1227e26d361ed24b503ee63353c00a20668bbd71b76f45fd8cf825ad698caaa69fd4bf2de383567bd2670a3f3981e8aa71081db32ae9d53029ab languageName: node linkType: hard -"@storybook/channels@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/channels@npm:6.4.13" +"@storybook/channels@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/channels@npm:6.4.15" dependencies: core-js: ^3.8.2 ts-dedent: ^2.0.0 util-deprecate: ^1.0.2 - checksum: 712bf9d72db1760ebdcfc6f6c8a91b12b529fdee2bd8ef6c60a8177602b3a5c3022563f7a26e3c5541bac14dabc627930d7704ecef57ed3b71f0c82c83c34f2d + checksum: 15467a58ddf1da417eb099e9569a85e65804d463f5928fdfa91825c412478d671af4606089d01d6e9ff06eb235bef5d2844bc7dd7a54b793ef480ad2a5cc05a0 languageName: node linkType: hard -"@storybook/client-api@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/client-api@npm:6.4.13" +"@storybook/client-api@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/client-api@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/channel-postmessage": 6.4.13 - "@storybook/channels": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/channel-postmessage": 6.4.15 + "@storybook/channels": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/store": 6.4.13 + "@storybook/store": 6.4.15 "@types/qs": ^6.9.5 "@types/webpack-env": ^1.16.0 core-js: ^3.8.2 @@ -7916,28 +7936,28 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: cb77e85059d5be80a482f79a12261999c6ed77f333aa2df6ae7f76776d6ad69c9ba698fa7f8e331baae6a64139531edda2ab2ef8b9c3d19aae332c39753a7a7f + checksum: f948d6bd8823fe84c1875171d2d77ed00e991d7f491ec6d1667f34fd58c4d92d800ddf7766450a959cbf378397361952b7d1c9f4e2925ee049e1ac79f44a728a languageName: node linkType: hard -"@storybook/client-logger@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/client-logger@npm:6.4.13" +"@storybook/client-logger@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/client-logger@npm:6.4.15" dependencies: core-js: ^3.8.2 global: ^4.4.0 - checksum: 2e0303ad45d705977300e3f207c2eeb0b1ecc3a47f7e9eef1f158c68d4d9f887e752120d78c285db9813907b9550952c41ad8d0a73380a0d0819d1222c79e4e8 + checksum: c4fd16d3743cc0baa3c6e8fb0598b12e0f9f34f712055110eefef87ce3e28ff37c6b5de597233718de78ebde492ebdfe0b3735810091b5c2ab684d1426320e1f languageName: node linkType: hard -"@storybook/components@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/components@npm:6.4.13" +"@storybook/components@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/components@npm:6.4.15" dependencies: "@popperjs/core": ^2.6.0 - "@storybook/client-logger": 6.4.13 + "@storybook/client-logger": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/theming": 6.4.13 + "@storybook/theming": 6.4.15 "@types/color-convert": ^2.0.0 "@types/overlayscrollbars": ^1.12.0 "@types/react-syntax-highlighter": 11.0.5 @@ -7961,24 +7981,24 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: c8454806dc98b6fe15006f70eeaaed574f325953e77a103da429d3a25ba07ce60b6845bcae15f5bd532c0c6d04276b442f1e2e5f0f7aee3f3b746b948a7c726b + checksum: a49885ff2ed49a565dd74da1e6ed8f24242324b807f318ea2d5540a8d780f88053cdcea9d68a34a6f90d45a37d80723e35800fdd53fa7e1ab1df07a65870c14b languageName: node linkType: hard -"@storybook/core-client@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/core-client@npm:6.4.13" +"@storybook/core-client@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/core-client@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/channel-postmessage": 6.4.13 - "@storybook/channel-websocket": 6.4.13 - "@storybook/client-api": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/channel-postmessage": 6.4.15 + "@storybook/channel-websocket": 6.4.15 + "@storybook/client-api": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/preview-web": 6.4.13 - "@storybook/store": 6.4.13 - "@storybook/ui": 6.4.13 + "@storybook/preview-web": 6.4.15 + "@storybook/store": 6.4.15 + "@storybook/ui": 6.4.15 airbnb-js-shims: ^2.2.1 ansi-to-html: ^0.6.11 core-js: ^3.8.2 @@ -7996,13 +8016,13 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 2affb7bef691a5428c8358d6c014a7582980bcee6c44d281f43ce6db3efad3a66aaac0e28daf8d2c3dbcc66e5c73d265aceb9f43b520860ed453fce859843c71 + checksum: b2ddd518076475e7dddcbe8bf1f81d945a0becedfc0bf14ee49ee4e1bd3fb213c91bf379f9dad6ffb84fc1ab117d7c35fac1a222334992980ac1aa6c58381f10 languageName: node linkType: hard -"@storybook/core-common@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/core-common@npm:6.4.13" +"@storybook/core-common@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/core-common@npm:6.4.15" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-proposal-class-properties": ^7.12.1 @@ -8025,7 +8045,7 @@ __metadata: "@babel/preset-react": ^7.12.10 "@babel/preset-typescript": ^7.12.7 "@babel/register": ^7.12.1 - "@storybook/node-logger": 6.4.13 + "@storybook/node-logger": 6.4.15 "@storybook/semver": ^7.3.2 "@types/node": ^14.0.10 "@types/pretty-hrtime": ^1.0.0 @@ -8059,34 +8079,34 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: d823f868ca3bcdbb6956532480c7a6bb03e1b314b7913ef68683da232f57ec5581d52a1076a35dda619a8c227a8ad7b7e74f81ed8a29fc057c12f46968fccd3e + checksum: a24aef6d2cdacadae35041ee07dfd0ac4192b1cd9cbbc3faa841d62ffde124da601728a35ca7e5821a725a030c29ab80607b556d2a9563e58b981c324124e486 languageName: node linkType: hard -"@storybook/core-events@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/core-events@npm:6.4.13" +"@storybook/core-events@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/core-events@npm:6.4.15" dependencies: core-js: ^3.8.2 - checksum: 157d88545f8dd6ec741f6a771f56ea12df749184123d39cbeaf1e879ae1da4bf87e14fd6c8b75577636a8f8fb49b6fa9461b375616e1dc17fba402453336bb2b + checksum: 72dfd7b0d8424955a1347b5fabd133512e87ef0a2b8c75a7a7bfe1a0f0a23ac2ad7f70be79f9aa0ce907f78b992cc274738f938facde28b122d55d0d62abbba5 languageName: node linkType: hard -"@storybook/core-server@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/core-server@npm:6.4.13" +"@storybook/core-server@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/core-server@npm:6.4.15" dependencies: "@discoveryjs/json-ext": ^0.5.3 - "@storybook/builder-webpack4": 6.4.13 - "@storybook/core-client": 6.4.13 - "@storybook/core-common": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/builder-webpack4": 6.4.15 + "@storybook/core-client": 6.4.15 + "@storybook/core-common": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/csf-tools": 6.4.13 - "@storybook/manager-webpack4": 6.4.13 - "@storybook/node-logger": 6.4.13 + "@storybook/csf-tools": 6.4.15 + "@storybook/manager-webpack4": 6.4.15 + "@storybook/node-logger": 6.4.15 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.4.13 + "@storybook/store": 6.4.15 "@types/node": ^14.0.10 "@types/node-fetch": ^2.5.7 "@types/pretty-hrtime": ^1.0.0 @@ -8119,8 +8139,8 @@ __metadata: webpack: 4 ws: ^8.2.3 peerDependencies: - "@storybook/builder-webpack5": 6.4.13 - "@storybook/manager-webpack5": 6.4.13 + "@storybook/builder-webpack5": 6.4.15 + "@storybook/manager-webpack5": 6.4.15 react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 peerDependenciesMeta: @@ -8130,18 +8150,18 @@ __metadata: optional: true typescript: optional: true - checksum: c033f049d5957c2d04afa1eeabc2f9f81d33065e19420f83858598e15b2f99a7567e8a7f2775f1f824e892f17ec63129f628b1e2aad188877a237bb96b47eba8 + checksum: 52be7c418ddaa60a4e9b8f268d5dea1d4dec1dd518efe8e57e8246b1c09d0290bae7b4c98b02ecc597b78f5dbd46df7e27cbd0cb4c31e51abab2677670193a7c languageName: node linkType: hard -"@storybook/core@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/core@npm:6.4.13" +"@storybook/core@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/core@npm:6.4.15" dependencies: - "@storybook/core-client": 6.4.13 - "@storybook/core-server": 6.4.13 + "@storybook/core-client": 6.4.15 + "@storybook/core-server": 6.4.15 peerDependencies: - "@storybook/builder-webpack5": 6.4.13 + "@storybook/builder-webpack5": 6.4.15 react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 webpack: "*" @@ -8150,13 +8170,13 @@ __metadata: optional: true typescript: optional: true - checksum: acdd691f0326e6dacd645b64704599f0c33e8e3f0fc60cddc411a90b6597bbf71c5d94e91da5ba8c737a1444bc1e074dad38f134289a9a407d6c990b71c73dbb + checksum: d3bb7166c4c5ce51a684b8aef89a57b84f6fcf197e83f31a0cf2b60406f56a94997830dc48cf947c622b5a1144c60bc08bb1d6a38bdb68c15c609b0c729f1d95 languageName: node linkType: hard -"@storybook/csf-tools@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/csf-tools@npm:6.4.13" +"@storybook/csf-tools@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/csf-tools@npm:6.4.15" dependencies: "@babel/core": ^7.12.10 "@babel/generator": ^7.12.11 @@ -8172,10 +8192,10 @@ __metadata: global: ^4.4.0 js-string-escape: ^1.0.1 lodash: ^4.17.21 - prettier: <=2.3.0 + prettier: ">=2.2.1 <=2.3.0" regenerator-runtime: ^0.13.7 ts-dedent: ^2.0.0 - checksum: e9fba470cdda298eea18d0ae2521300f66e0d28ee38035314ff135e5c039e1a66762b6dbe979d2eea1818e8c441aaf9d9a7c16a4c9c6c2db17a0af9e75480b03 + checksum: f9eb5b5f85ec4ac34c22b616d4536475dd61b66fe4d20f13e3dbcdab511cd7ee74c5ac8568eed3752ceac6d5411504e213f4fb764ddc2ed0c0e47ba36535a6df languageName: node linkType: hard @@ -8188,19 +8208,19 @@ __metadata: languageName: node linkType: hard -"@storybook/manager-webpack4@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/manager-webpack4@npm:6.4.13" +"@storybook/manager-webpack4@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/manager-webpack4@npm:6.4.15" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-transform-template-literals": ^7.12.1 "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.4.13 - "@storybook/core-client": 6.4.13 - "@storybook/core-common": 6.4.13 - "@storybook/node-logger": 6.4.13 - "@storybook/theming": 6.4.13 - "@storybook/ui": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/core-client": 6.4.15 + "@storybook/core-common": 6.4.15 + "@storybook/node-logger": 6.4.15 + "@storybook/theming": 6.4.15 + "@storybook/ui": 6.4.15 "@types/node": ^14.0.10 "@types/webpack": ^4.41.26 babel-loader: ^8.0.0 @@ -8234,23 +8254,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: ad2dd686faa60123a2d72d6e442a55d42f96660e0372d6cff6575110c6d00db17ba94e11aff65fe978707966b89a317fd125d4be4f4c8aed5de81d5ef402e3ba + checksum: 88345b8850261a06c829e0424ae9ff7fc803f123b76b022d2b946b91b4d9f2b60419e5f195a5b3d14f0cd354ae1eef8290a3876fb9078c7441aa091cf3fe9656 languageName: node linkType: hard -"@storybook/manager-webpack5@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/manager-webpack5@npm:6.4.13" +"@storybook/manager-webpack5@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/manager-webpack5@npm:6.4.15" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-transform-template-literals": ^7.12.1 "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.4.13 - "@storybook/core-client": 6.4.13 - "@storybook/core-common": 6.4.13 - "@storybook/node-logger": 6.4.13 - "@storybook/theming": 6.4.13 - "@storybook/ui": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/core-client": 6.4.15 + "@storybook/core-common": 6.4.15 + "@storybook/node-logger": 6.4.15 + "@storybook/theming": 6.4.15 + "@storybook/ui": 6.4.15 "@types/node": ^14.0.10 babel-loader: ^8.0.0 case-sensitive-paths-webpack-plugin: ^2.3.0 @@ -8263,6 +8283,7 @@ __metadata: fs-extra: ^9.0.1 html-webpack-plugin: ^5.0.0 node-fetch: ^2.6.1 + process: ^0.11.10 read-pkg-up: ^7.0.1 regenerator-runtime: ^0.13.7 resolve-from: ^5.0.0 @@ -8280,42 +8301,42 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 3eb0c58acf01172938e9aca593ca8a724e96053fe2f3cb09f1a9bdeb591800dd77835de9e5fae571ea73dcc2782d60349d15c83455500f71b81cbef907a52a7e + checksum: d81668a8bef94195095ed6981c8c6d9b9926fe5cc094fc8a9c36e5aa8875e02e4628f5bbf7423ee77a7f4eff0c9b55d56ad1b6cb04a4db39efff357e44673786 languageName: node linkType: hard -"@storybook/node-logger@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/node-logger@npm:6.4.13" +"@storybook/node-logger@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/node-logger@npm:6.4.15" dependencies: "@types/npmlog": ^4.1.2 chalk: ^4.1.0 core-js: ^3.8.2 npmlog: ^5.0.1 pretty-hrtime: ^1.0.3 - checksum: 7714ca70d1194882c54bf83e0f6e94944148fa1d6fc35457baaf8fc13df5bbfae835a1a9b1534a7baa398635e55349fdfca98ff36584063636899287a02bb92f + checksum: 3e37a29574b218f363c0687116d115caffe2c6e38380adb68abcd749f406fd15fc59c61b1cc21b91ff47eb2e003087935b9e6c6618deab4528b8464ff5409e52 languageName: node linkType: hard -"@storybook/postinstall@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/postinstall@npm:6.4.13" +"@storybook/postinstall@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/postinstall@npm:6.4.15" dependencies: core-js: ^3.8.2 - checksum: c5853f93d04560c2280d534aca9220816c3b5a12dfdd229807777b7be089b9f897d3ccada206e8c6f91a93309d41b7ed81fbc924111b9d5e2314cccb83785cf0 + checksum: fc81669e1772723e61f9b72084b98cfc3b6450a28c4f6e8884002fee917aca8816b3dc57ccd5cc67b9ff79541e800e15c3f80e65e2e7bfa7e75aec7abde51e47 languageName: node linkType: hard -"@storybook/preview-web@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/preview-web@npm:6.4.13" +"@storybook/preview-web@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/preview-web@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/channel-postmessage": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/channel-postmessage": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/store": 6.4.13 + "@storybook/store": 6.4.15 ansi-to-html: ^0.6.11 core-js: ^3.8.2 global: ^4.4.0 @@ -8329,7 +8350,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 1ab551fa0f8e0590cb9dfa2fd4d6794e61d11018da8f5aa849a9adf239b2edc62a5c6730f09c3aebcc40c1078ae11c82d8bd6a0855e7727178edf11261315d0c + checksum: 66d20941f612d9163852309e545a44f87c98b6dafb5416b3213e8c0ad2eed86a7c55febcba5224403d7de80dce7eeb5a36ff5eb56ca7da55ac47ccabb3d8d169 languageName: node linkType: hard @@ -8351,21 +8372,21 @@ __metadata: languageName: node linkType: hard -"@storybook/react@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/react@npm:6.4.13" +"@storybook/react@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/react@npm:6.4.15" dependencies: "@babel/preset-flow": ^7.12.1 "@babel/preset-react": ^7.12.10 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.1 - "@storybook/addons": 6.4.13 - "@storybook/core": 6.4.13 - "@storybook/core-common": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/core": 6.4.15 + "@storybook/core-common": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/node-logger": 6.4.13 + "@storybook/node-logger": 6.4.15 "@storybook/react-docgen-typescript-plugin": 1.0.2-canary.253f8c1.0 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.4.13 + "@storybook/store": 6.4.15 "@types/webpack-env": ^1.16.0 babel-plugin-add-react-displayname: ^0.0.5 babel-plugin-named-asset-import: ^0.3.1 @@ -8392,15 +8413,15 @@ __metadata: build-storybook: bin/build.js start-storybook: bin/index.js storybook-server: bin/index.js - checksum: 1b15dfb40f92c8b7efee5b67e7716ef5f9f07bc08215943a56f6d21d1e5a1c44c0b1a009117b24a57e716ae9e2e3788c606497e52973ac97e00ed5871ba26f0d + checksum: 8bd420f025af38029c89526d5374f904770be606e19d1ea10749846185a686d1674f07811f4151a3ba1958940738fc704075873528272ca10c1f65a6c0d1c744 languageName: node linkType: hard -"@storybook/router@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/router@npm:6.4.13" +"@storybook/router@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/router@npm:6.4.15" dependencies: - "@storybook/client-logger": 6.4.13 + "@storybook/client-logger": 6.4.15 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -8414,7 +8435,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 43247662c63c2f550d7797f2ebd6205d2e6ae42beb1a38584960bd64fad9cdeea2ae2792388989ffb306611b9469126d19b591e7bf47ce82fee694fdab7fc305 + checksum: 10bb86b31bfd4ae2f75986330328f935161ee68472a8401e8bec4c1cab820a2e991bc344ad04d0e595c79bf8f251d70cd39efe1b0685612fc9a48d607759c9f5 languageName: node linkType: hard @@ -8430,34 +8451,34 @@ __metadata: languageName: node linkType: hard -"@storybook/source-loader@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/source-loader@npm:6.4.13" +"@storybook/source-loader@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/source-loader@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/client-logger": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/client-logger": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 core-js: ^3.8.2 estraverse: ^5.2.0 global: ^4.4.0 loader-utils: ^2.0.0 lodash: ^4.17.21 - prettier: <=2.3.0 + prettier: ">=2.2.1 <=2.3.0" regenerator-runtime: ^0.13.7 peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: d5efb669a10b602936eaf8a195080d9dd500864ef83c7bf8c687e394d51a9e96ff80dcbc4f89373cc22cac39ec7eac90dd3a0ed4b4d09034c486b2f5d5c99772 + checksum: 85e91d62d541d8a41db70280dabcba700a85a24eb57dbe2973403d2733fd57e8575c88b69d40e6042ec9b3c451e87ea5bd121f3fd0853df656d55e8ec60306d9 languageName: node linkType: hard -"@storybook/store@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/store@npm:6.4.13" +"@storybook/store@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/store@npm:6.4.15" dependencies: - "@storybook/addons": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/core-events": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/core-events": 6.4.15 "@storybook/csf": 0.0.2--canary.87bc651.0 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 @@ -8473,18 +8494,18 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: d54d4567193e0c7a4f09fd6a1e88c2f2968503646c3e8dafaee971b1abca881d8aaa6c63122fd6df1d4a0df05db8f161556f8ee255a6a7a7cfd18f2e39beec99 + checksum: f0123dec020d19badaa3388137e8712f7607067fef976bfc2747f8818f58b22b2daed94c17ae5a91ba4058f8a8cf354bfd54e9dfdaff3e75e01cde14dc4a9998 languageName: node linkType: hard -"@storybook/theming@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/theming@npm:6.4.13" +"@storybook/theming@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/theming@npm:6.4.15" dependencies: "@emotion/core": ^10.1.1 "@emotion/is-prop-valid": ^0.8.6 "@emotion/styled": ^10.0.27 - "@storybook/client-logger": 6.4.13 + "@storybook/client-logger": 6.4.15 core-js: ^3.8.2 deep-object-diff: ^1.1.0 emotion-theming: ^10.0.27 @@ -8496,24 +8517,24 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: a0632a008dd428d0ca3ba032362c126415ed3ae01fe7e75b03a281117e7fb3c4cc61a45cf039fdc308ad158ce2aa398a299ad08bb63878dedae89861283d03aa + checksum: 2a9217bbc0ecbf85733fb64fc506e210e33897980fb9907aaa722412728391f702922a1c3ab53fd6c1c683268b72934f29c8c5b5f3232cf07c2674ea48216a49 languageName: node linkType: hard -"@storybook/ui@npm:6.4.13": - version: 6.4.13 - resolution: "@storybook/ui@npm:6.4.13" +"@storybook/ui@npm:6.4.15": + version: 6.4.15 + resolution: "@storybook/ui@npm:6.4.15" dependencies: "@emotion/core": ^10.1.1 - "@storybook/addons": 6.4.13 - "@storybook/api": 6.4.13 - "@storybook/channels": 6.4.13 - "@storybook/client-logger": 6.4.13 - "@storybook/components": 6.4.13 - "@storybook/core-events": 6.4.13 - "@storybook/router": 6.4.13 + "@storybook/addons": 6.4.15 + "@storybook/api": 6.4.15 + "@storybook/channels": 6.4.15 + "@storybook/client-logger": 6.4.15 + "@storybook/components": 6.4.15 + "@storybook/core-events": 6.4.15 + "@storybook/router": 6.4.15 "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.4.13 + "@storybook/theming": 6.4.15 copy-to-clipboard: ^3.3.1 core-js: ^3.8.2 core-js-pure: ^3.8.2 @@ -8535,7 +8556,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: f3f8cad91a91d682cc68cc4fb860999ea744b149f6e185845d2c6a407622d626674b6ec2903c10f2c81568659a74c7a012b393e236105db75c0874098b0157c5 + checksum: dc0f8b84964ca68cc90da007a32470897b5fc16889e234de3008842e770c586877b146ec19361b54b123e5f245c346cd79f60943eee619f76818ce93bed03e44 languageName: node linkType: hard @@ -11769,7 +11790,7 @@ __metadata: languageName: node linkType: hard -"address@npm:1.1.2, address@npm:^1.0.1": +"address@npm:^1.0.1, address@npm:^1.1.2": version: 1.1.2 resolution: "address@npm:1.1.2" checksum: d966deee6ab9a0f96ed1d25dc73e91a248f64479c91f9daeb15237b8e3c39a02faac4e6afe8987ef9e5aea60a1593cef5882b7456ab2e6196fc0229a93ec39c2 @@ -13372,20 +13393,6 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:4.14.2": - version: 4.14.2 - resolution: "browserslist@npm:4.14.2" - dependencies: - caniuse-lite: ^1.0.30001125 - electron-to-chromium: ^1.3.564 - escalade: ^3.0.2 - node-releases: ^1.1.61 - bin: - browserslist: cli.js - checksum: 44b5d7a444b867e1f027923f37a8ed537b4403f8a85a35869904e7d3e4071b37459df08d41ab4d425f5191f3125f1c5a191cbff9070f81f4d311803dc0a2fb0f - languageName: node - linkType: hard - "browserslist@npm:^4.0.0, browserslist@npm:^4.12.0, browserslist@npm:^4.14.5, browserslist@npm:^4.16.0, browserslist@npm:^4.16.6, browserslist@npm:^4.17.5, browserslist@npm:^4.6.4": version: 4.17.5 resolution: "browserslist@npm:4.17.5" @@ -13416,7 +13423,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.19.1": +"browserslist@npm:^4.18.1, browserslist@npm:^4.19.1": version: 4.19.1 resolution: "browserslist@npm:4.19.1" dependencies: @@ -13817,7 +13824,7 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001125, caniuse-lite@npm:^1.0.30001271": +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001271": version: 1.0.30001271 resolution: "caniuse-lite@npm:1.0.30001271" checksum: 0271e234e17c588fa203a34523f90c5a21ab62f41a2c3acabe96f0251593ac92f192cadd7a3dcbcf49bb28de4f57ca274d775c75700c3f38e9c42028d3becb0f @@ -13877,7 +13884,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:2.4.2, chalk@npm:^2.0.0, chalk@npm:^2.0.1, chalk@npm:^2.1.0, chalk@npm:^2.3.0, chalk@npm:^2.3.2, chalk@npm:^2.4.1, chalk@npm:^2.4.2": +"chalk@npm:^2.0.0, chalk@npm:^2.0.1, chalk@npm:^2.1.0, chalk@npm:^2.3.0, chalk@npm:^2.3.2, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" dependencies: @@ -14011,7 +14018,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -15196,17 +15203,6 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" - dependencies: - path-key: ^3.1.0 - shebang-command: ^2.0.0 - which: ^2.0.1 - checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 - languageName: node - linkType: hard - "cross-spawn@npm:^6.0.0, cross-spawn@npm:^6.0.5": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -15220,6 +15216,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + languageName: node + linkType: hard + "crypto-browserify@npm:^3.11.0": version: 3.12.0 resolution: "crypto-browserify@npm:3.12.0" @@ -16638,7 +16645,7 @@ __metadata: languageName: node linkType: hard -"detect-port-alt@npm:1.1.6": +"detect-port-alt@npm:^1.1.6": version: 1.1.6 resolution: "detect-port-alt@npm:1.1.6" dependencies: @@ -17049,7 +17056,7 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.3.564, electron-to-chromium@npm:^1.3.878": +"electron-to-chromium@npm:^1.3.878": version: 1.3.879 resolution: "electron-to-chromium@npm:1.3.879" checksum: 9f7bfa6c7b28e02f40aafe30865689e6859b8d667deeea8e5e0877ae77cd2f5c24a10177b01e93daa621135ed96e3ea1052dcf2f96bef7b896ea110c3b87c372 @@ -17504,7 +17511,7 @@ __metadata: languageName: node linkType: hard -"escalade@npm:^3.0.2, escalade@npm:^3.1.1": +"escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" checksum: a3e2a99f07acb74b3ad4989c48ca0c3140f69f923e56d0cba0526240ee470b91010f9d39001f2a4a313841d237ede70a729e92125191ba5d21e74b106800b133 @@ -17525,13 +17532,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:2.0.0, escape-string-regexp@npm:^2.0.0": - version: 2.0.0 - resolution: "escape-string-regexp@npm:2.0.0" - checksum: 9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395 - languageName: node - linkType: hard - "escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -17546,6 +17546,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395 + languageName: node + linkType: hard + "escodegen@npm:^1.11.1": version: 1.14.3 resolution: "escodegen@npm:1.14.3" @@ -18481,10 +18488,10 @@ __metadata: languageName: node linkType: hard -"filesize@npm:6.1.0": - version: 6.1.0 - resolution: "filesize@npm:6.1.0" - checksum: c46d644cb562fba7b7e837d5cd339394492abaa06722018b91a97d2a63b6c753ef30653de5c03bf178c631185bf55c3561c28fa9ccc4e9755f42d853c6ed4d09 +"filesize@npm:^8.0.6": + version: 8.0.7 + resolution: "filesize@npm:8.0.7" + checksum: 8603d27c5287b984cb100733640645e078f5f5ad65c6d913173e01fb99e09b0747828498fd86647685ccecb69be31f3587b9739ab1e50732116b2374aff4cbf9 languageName: node linkType: hard @@ -18560,16 +18567,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:4.1.0, find-up@npm:^4.0.0, find-up@npm:^4.1.0": - version: 4.1.0 - resolution: "find-up@npm:4.1.0" - dependencies: - locate-path: ^5.0.0 - path-exists: ^4.0.0 - checksum: 4c172680e8f8c1f78839486e14a43ef82e9decd0e74145f40707cc42e7420506d5ec92d9a11c22bd2c48fb0c384ea05dd30e10dd152fefeec6f2f75282a8b844 - languageName: node - linkType: hard - "find-up@npm:5.0.0, find-up@npm:^5.0.0": version: 5.0.0 resolution: "find-up@npm:5.0.0" @@ -18598,6 +18595,16 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": + version: 4.1.0 + resolution: "find-up@npm:4.1.0" + dependencies: + locate-path: ^5.0.0 + path-exists: ^4.0.0 + checksum: 4c172680e8f8c1f78839486e14a43ef82e9decd0e74145f40707cc42e7420506d5ec92d9a11c22bd2c48fb0c384ea05dd30e10dd152fefeec6f2f75282a8b844 + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.0.4 resolution: "flat-cache@npm:3.0.4" @@ -18701,21 +18708,6 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:4.1.6, fork-ts-checker-webpack-plugin@npm:^4.1.6": - version: 4.1.6 - resolution: "fork-ts-checker-webpack-plugin@npm:4.1.6" - dependencies: - "@babel/code-frame": ^7.5.5 - chalk: ^2.4.1 - micromatch: ^3.1.10 - minimatch: ^3.0.4 - semver: ^5.6.0 - tapable: ^1.0.0 - worker-rpc: ^0.1.0 - checksum: 4cc4fa7919dd9a0d765514d064c86e3a6f9cea8e700996b3e775cfcc0280f606a2dd16203d9b7e294b64e900795b0d80eb41fc8c192857d3350e407f14ef3eed - languageName: node - linkType: hard - "fork-ts-checker-webpack-plugin@npm:6.4.0, fork-ts-checker-webpack-plugin@npm:^6.0.4": version: 6.4.0 resolution: "fork-ts-checker-webpack-plugin@npm:6.4.0" @@ -18747,37 +18739,47 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:6.5.0": +"fork-ts-checker-webpack-plugin@npm:6.5.0, fork-ts-checker-webpack-plugin@npm:^6.5.0": version: 6.5.0 resolution: "fork-ts-checker-webpack-plugin@npm:6.5.0" dependencies: - "@babel/code-frame": ^7.8.3 - "@types/json-schema": ^7.0.5 - chalk: ^4.1.0 - chokidar: ^3.4.2 - cosmiconfig: ^6.0.0 + "@babel/code-frame": ^7.16.7 + chalk: ^4.1.2 + chokidar: ^3.5.3 + cosmiconfig: ^7.0.1 deepmerge: ^4.2.2 - fs-extra: ^9.0.0 - glob: ^7.1.6 - memfs: ^3.1.2 + fs-extra: ^10.0.0 + memfs: ^3.4.1 minimatch: ^3.0.4 - schema-utils: 2.7.0 - semver: ^7.3.2 - tapable: ^1.0.0 + schema-utils: 4.0.0 + semver: ^7.3.5 + tapable: ^2.2.1 peerDependencies: - eslint: ">= 6" - typescript: ">= 2.7" + typescript: ">3.6.0" vue-template-compiler: "*" - webpack: ">= 4" + webpack: ^5.11.0 peerDependenciesMeta: - eslint: - optional: true vue-template-compiler: optional: true checksum: 95d145ab7936445f3a9bfa4116ef73537f97196cfaa3f5b24473dff36d034e839d3b0e034a23beefc9619eceb7a9866816bfd55afd1968e955eb3b3f8cfc35ed languageName: node linkType: hard +"fork-ts-checker-webpack-plugin@npm:^4.1.6": + version: 4.1.6 + resolution: "fork-ts-checker-webpack-plugin@npm:4.1.6" + dependencies: + "@babel/code-frame": ^7.5.5 + chalk: ^2.4.1 + micromatch: ^3.1.10 + minimatch: ^3.0.4 + semver: ^5.6.0 + tapable: ^1.0.0 + worker-rpc: ^0.1.0 + checksum: 4cc4fa7919dd9a0d765514d064c86e3a6f9cea8e700996b3e775cfcc0280f606a2dd16203d9b7e294b64e900795b0d80eb41fc8c192857d3350e407f14ef3eed + languageName: node + linkType: hard + "form-data@npm:^3.0.0": version: 3.0.1 resolution: "form-data@npm:3.0.1" @@ -19411,7 +19413,7 @@ __metadata: languageName: node linkType: hard -"global-modules@npm:2.0.0, global-modules@npm:^2.0.0": +"global-modules@npm:^2.0.0": version: 2.0.0 resolution: "global-modules@npm:2.0.0" dependencies: @@ -19491,20 +19493,6 @@ __metadata: languageName: node linkType: hard -"globby@npm:11.0.1": - version: 11.0.1 - resolution: "globby@npm:11.0.1" - dependencies: - array-union: ^2.1.0 - dir-glob: ^3.0.1 - fast-glob: ^3.1.1 - ignore: ^5.1.4 - merge2: ^1.3.0 - slash: ^3.0.0 - checksum: b0b26e580666ef8caf0b0facd585c1da46eb971207ee9f8c7b690c1372d77602dd072f047f26c3ae1c293807fdf8fb6890d9291d37bc6d2602b1f07386f983e5 - languageName: node - linkType: hard - "globby@npm:^10.0.1": version: 10.0.2 resolution: "globby@npm:10.0.2" @@ -19664,6 +19652,7 @@ __metadata: "@pmmmwh/react-refresh-webpack-plugin": 0.5.4 "@popperjs/core": 2.11.2 "@react-aria/button": 3.3.4 + "@react-aria/dialog": 3.1.4 "@react-aria/focus": 3.5.0 "@react-aria/interactions": 3.7.0 "@react-aria/menu": 3.3.0 @@ -19907,7 +19896,7 @@ __metadata: ts-node: 10.4.0 tslib: 2.3.1 typescript: 4.4.4 - uplot: 1.6.18 + uplot: 1.6.19 uuid: 8.3.2 vendor: "link:./public/vendor" visjs-network: 4.25.0 @@ -19935,16 +19924,6 @@ __metadata: languageName: node linkType: hard -"gzip-size@npm:5.1.1": - version: 5.1.1 - resolution: "gzip-size@npm:5.1.1" - dependencies: - duplexer: ^0.1.1 - pify: ^4.0.1 - checksum: 6451ba2210877368f6d9ee9b4dc0d14501671472801323bf81fbd38bdeb8525f40a78be45a59d0182895d51e6b60c6314b7d02bd6ed40e7225a01e8d038aac1b - languageName: node - linkType: hard - "gzip-size@npm:^6.0.0": version: 6.0.0 resolution: "gzip-size@npm:6.0.0" @@ -20851,13 +20830,6 @@ __metadata: languageName: node linkType: hard -"immer@npm:8.0.1": - version: 8.0.1 - resolution: "immer@npm:8.0.1" - checksum: 63d875c04882b862481a0a692816e5982975a47a57619698bc1bb52963ad3b624911991708b2b81a7af6fdac15083d408e43932d83a6a61216e5a4503efd4095 - languageName: node - linkType: hard - "immer@npm:9.0.7, immer@npm:^9.0.7": version: 9.0.7 resolution: "immer@npm:9.0.7" @@ -21728,7 +21700,7 @@ __metadata: languageName: node linkType: hard -"is-root@npm:2.1.0": +"is-root@npm:^2.1.0": version: 2.1.0 resolution: "is-root@npm:2.1.0" checksum: 37eea0822a2a9123feb58a9d101558ba276771a6d830f87005683349a9acff15958a9ca590a44e778c6b335660b83e85c744789080d734f6081a935a4880aee2 @@ -24094,6 +24066,13 @@ __metadata: languageName: node linkType: hard +"loader-utils@npm:^3.2.0": + version: 3.2.0 + resolution: "loader-utils@npm:3.2.0" + checksum: c7b9a8dc4b3bc19e9ef563c48e3a18ea9f8bb2da1ad38a12e4b88358cfba5f148a7baf12d78fe78ffcb718ce1e062ab31fcf5c148459f1247a672a4213471e80 + languageName: node + linkType: hard + "locate-path@npm:^2.0.0": version: 2.0.0 resolution: "locate-path@npm:2.0.0" @@ -24719,6 +24698,15 @@ __metadata: languageName: node linkType: hard +"memfs@npm:^3.4.1": + version: 3.4.1 + resolution: "memfs@npm:3.4.1" + dependencies: + fs-monkey: 1.0.3 + checksum: 6d2f49d447d1be24ff9c747618933784eeb059189bc6a0d77b7a51c7daf06e2d3a74674a2e2ff1520e2c312bf91e719ed37144cf05087379b3ba0aef0b6aa062 + languageName: node + linkType: hard + "memoize-one@npm:6.0.0": version: 6.0.0 resolution: "memoize-one@npm:6.0.0" @@ -25815,13 +25803,6 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^1.1.61": - version: 1.1.77 - resolution: "node-releases@npm:1.1.77" - checksum: eb2fcb45310e7d77f82bfdadeca546a698d258e011f15d88ad9a452a5e838a672ec532906581096ca19c66284a788330c3b09227ffc540e67228730f41b9c2e2 - languageName: node - linkType: hard - "node-releases@npm:^2.0.1": version: 2.0.1 resolution: "node-releases@npm:2.0.1" @@ -26368,7 +26349,7 @@ __metadata: languageName: node linkType: hard -"open@npm:^7.0.2, open@npm:^7.0.3": +"open@npm:^7.0.3": version: 7.4.2 resolution: "open@npm:7.4.2" dependencies: @@ -26378,7 +26359,7 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.0.9": +"open@npm:^8.0.9, open@npm:^8.4.0": version: 8.4.0 resolution: "open@npm:8.4.0" dependencies: @@ -27214,7 +27195,7 @@ __metadata: languageName: node linkType: hard -"pkg-up@npm:3.1.0, pkg-up@npm:^3.1.0": +"pkg-up@npm:^3.1.0": version: 3.1.0 resolution: "pkg-up@npm:3.1.0" dependencies: @@ -28516,7 +28497,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:<=2.3.0": +"prettier@npm:>=2.2.1 <=2.3.0": version: 2.3.0 resolution: "prettier@npm:2.3.0" bin: @@ -28696,17 +28677,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:2.4.0": - version: 2.4.0 - resolution: "prompts@npm:2.4.0" - dependencies: - kleur: ^3.0.3 - sisteransi: ^1.0.5 - checksum: 96c7bef8eb3c0bb2076d2bc5ee473f06e6d8ac01ac4d0f378dfeb0ddaf2f31c339360ec8f0f8486f78601d16ebef7c6bd9886d44b937ba01bab568b937190265 - languageName: node - linkType: hard - -"prompts@npm:^2.0.1, prompts@npm:^2.4.0": +"prompts@npm:^2.0.1, prompts@npm:^2.4.0, prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -29541,35 +29512,35 @@ __metadata: languageName: node linkType: hard -"react-dev-utils@npm:^11.0.4": - version: 11.0.4 - resolution: "react-dev-utils@npm:11.0.4" +"react-dev-utils@npm:^12.0.0": + version: 12.0.0 + resolution: "react-dev-utils@npm:12.0.0" dependencies: - "@babel/code-frame": 7.10.4 - address: 1.1.2 - browserslist: 4.14.2 - chalk: 2.4.2 - cross-spawn: 7.0.3 - detect-port-alt: 1.1.6 - escape-string-regexp: 2.0.0 - filesize: 6.1.0 - find-up: 4.1.0 - fork-ts-checker-webpack-plugin: 4.1.6 - global-modules: 2.0.0 - globby: 11.0.1 - gzip-size: 5.1.1 - immer: 8.0.1 - is-root: 2.1.0 - loader-utils: 2.0.0 - open: ^7.0.2 - pkg-up: 3.1.0 - prompts: 2.4.0 - react-error-overlay: ^6.0.9 - recursive-readdir: 2.2.2 - shell-quote: 1.7.2 - strip-ansi: 6.0.0 - text-table: 0.2.0 - checksum: b41c95010a4fb60d4ea6309423520e6268757b68df34de7e9e8dbc72549236a1f5a698ff99ad72a034ac51b042aa79ee53994330ce4df05bf867e63c5464bb3f + "@babel/code-frame": ^7.16.0 + address: ^1.1.2 + browserslist: ^4.18.1 + chalk: ^4.1.2 + cross-spawn: ^7.0.3 + detect-port-alt: ^1.1.6 + escape-string-regexp: ^4.0.0 + filesize: ^8.0.6 + find-up: ^5.0.0 + fork-ts-checker-webpack-plugin: ^6.5.0 + global-modules: ^2.0.0 + globby: ^11.0.4 + gzip-size: ^6.0.0 + immer: ^9.0.7 + is-root: ^2.1.0 + loader-utils: ^3.2.0 + open: ^8.4.0 + pkg-up: ^3.1.0 + prompts: ^2.4.2 + react-error-overlay: ^6.0.10 + recursive-readdir: ^2.2.2 + shell-quote: ^1.7.3 + strip-ansi: ^6.0.1 + text-table: ^0.2.0 + checksum: d3be371c8e1e10877956ef8ceba77e8cb0c47440695d92ad3ca1734fdde77a1aa27a6c15dea9d82c873c7e6817e47d3d7410dfbeaff5c34508ffa9f5378dd1d1 languageName: node linkType: hard @@ -29718,10 +29689,10 @@ __metadata: languageName: node linkType: hard -"react-error-overlay@npm:^6.0.9": - version: 6.0.9 - resolution: "react-error-overlay@npm:6.0.9" - checksum: 695853bc885e798008a00c10d8d94e5ac91626e8130802fea37345f9c037f41b80104345db2ee87f225feb4a4ef71b0df572b17c378a6d397b6815f6d4a84293 +"react-error-overlay@npm:^6.0.10": + version: 6.0.10 + resolution: "react-error-overlay@npm:6.0.10" + checksum: e7384f086a0162eecac8e081fe3c79b32f4ac8690c56bde35ab6b6380d10e6c8375bbb689a450902b6615261fcf6c95ea016fc0b200934667089ca83536bc4a7 languageName: node linkType: hard @@ -30564,7 +30535,7 @@ __metadata: languageName: node linkType: hard -"recursive-readdir@npm:2.2.2": +"recursive-readdir@npm:^2.2.2": version: 2.2.2 resolution: "recursive-readdir@npm:2.2.2" dependencies: @@ -31590,6 +31561,18 @@ __metadata: languageName: node linkType: hard +"schema-utils@npm:4.0.0, schema-utils@npm:^4.0.0": + version: 4.0.0 + resolution: "schema-utils@npm:4.0.0" + dependencies: + "@types/json-schema": ^7.0.9 + ajv: ^8.8.0 + ajv-formats: ^2.1.1 + ajv-keywords: ^5.0.0 + checksum: c843e92fdd1a5c145dbb6ffdae33e501867f9703afac67bdf35a685e49f85b1dcc10ea250033175a64bd9d31f0555bc6785b8359da0c90bcea30cf6dfbb55a8f + languageName: node + linkType: hard + "schema-utils@npm:^1.0.0": version: 1.0.0 resolution: "schema-utils@npm:1.0.0" @@ -31623,18 +31606,6 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:^4.0.0": - version: 4.0.0 - resolution: "schema-utils@npm:4.0.0" - dependencies: - "@types/json-schema": ^7.0.9 - ajv: ^8.8.0 - ajv-formats: ^2.1.1 - ajv-keywords: ^5.0.0 - checksum: c843e92fdd1a5c145dbb6ffdae33e501867f9703afac67bdf35a685e49f85b1dcc10ea250033175a64bd9d31f0555bc6785b8359da0c90bcea30cf6dfbb55a8f - languageName: node - linkType: hard - "screenfull@npm:^5.1.0": version: 5.1.0 resolution: "screenfull@npm:5.1.0" @@ -31948,10 +31919,10 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:1.7.2": - version: 1.7.2 - resolution: "shell-quote@npm:1.7.2" - checksum: efad426fb25d8a54d06363f1f45774aa9e195f62f14fa696d542b44bfe418ab41206448b63af18d726c62e099e66d9a3f4f44858b9ea2ce4b794b41b802672d1 +"shell-quote@npm:^1.7.3": + version: 1.7.3 + resolution: "shell-quote@npm:1.7.3" + checksum: aca58e73a3a5d933d02e0bdddedc53ee14f7c2ec264f97ac915b9d4482d077a38e422aa664631d60a672cd3cdb4054eb2e6c0303f54882453dacb6483e482d34 languageName: node linkType: hard @@ -32963,15 +32934,6 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:6.0.0": - version: 6.0.0 - resolution: "strip-ansi@npm:6.0.0" - dependencies: - ansi-regex: ^5.0.0 - checksum: 04c3239ede44c4d195b0e66c0ad58b932f08bec7d05290416d361ff908ad282ecdaf5d9731e322c84f151d427436bde01f05b7422c3ec26dd927586736b0e5d0 - languageName: node - linkType: hard - "strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1": version: 3.0.1 resolution: "strip-ansi@npm:3.0.1" @@ -33437,7 +33399,7 @@ __metadata: languageName: node linkType: hard -"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0": +"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.1 resolution: "tapable@npm:2.2.1" checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 @@ -33708,7 +33670,7 @@ __metadata: languageName: node linkType: hard -"text-table@npm:0.2.0, text-table@npm:^0.2.0": +"text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" checksum: b6937a38c80c7f84d9c11dd75e49d5c44f71d95e810a3250bd1f1797fc7117c57698204adf676b71497acc205d769d65c16ae8fa10afad832ae1322630aef10a @@ -34941,10 +34903,10 @@ __metadata: languageName: node linkType: hard -"uplot@npm:1.6.18": - version: 1.6.18 - resolution: "uplot@npm:1.6.18" - checksum: 6e6365be1954b506fbe4f1cd905f53b7494df21bdad2fa71def4977521adb392c22df4568b195bc2081e6ebeb85e77afdb1c4db2d99cd1a2feb1c96a2330842e +"uplot@npm:1.6.19": + version: 1.6.19 + resolution: "uplot@npm:1.6.19" + checksum: 181cb158e920bc452940292f356eca4a75c31ebabbe9cd40b77e00a3ba90ac76e62e97fde9c06b45c4cd61e34734da0dbefb32c2689bf0d0c24b5d42aa38763a languageName: node linkType: hard