diff --git a/.drone.yml b/.drone.yml index fb6733b7760..d026d45679c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -28,10 +28,11 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - ./bin/grabpl verify-drone - make gen-go + - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - yarn install --immutable image: grafana/build-container:1.4.3 name: initialize @@ -145,20 +146,12 @@ steps: image: grafana/build-container:1.4.3 name: ensure-cuetsified - commands: - - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} + - . scripts/build/gpg-test-vars.sh && ./bin/grabpl package --jobs 8 --edition oss + --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise --variants linux-x64,linux-x64-musl,osx64,win64,armv6 depends_on: - build-plugins - build-backend - build-frontend - - codespell - - shellcheck - image: grafana/build-container:1.4.3 - name: gen-version -- commands: - - . scripts/build/gpg-test-vars.sh && ./bin/grabpl package --jobs 8 --edition oss - --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise --variants linux-x64,linux-x64-musl,osx64,win64,armv6 - depends_on: - - gen-version environment: null image: grafana/build-container:1.4.3 name: package @@ -184,7 +177,7 @@ steps: - yarn storybook:build - ./bin/grabpl verify-storybook depends_on: - - package + - build-frontend environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.4.3 @@ -220,7 +213,7 @@ steps: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - - end-to-end-tests-server + - package image: grafana/build-container:1.4.3 name: copy-packages-for-docker - depends_on: @@ -298,10 +291,11 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - ./bin/grabpl verify-drone - make gen-go + - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - yarn install --immutable image: grafana/build-container:1.4.3 name: initialize @@ -428,20 +422,12 @@ steps: image: grafana/build-container:1.4.3 name: ensure-cuetsified - commands: - - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} + - ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise + --sign depends_on: - build-plugins - build-backend - build-frontend - - codespell - - shellcheck - image: grafana/build-container:1.4.3 - name: gen-version -- commands: - - ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise - --sign - depends_on: - - gen-version environment: GITHUB_TOKEN: from_secret: github_token @@ -477,7 +463,7 @@ steps: - yarn storybook:build - ./bin/grabpl verify-storybook depends_on: - - package + - build-frontend environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.4.3 @@ -527,7 +513,7 @@ steps: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - - end-to-end-tests-server + - package image: grafana/build-container:1.4.3 name: copy-packages-for-docker - depends_on: @@ -637,7 +623,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -684,7 +670,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - ./bin/grabpl verify-drone - make gen-go @@ -772,11 +758,12 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - ./bin/grabpl verify-drone - make gen-go - ./bin/grabpl verify-version ${DRONE_TAG} + - ./bin/grabpl gen-version ${DRONE_TAG} - yarn install --immutable image: grafana/build-container:1.4.3 name: initialize @@ -894,20 +881,12 @@ steps: image: grafana/build-container:1.4.3 name: ensure-cuetsified - commands: - - ./bin/grabpl gen-version ${DRONE_TAG} + - ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise + --sign ${DRONE_TAG} depends_on: - build-plugins - build-backend - build-frontend - - codespell - - shellcheck - image: grafana/build-container:1.4.3 - name: gen-version -- commands: - - ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise - --sign ${DRONE_TAG} - depends_on: - - gen-version environment: GITHUB_TOKEN: from_secret: github_token @@ -943,7 +922,7 @@ steps: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - - end-to-end-tests-server + - package image: grafana/build-container:1.4.3 name: copy-packages-for-docker - depends_on: @@ -1006,7 +985,7 @@ steps: - yarn storybook:build - ./bin/grabpl verify-storybook depends_on: - - package + - build-frontend environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.4.3 @@ -1076,7 +1055,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -1143,7 +1122,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -1164,6 +1143,7 @@ steps: - ./bin/grabpl verify-drone - make gen-go - ./bin/grabpl verify-version ${DRONE_TAG} + - ./bin/grabpl gen-version ${DRONE_TAG} - yarn install --immutable depends_on: - clone @@ -1315,22 +1295,14 @@ steps: image: grafana/build-container:1.4.3 name: build-backend-enterprise2 - commands: - - ./bin/grabpl gen-version ${DRONE_TAG} + - ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN} + --no-pull-enterprise --sign ${DRONE_TAG} depends_on: - build-plugins - build-backend - build-frontend - - codespell - - shellcheck - build-backend-enterprise2 - test-backend-enterprise2 - image: grafana/build-container:1.4.3 - name: gen-version -- commands: - - ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN} - --no-pull-enterprise --sign ${DRONE_TAG} - depends_on: - - gen-version environment: GITHUB_TOKEN: from_secret: github_token @@ -1368,7 +1340,7 @@ steps: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - - end-to-end-tests-server + - package image: grafana/build-container:1.4.3 name: copy-packages-for-docker - depends_on: @@ -1473,7 +1445,11 @@ steps: - ./bin/grabpl package --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN} --no-pull-enterprise --sign ${DRONE_TAG} depends_on: - - gen-version + - build-plugins + - build-backend + - build-frontend + - build-backend-enterprise2 + - test-backend-enterprise2 environment: GITHUB_TOKEN: from_secret: github_token @@ -1554,7 +1530,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -1619,7 +1595,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - ./bin/grabpl verify-drone - make gen-go @@ -1726,11 +1702,12 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - ./bin/grabpl verify-drone - make gen-go - ./bin/grabpl verify-version v7.3.0-test + - ./bin/grabpl gen-version v7.3.0-test - yarn install --immutable image: grafana/build-container:1.4.3 name: initialize @@ -1848,20 +1825,12 @@ steps: image: grafana/build-container:1.4.3 name: ensure-cuetsified - commands: - - ./bin/grabpl gen-version v7.3.0-test + - ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise + --sign v7.3.0-test depends_on: - build-plugins - build-backend - build-frontend - - codespell - - shellcheck - image: grafana/build-container:1.4.3 - name: gen-version -- commands: - - ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise - --sign v7.3.0-test - depends_on: - - gen-version environment: GITHUB_TOKEN: from_secret: github_token @@ -1897,7 +1866,7 @@ steps: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - - end-to-end-tests-server + - package image: grafana/build-container:1.4.3 name: copy-packages-for-docker - depends_on: @@ -1952,7 +1921,7 @@ steps: - yarn storybook:build - ./bin/grabpl verify-storybook depends_on: - - package + - build-frontend environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.4.3 @@ -2018,7 +1987,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -2086,7 +2055,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2107,6 +2076,7 @@ steps: - ./bin/grabpl verify-drone - make gen-go - ./bin/grabpl verify-version v7.3.0-test + - ./bin/grabpl gen-version v7.3.0-test - yarn install --immutable depends_on: - clone @@ -2258,22 +2228,14 @@ steps: image: grafana/build-container:1.4.3 name: build-backend-enterprise2 - commands: - - ./bin/grabpl gen-version v7.3.0-test + - ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN} + --no-pull-enterprise --sign v7.3.0-test depends_on: - build-plugins - build-backend - build-frontend - - codespell - - shellcheck - build-backend-enterprise2 - test-backend-enterprise2 - image: grafana/build-container:1.4.3 - name: gen-version -- commands: - - ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN} - --no-pull-enterprise --sign v7.3.0-test - depends_on: - - gen-version environment: GITHUB_TOKEN: from_secret: github_token @@ -2311,7 +2273,7 @@ steps: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - - end-to-end-tests-server + - package image: grafana/build-container:1.4.3 name: copy-packages-for-docker - depends_on: @@ -2408,7 +2370,11 @@ steps: - ./bin/grabpl package --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN} --no-pull-enterprise --sign v7.3.0-test depends_on: - - gen-version + - build-plugins + - build-backend + - build-frontend + - build-backend-enterprise2 + - test-backend-enterprise2 environment: GITHUB_TOKEN: from_secret: github_token @@ -2489,7 +2455,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2555,7 +2521,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - ./bin/grabpl verify-drone - make gen-go @@ -2667,10 +2633,11 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - ./bin/grabpl verify-drone - make gen-go + - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - yarn install --immutable image: grafana/build-container:1.4.3 name: initialize @@ -2786,20 +2753,12 @@ steps: image: grafana/build-container:1.4.3 name: ensure-cuetsified - commands: - - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} + - ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise + --sign depends_on: - build-plugins - build-backend - build-frontend - - codespell - - shellcheck - image: grafana/build-container:1.4.3 - name: gen-version -- commands: - - ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise - --sign - depends_on: - - gen-version environment: GITHUB_TOKEN: from_secret: github_token @@ -2835,7 +2794,7 @@ steps: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - - end-to-end-tests-server + - package image: grafana/build-container:1.4.3 name: copy-packages-for-docker - depends_on: @@ -2890,7 +2849,7 @@ steps: - yarn storybook:build - ./bin/grabpl verify-storybook depends_on: - - package + - build-frontend environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.4.3 @@ -2936,7 +2895,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -2999,7 +2958,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl - chmod +x bin/grabpl - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -3019,6 +2978,7 @@ steps: - mv /tmp/grabpl bin/ - ./bin/grabpl verify-drone - make gen-go + - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - yarn install --immutable depends_on: - clone @@ -3166,22 +3126,14 @@ steps: image: grafana/build-container:1.4.3 name: build-backend-enterprise2 - commands: - - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} + - ./bin/grabpl package --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} + --no-pull-enterprise --sign depends_on: - build-plugins - build-backend - build-frontend - - codespell - - shellcheck - build-backend-enterprise2 - test-backend-enterprise2 - image: grafana/build-container:1.4.3 - name: gen-version -- commands: - - ./bin/grabpl package --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} - --no-pull-enterprise --sign - depends_on: - - gen-version environment: GITHUB_TOKEN: from_secret: github_token @@ -3219,7 +3171,7 @@ steps: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - - end-to-end-tests-server + - package image: grafana/build-container:1.4.3 name: copy-packages-for-docker - depends_on: @@ -3274,7 +3226,7 @@ steps: - yarn storybook:build - ./bin/grabpl verify-storybook depends_on: - - package + - build-frontend environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.4.3 @@ -3325,7 +3277,11 @@ steps: - ./bin/grabpl package --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise --variants linux-x64 --sign depends_on: - - gen-version + - build-plugins + - build-backend + - build-frontend + - build-backend-enterprise2 + - test-backend-enterprise2 environment: GITHUB_TOKEN: from_secret: github_token @@ -3406,7 +3362,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -3529,6 +3485,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 96d23b144a0a27bce871f7faf782827c17cc132492fe058a7c56768c543078c9 +hmac: 0d26eda19dc6e530d02c734704c8a9d01beb5082c7e14b2609577b8695ed06f5 ... diff --git a/.gitignore b/.gitignore index 2f9e471d08e..b0f2c0dc90a 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ public/css/*.min.css !.vscode/launch.json .vs/ .eslintcache +.stylelintcache /data/* /bin/* diff --git a/CHANGELOG.md b/CHANGELOG.md index d7afd0b2bd0..e6b8c312b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ + + +# 8.2.2 (2021-10-21) + +### Features and enhancements + +- **Annotations:** We have improved tag search performance. [#40567](https://github.com/grafana/grafana/pull/40567), [@ashharrison90](https://github.com/ashharrison90) +- **Application:** You can now configure an error-template title. [#40310](https://github.com/grafana/grafana/pull/40310), [@benrubson](https://github.com/benrubson) +- **AzureMonitor:** We removed a restriction from the resource filter query. [#40690](https://github.com/grafana/grafana/pull/40690), [@andresmgot](https://github.com/andresmgot) +- **Caching:** Make cache size metric collection optional. (Enterprise) +- **Packaging:** We removed the ProcSubset option in systemd. This option prevented Grafana from starting in LXC environments. [#40339](https://github.com/grafana/grafana/pull/40339), [@kminehart](https://github.com/kminehart) +- **Prometheus:** We removed the autocomplete limit for metrics. [#39363](https://github.com/grafana/grafana/pull/39363), [@ivanahuckova](https://github.com/ivanahuckova) +- **Request interceptor:** Allow MSSQL's named instances. (Enterprise) +- **Table:** We improved the styling of the type icons to make them more distinct from column / field name. [#40596](https://github.com/grafana/grafana/pull/40596), [@torkelo](https://github.com/torkelo) +- **ValueMappings:** You can now use value mapping in stat, gauge, bar gauge, and pie chart visualizations. [#40612](https://github.com/grafana/grafana/pull/40612), [@torkelo](https://github.com/torkelo) + +### Bug fixes + +- **Alerting:** Fix panic when Slack's API sends unexpected response. [#40721](https://github.com/grafana/grafana/pull/40721), [@santihernandezc](https://github.com/santihernandezc) +- **Alerting:** The Create Alert button now appears on the dashboard panel when you are working with a default datasource. [#40334](https://github.com/grafana/grafana/pull/40334), [@domasx2](https://github.com/domasx2) +- **Explore:** We fixed the problem where the Explore log panel disappears when an Elasticsearch logs query returns no results. [#40217](https://github.com/grafana/grafana/pull/40217), [@Elfo404](https://github.com/Elfo404) +- **Graph:** You can now see annotation descriptions on hover. [#40581](https://github.com/grafana/grafana/pull/40581), [@axelavargas](https://github.com/axelavargas) +- **Logs:** The system now uses the JSON parser only if the line is parsed to an object. [#40507](https://github.com/grafana/grafana/pull/40507), [@ivanahuckova](https://github.com/ivanahuckova) +- **Prometheus:** We fixed the issue where the system did not reuse TCP connections when querying from Grafana alerting. [#40349](https://github.com/grafana/grafana/pull/40349), [@kminehart](https://github.com/kminehart) +- **Prometheus:** We fixed the problem that resulted in an error when a user created a query with a $\_\_interval min step. [#40525](https://github.com/grafana/grafana/pull/40525), [@ivanahuckova](https://github.com/ivanahuckova) +- **RowsToFields:** We fixed the issue where the system was not properly interpreting number values. [#40580](https://github.com/grafana/grafana/pull/40580), [@torkelo](https://github.com/torkelo) +- **Scale:** We fixed how the system handles NaN percent when data min = data max. [#40622](https://github.com/grafana/grafana/pull/40622), [@torkelo](https://github.com/torkelo) +- **Table panel:** You can now create a filter that includes special characters. [#40458](https://github.com/grafana/grafana/pull/40458), [@dprokop](https://github.com/dprokop) + + # 8.2.1 (2021-10-11) diff --git a/docs/sources/alerting/unified-alerting/alert-groups.md b/docs/sources/alerting/unified-alerting/alert-groups.md new file mode 100644 index 00000000000..3aeca4cc782 --- /dev/null +++ b/docs/sources/alerting/unified-alerting/alert-groups.md @@ -0,0 +1,27 @@ ++++ +title = "Alert groups" +description = "Alert groups" +keywords = ["grafana", "alerting", "alerts", "groups"] +weight = 400 ++++ + +# View alert groups + +Alert groups shows grouped alerts from an alertmanager instance. Alertmanager will group alerts based on common label values. This prevents duplicate alerts from being fired by grouping common alerts into a single alert group. By default, the alerts are grouped by the label keys for the root policy in [notification policies]({{< relref "./notification-policies.md" >}}). + +## Show alert groups for an external Alertmanager + +Grafana alerting UI supports alert groups from external Alertmanager data sources. Once you add an [Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}), a dropdown displays at the top of the page where you can select either `Grafana` or an external Alertmanager as your data source. + +## View different alert groupings + +To view a grouping other than the default use the **custom group by** dropdown to select combinations of labels to group alerts by. This is useful for debugging and verifying your notification policies grouping. + +If an alert does not contain labels specified in the grouping of the route policy or the custom grouping it will be added to a catch all group with a header of `No grouping`. + +## Filter alerts + +You can use the following filters to view only alerts that match specific criteria: + +- **Filter alerts by label -** Search by alert labels using label selectors in the **Search** input. eg: `environment=production,region=~US|EU,severity!=warning` +- **Filter alerts by state -** In **States** Select which alert states you want to see. All others are hidden. diff --git a/docs/sources/alerting/unified-alerting/contact-points.md b/docs/sources/alerting/unified-alerting/contact-points.md index 61ba3029a88..3f947acac0b 100644 --- a/docs/sources/alerting/unified-alerting/contact-points.md +++ b/docs/sources/alerting/unified-alerting/contact-points.md @@ -179,7 +179,6 @@ consists of an array of alerts. #### Removed fields related to dashboards Alerts are not coupled to dashboards anymore therefore the fields related to dashboards `dashboardId` and `panelId` have been removed. -where removed. The removed fields are `dashboardId` and `panelId`. ## Manage contact points for an external Alertmanager diff --git a/docs/sources/alerting/unified-alerting/silences.md b/docs/sources/alerting/unified-alerting/silences.md index c013c4d0089..8686e806064 100644 --- a/docs/sources/alerting/unified-alerting/silences.md +++ b/docs/sources/alerting/unified-alerting/silences.md @@ -33,9 +33,14 @@ To add a silence: Alert instances that have labels that match all of the "Matching Labels" specified in the silence will have their notifications suppressed. - The **Label** field is the name of the label to match. It must exactly match the label name. -- The **Value** field matches against the corresponding value for the specified **Label** name. How it matches depends on the **Regex** and **Equal** checkboxes. -- The **Regex** checkbox specifies if the inputted **Value** should be matched against labels as a regular expression. The regular expression is always anchored. If not selected it is an exact string match. -- The **Equal** checkbox specifies if the match should include alert instances that match or do not match. If not checked, the silence includes alert instances _do not_ match. +- The **Operator** field is the operator to match against the label value. The available operators are: + + - `=`: Select labels that are exactly equal to the provided string. + - `!=`: Select labels that are not equal to the provided string. + - `=~`: Select labels that regex-match the provided string. + - `!~`: Select labels that do not regex-match the provided string. + +- The **Value** field matches against the corresponding value for the specified **Label** name. How it matches depends on the **Operator** value. ## Viewing and editing silences @@ -52,6 +57,6 @@ Grafana alerting UI supports managing external Alertmanager silences. Once you a When linking to a silence form, provide the default matching labels and comment via `matchers` and `comment` query parameters. The `matchers` parameter requires one more matching labels of the type `[label][operator][value]` joined by a comma while the `operator` parameter can be one of the following: `=` (equals, not regex), `!=` (not equals, not regex), `=~` (equals, regex), `!~` (not equals, regex). -For example, to link to silence form with matching labels `severity=critical` & `cluster!~europe-.*` and comment `Silence critical EU alerts`, create a URL `https://mygrafana/aleting/silence/new?matchers=severity%3Dcritical%2Ccluster!~europe-*&comment=Silence%20critical%20EU%20alert`. +For example, to link to silence form with matching labels `severity=critical` & `cluster!~europe-.*` and comment `Silence critical EU alerts`, create a URL `https://mygrafana/alerting/silence/new?matchers=severity%3Dcritical%2Ccluster!~europe-*&comment=Silence%20critical%20EU%20alert`. To link to a new silence page for an [external Alertmanager]({{< relref "../../datasources/alertmanager.md" >}}), add a `alertmanager` query parameter with the Alertmanager data source name. diff --git a/docs/sources/datasources/influxdb/influxdb-flux.md b/docs/sources/datasources/influxdb/influxdb-flux.md index edb5d62dd75..c0fa0eb5b80 100644 --- a/docs/sources/datasources/influxdb/influxdb-flux.md +++ b/docs/sources/datasources/influxdb/influxdb-flux.md @@ -40,7 +40,7 @@ You can use the [Flux query and scripting language](https://www.influxdata.com/p ## Supported macros -The macros support copying and pasting from [Chronograph](https://www.influxdata.com/time-series-platform/chronograf/). +The macros support copying and pasting from [Chronograf](https://www.influxdata.com/time-series-platform/chronograf/). | Macro example | Description | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/docs/sources/http_api/dashboard_versions.md b/docs/sources/http_api/dashboard_versions.md index e15bec235ac..7e301d390b3 100644 --- a/docs/sources/http_api/dashboard_versions.md +++ b/docs/sources/http_api/dashboard_versions.md @@ -69,7 +69,7 @@ Status Codes: `GET /api/dashboards/id/:dashboardId/versions/:id` -Get the dashboard version with the given id, for the dashboard with the given id. +Get the dashboard version with the given version, for the dashboard with the given id. **Example request for getting a dashboard version**: diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 01e35b3506a..ad8996429c0 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -8,6 +8,7 @@ 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.2.2]({{< relref "release-notes-8-2-2" >}}) - [Release notes for 8.2.1]({{< relref "release-notes-8-2-1" >}}) - [Release notes for 8.2.0]({{< relref "release-notes-8-2-0" >}}) - [Release notes for 8.2.0-beta2]({{< relref "release-notes-8-2-0-beta2" >}}) diff --git a/docs/sources/release-notes/release-notes-8-2-2.md b/docs/sources/release-notes/release-notes-8-2-2.md new file mode 100644 index 00000000000..2935a012705 --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-2-2.md @@ -0,0 +1,34 @@ ++++ +title = "Release notes for Grafana 8.2.2" +[_build] +list = false ++++ + + + +# Release notes for Grafana 8.2.2 + +### Features and enhancements + +- **Annotations:** We have improved tag search performance. [#40567](https://github.com/grafana/grafana/pull/40567), [@ashharrison90](https://github.com/ashharrison90) +- **Application:** You can now configure an error-template title. [#40310](https://github.com/grafana/grafana/pull/40310), [@benrubson](https://github.com/benrubson) +- **AzureMonitor:** We removed a restriction from the resource filter query. [#40690](https://github.com/grafana/grafana/pull/40690), [@andresmgot](https://github.com/andresmgot) +- **Caching:** Make cache size metric collection optional. (Enterprise) +- **Packaging:** We removed the ProcSubset option in systemd. This option prevented Grafana from starting in LXC environments. [#40339](https://github.com/grafana/grafana/pull/40339), [@kminehart](https://github.com/kminehart) +- **Prometheus:** We removed the autocomplete limit for metrics. [#39363](https://github.com/grafana/grafana/pull/39363), [@ivanahuckova](https://github.com/ivanahuckova) +- **Request interceptor:** Allow MSSQL's named instances. (Enterprise) +- **Table:** We improved the styling of the type icons to make them more distinct from column / field name. [#40596](https://github.com/grafana/grafana/pull/40596), [@torkelo](https://github.com/torkelo) +- **ValueMappings:** You can now use value mapping in stat, gauge, bar gauge, and pie chart visualizations. [#40612](https://github.com/grafana/grafana/pull/40612), [@torkelo](https://github.com/torkelo) + +### Bug fixes + +- **Alerting:** Fix panic when Slack's API sends unexpected response. [#40721](https://github.com/grafana/grafana/pull/40721), [@santihernandezc](https://github.com/santihernandezc) +- **Alerting:** The Create Alert button now appears on the dashboard panel when you are working with a default datasource. [#40334](https://github.com/grafana/grafana/pull/40334), [@domasx2](https://github.com/domasx2) +- **Explore:** We fixed the problem where the Explore log panel disappears when an Elasticsearch logs query returns no results. [#40217](https://github.com/grafana/grafana/pull/40217), [@Elfo404](https://github.com/Elfo404) +- **Graph:** You can now see annotation descriptions on hover. [#40581](https://github.com/grafana/grafana/pull/40581), [@axelavargas](https://github.com/axelavargas) +- **Logs:** The system now uses the JSON parser only if the line is parsed to an object. [#40507](https://github.com/grafana/grafana/pull/40507), [@ivanahuckova](https://github.com/ivanahuckova) +- **Prometheus:** We fixed the issue where the system did not reuse TCP connections when querying from Grafana alerting. [#40349](https://github.com/grafana/grafana/pull/40349), [@kminehart](https://github.com/kminehart) +- **Prometheus:** We fixed the problem that resulted in an error when a user created a query with a $\_\_interval min step. [#40525](https://github.com/grafana/grafana/pull/40525), [@ivanahuckova](https://github.com/ivanahuckova) +- **RowsToFields:** We fixed the issue where the system was not properly interpreting number values. [#40580](https://github.com/grafana/grafana/pull/40580), [@torkelo](https://github.com/torkelo) +- **Scale:** We fixed how the system handles NaN percent when data min = data max. [#40622](https://github.com/grafana/grafana/pull/40622), [@torkelo](https://github.com/torkelo) +- **Table panel:** You can now create a filter that includes special characters. [#40458](https://github.com/grafana/grafana/pull/40458), [@dprokop](https://github.com/dprokop) diff --git a/package.json b/package.json index 2ee0bb294e7..8e53525d566 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:accessibility-report": "./scripts/generate-a11y-report.sh", "lint": "yarn run lint:ts && yarn run lint:sass", "lint:ts": "eslint . --ext .js,.tsx,.ts --cache", - "lint:sass": "yarn run sass-lint -c public/sass/.sass-lint.yml 'public/sass/**/*.scss, packages/**/*.scss' -v -i '**/node_modules/**/*.scss'", + "lint:sass": "yarn stylelint '{public/sass,packages}/**/*.scss' --cache", "test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%}", "lint:fix": "yarn lint --fix", "packages:build": "lerna run clean && lerna run build --ignore @grafana-plugins/input-datasource", @@ -90,7 +90,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-rc.6", "@testing-library/jest-dom": "5.11.5", "@testing-library/react": "12.1.2", - "@testing-library/react-hooks": "^3.2.1", + "@testing-library/react-hooks": "7.0.2", "@testing-library/user-event": "13.3.0", "@types/angular": "1.6.56", "@types/angular-route": "1.7.0", @@ -120,7 +120,7 @@ "@types/rc-time-picker": "^3", "@types/react": "17.0.30", "@types/react-beautiful-dnd": "13.1.1", - "@types/react-dom": "16.9.9", + "@types/react-dom": "17.0.10", "@types/react-grid-layout": "1.1.1", "@types/react-highlight-words": "^0.16.2", "@types/react-loadable": "5.5.2", @@ -207,10 +207,12 @@ "rimraf": "3.0.1", "rxjs-spy": "8.0.0", "sass": "1.32.13", - "sass-lint": "1.12.1", "sass-loader": "12.1.0", "sinon": "8.1.1", "style-loader": "3.3.0", + "stylelint": "13.13.1", + "stylelint-config-prettier": "9.0.3", + "stylelint-config-sass-guidelines": "8.0.0", "terser-webpack-plugin": "5.2.4", "testing-library-selector": "^0.1.3", "ts-jest": "26.4.4", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index a838549ce98..fff69c9c6e0 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -45,7 +45,7 @@ "@types/node": "10.14.1", "@types/papaparse": "5.2.0", "@types/react": "17.0.30", - "@types/rollup-plugin-visualizer": "2.6.0", + "@types/rollup-plugin-visualizer": "4.2.1", "@types/sinon": "^10.0.4", "rollup": "2.33.3", "rollup-plugin-sourcemaps": "0.6.3", diff --git a/packages/grafana-data/src/utils/datasource.ts b/packages/grafana-data/src/utils/datasource.ts index 6fad3a54b09..3697a132005 100644 --- a/packages/grafana-data/src/utils/datasource.ts +++ b/packages/grafana-data/src/utils/datasource.ts @@ -47,8 +47,8 @@ export const onUpdateDatasourceResetOption = (props: DataSourcePluginOptionsEdit updateDatasourcePluginResetOption(props, key); }; -export function updateDatasourcePluginOption( - props: DataSourcePluginOptionsEditorProps, +export function updateDatasourcePluginOption( + props: DataSourcePluginOptionsEditorProps, key: keyof DataSourceSettings, val: any ) { @@ -82,21 +82,27 @@ export const updateDatasourcePluginSecureJsonDataOption = { const config = props.options; + if (!config.secureJsonData) { + return; + } props.onOptionsChange({ ...config, secureJsonData: { - ...config.secureJsonData!, + ...config.secureJsonData, [key]: val, }, }); }; export const updateDatasourcePluginResetOption = ( - props: DataSourcePluginOptionsEditorProps, + props: DataSourcePluginOptionsEditorProps, key: string ) => { const config = props.options; + if (!config.secureJsonData) { + return; + } props.onOptionsChange({ ...config, diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 1fe43c7602b..55a0992670b 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -27,7 +27,7 @@ "@rollup/plugin-commonjs": "16.0.0", "@rollup/plugin-node-resolve": "10.0.0", "@types/node": "13.7.7", - "@types/rollup-plugin-visualizer": "2.6.0", + "@types/rollup-plugin-visualizer": "4.2.1", "@types/systemjs": "^0.20.6", "rollup": "2.33.3", "rollup-plugin-sourcemaps": "0.6.3", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 008e66f188e..ac8b2e3ce1b 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -33,7 +33,7 @@ "@rollup/plugin-commonjs": "16.0.0", "@rollup/plugin-node-resolve": "10.0.0", "@types/node": "13.7.7", - "@types/rollup-plugin-visualizer": "2.6.0", + "@types/rollup-plugin-visualizer": "4.2.1", "date-fns": "2.21.3", "rollup": "2.33.3", "rollup-plugin-copy": "3.3.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 02e350743f8..2f325dd228a 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -37,7 +37,7 @@ "@rollup/plugin-node-resolve": "10.0.0", "@types/history": "^4.7.8", "@types/jest": "26.0.15", - "@types/rollup-plugin-visualizer": "2.6.0", + "@types/rollup-plugin-visualizer": "4.2.1", "@types/systemjs": "^0.20.6", "lodash": "4.17.21", "rollup": "2.33.3", diff --git a/packages/grafana-runtime/src/services/appEvents.ts b/packages/grafana-runtime/src/services/appEvents.ts new file mode 100644 index 00000000000..c430a554df9 --- /dev/null +++ b/packages/grafana-runtime/src/services/appEvents.ts @@ -0,0 +1,59 @@ +import { BusEventBase, BusEventWithPayload, EventBus, GrafanaTheme2, PanelModel, TimeRange } from '@grafana/data'; + +/** + * Called when a dashboard is refreshed + * + * @public + */ +export class RefreshEvent extends BusEventBase { + static type = 'refresh'; +} + +/** + * Called when the theme settings change + * + * @public + */ +export class ThemeChangedEvent extends BusEventWithPayload { + static type = 'theme-changed'; +} + +/** + * Called when time range is updated + * + * @public + */ +export class TimeRangeUpdatedEvent extends BusEventWithPayload { + static type = 'time-range-updated'; +} + +/** + * Called to copy a panel JSON into local storage + * + * @public + */ +export class CopyPanelEvent extends BusEventWithPayload { + static type = 'copy-panel'; +} + +// Internal singleton instance +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. + * + * @internal + */ +export function setAppEvents(instance: EventBus) { + singletonInstance = instance; +} + +/** + * Used to retrieve an event bus that manages application level events + * + * @public + */ +export function getAppEvents(): EventBus { + return singletonInstance; +} diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index 85a9671b4d8..04b7de736fd 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -7,3 +7,4 @@ export * from './templateSrv'; export * from './legacyAngularInjector'; export * from './live'; export * from './LocationService'; +export * from './appEvents'; diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index a2a2e773d63..d38eb5562e2 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -26,7 +26,7 @@ "@rollup/plugin-commonjs": "16.0.0", "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "10.0.0", - "@types/rollup-plugin-visualizer": "2.6.0", + "@types/rollup-plugin-visualizer": "4.2.1", "rollup": "2.33.3", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", diff --git a/packages/grafana-ui/.storybook/preview.ts b/packages/grafana-ui/.storybook/preview.ts index d8a51bcd804..c4ac4691dcd 100644 --- a/packages/grafana-ui/.storybook/preview.ts +++ b/packages/grafana-ui/.storybook/preview.ts @@ -39,8 +39,8 @@ export const parameters = { container: ThemedDocsContainer, }, darkMode: { - dark: GrafanaDark, - light: GrafanaLight, + dark: { ...GrafanaDark, base: 'dark' }, + light: { ...GrafanaLight, base: 'light' }, }, layout: 'fullscreen', actions: { argTypesRegex: '^on[A-Z].*' }, diff --git a/packages/grafana-ui/.storybook/storybookTheme.ts b/packages/grafana-ui/.storybook/storybookTheme.ts index e57efc4614b..36715f8d776 100644 --- a/packages/grafana-ui/.storybook/storybookTheme.ts +++ b/packages/grafana-ui/.storybook/storybookTheme.ts @@ -5,8 +5,6 @@ import '../src/components/Icon/iconBundle'; const createStorybookTheme = (theme: GrafanaTheme2) => { return create({ - base: theme.name.includes('Light') ? 'light' : 'dark', - colorPrimary: theme.colors.primary.main, colorSecondary: theme.colors.error.main, diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ffe2a111a85..2d3025fec34 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -111,7 +111,7 @@ "@types/react-table": "7.7.2", "@types/react-test-renderer": "17.0.1", "@types/react-transition-group": "4.4.0", - "@types/rollup-plugin-visualizer": "2.6.0", + "@types/rollup-plugin-visualizer": "4.2.1", "@types/slate": "0.47.2", "@types/slate-react": "0.22.5", "@types/tinycolor2": "1.4.1", diff --git a/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index 8e3b9129ecf..ab7734d4f80 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -13,6 +13,7 @@ export interface IconProps extends React.HTMLAttributes { name: IconName; size?: IconSize; type?: IconType; + title?: string; } const getIconStyles = stylesFactory((theme: GrafanaTheme) => { @@ -44,7 +45,7 @@ function getIconSubDir(name: IconName, type: string): string { } export const Icon = React.forwardRef( - ({ size = 'md', type = 'default', name, className, style, ...divElementProps }, ref) => { + ({ size = 'md', type = 'default', name, className, style, title = '', ...divElementProps }, ref) => { const theme = useTheme(); /* Temporary solution to display also font awesome icons */ @@ -73,6 +74,7 @@ export const Icon = React.forwardRef( src={svgPath} width={svgWid} height={svgHgt} + title={title} className={cx(styles.icon, className, type === 'mono' ? { [styles.orange]: name === 'favorite' } : '')} style={style} /> diff --git a/packages/grafana-ui/src/components/Select/SingleValue.tsx b/packages/grafana-ui/src/components/Select/SingleValue.tsx index 9f371c5d960..281153b33c0 100644 --- a/packages/grafana-ui/src/components/Select/SingleValue.tsx +++ b/packages/grafana-ui/src/components/Select/SingleValue.tsx @@ -7,6 +7,7 @@ import { SlideOutTransition } from '../transitions/SlideOutTransition'; import { FadeTransition } from '../transitions/FadeTransition'; import { Spinner } from '../Spinner/Spinner'; import { GrafanaTheme2 } from '@grafana/data'; +import tinycolor from 'tinycolor2'; const getStyles = (theme: GrafanaTheme2) => { const singleValue = css` @@ -35,7 +36,7 @@ const getStyles = (theme: GrafanaTheme2) => { `; const disabled = css` - color: ${theme.colors.action.disabledText}; + color: ${tinycolor(theme.colors.text.disabled).setAlpha(0.64).toString()}; `; return { singleValue, container, item, disabled }; diff --git a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts index 73adaded65b..dab9f57cc79 100644 --- a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts @@ -95,8 +95,9 @@ $height-lg: ${theme.v1.height.lg}; // Typography // ------------------------- - +/* stylelint-disable-next-line string-quotes */ $font-family-sans-serif: ${theme.v1.typography.fontFamily.sansSerif}; +/* stylelint-disable-next-line string-quotes */ $font-family-monospace: ${theme.v1.typography.fontFamily.monospace}; $font-size-base: ${theme.v1.typography.size.base} !default; diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 179aa7df6c2..844cf145734 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -27,7 +27,9 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi WORKDIR $GF_PATHS_HOME RUN apk add --no-cache ca-certificates bash tzdata && \ - apk add --no-cache openssl musl-utils + apk add --no-cache musl-utils + +RUN apk add --no-cache openssl --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main # Oracle Support for x86_64 only RUN if [ `arch` = "x86_64" ]; then \ diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 2336af4d34b..5c42b5e5574 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -72,6 +72,17 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext, cmd models.AddApiKeyComman } } cmd.OrgId = c.OrgId + var err error + var serviceAccount *models.User = &models.User{Id: -1} + if hs.Cfg.FeatureToggles["service-accounts"] { + if cmd.CreateNewServiceAccount { + serviceAccount, err = hs.AccessControl.CloneUserToServiceAccount(c.Req.Context(), c.SignedInUser) + if err != nil { + return response.Error(500, "Unable to clone user to service account", err) + } + cmd.ServiceAccountId = serviceAccount.Id + } + } newKeyInfo, err := apikeygen.New(cmd.OrgId, cmd.Name) if err != nil { diff --git a/pkg/api/index.go b/pkg/api/index.go index c566245182d..c6431fc5ead 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -199,16 +199,19 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto navTree = append(navTree, hs.getProfileNode(c)) } - if setting.AlertingEnabled || hs.Cfg.UnifiedAlerting.Enabled { + _, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgId] + uaVisibleForOrg := hs.Cfg.UnifiedAlerting.Enabled && !uaIsDisabledForOrg + + if setting.AlertingEnabled || uaVisibleForOrg { alertChildNavs := []*dtos.NavLink{ {Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul"}, } - if hs.Cfg.UnifiedAlerting.Enabled { + if uaVisibleForOrg { alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Alert groups", Id: "groups", Url: hs.Cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"}) alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"}) } if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR { - if hs.Cfg.UnifiedAlerting.Enabled { + if uaVisibleForOrg { alertChildNavs = append(alertChildNavs, &dtos.NavLink{ Text: "Contact points", Id: "receivers", Url: hs.Cfg.AppSubURL + "/alerting/notifications", Icon: "comment-alt-share", @@ -221,7 +224,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto }) } } - if c.OrgRole == models.ROLE_ADMIN && hs.Cfg.UnifiedAlerting.Enabled { + if c.OrgRole == models.ROLE_ADMIN && uaVisibleForOrg { alertChildNavs = append(alertChildNavs, &dtos.NavLink{ Text: "Admin", Id: "alerting-admin", Url: hs.Cfg.AppSubURL + "/alerting/admin", Icon: "cog", @@ -380,7 +383,11 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "import", Url: hs.Cfg.AppSubURL + "/dashboard/import", }) - if setting.AlertingEnabled || hs.Cfg.UnifiedAlerting.Enabled { + + _, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgId] + uaVisibleForOrg := hs.Cfg.UnifiedAlerting.Enabled && !uaIsDisabledForOrg + + if setting.AlertingEnabled || uaVisibleForOrg { children = append(children, &dtos.NavLink{ Text: "Alert rule", SubTitle: "Create an alert rule", Id: "alert", Icon: "bell", Url: hs.Cfg.AppSubURL + "/alerting/new", diff --git a/pkg/models/apikey.go b/pkg/models/apikey.go index ddde171735b..9c05105d10d 100644 --- a/pkg/models/apikey.go +++ b/pkg/models/apikey.go @@ -13,24 +13,27 @@ var ( ) type ApiKey struct { - Id int64 - OrgId int64 - Name string - Key string - Role RoleType - Created time.Time - Updated time.Time - Expires *int64 + Id int64 + OrgId int64 + Name string + Key string + Role RoleType + Created time.Time + Updated time.Time + Expires *int64 + ServiceAccountId int64 } // --------------------- // COMMANDS type AddApiKeyCommand struct { - Name string `json:"name" binding:"Required"` - Role RoleType `json:"role" binding:"Required"` - OrgId int64 `json:"-"` - Key string `json:"-"` - SecondsToLive int64 `json:"secondsToLive"` + Name string `json:"name" binding:"Required"` + Role RoleType `json:"role" binding:"Required"` + OrgId int64 `json:"-"` + Key string `json:"-"` + SecondsToLive int64 `json:"secondsToLive"` + ServiceAccountId int64 `json:"serviceAccount"` + CreateNewServiceAccount bool `json:"createServiceAccount"` Result *ApiKey `json:"-"` } diff --git a/pkg/models/user.go b/pkg/models/user.go index f987a76f95b..0a19bce31a6 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -56,18 +56,19 @@ func (u *User) NameOrFallback() string { // COMMANDS type CreateUserCommand struct { - Email string - Login string - Name string - Company string - OrgId int64 - OrgName string - Password string - EmailVerified bool - IsAdmin bool - IsDisabled bool - SkipOrgSetup bool - DefaultOrgRole string + Email string + Login string + Name string + Company string + OrgId int64 + OrgName string + Password string + EmailVerified bool + IsAdmin bool + IsDisabled bool + SkipOrgSetup bool + DefaultOrgRole string + IsServiceAccount bool Result User } diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 7514a102617..5961caae77d 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -14,6 +14,15 @@ type AccessControl interface { // GetUserPermissions returns user permissions. GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*Permission, error) + // GetUserRoles returns user roles. + GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*RoleDTO, error) + + // CloneUserToServiceAccount Creates a new service account and assigns it the same roles as the user has + CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error) + + // LinkAPIKeyToServiceAccount Connects an APIkey to a service account. Multiple API keys may be linked to one account. + LinkAPIKeyToServiceAccount(ctx context.Context, ApiKey *models.ApiKey, serviceAccount *models.User) error + //IsDisabled returns if access control is enabled or not IsDisabled() bool diff --git a/pkg/services/accesscontrol/mock/mock.go b/pkg/services/accesscontrol/mock/mock.go index 1cf6ebcf856..af90dd3dfaa 100644 --- a/pkg/services/accesscontrol/mock/mock.go +++ b/pkg/services/accesscontrol/mock/mock.go @@ -14,17 +14,24 @@ type fullAccessControl interface { } type Calls struct { - Evaluate []interface{} - GetUserPermissions []interface{} - IsDisabled []interface{} - DeclareFixedRoles []interface{} - GetUserBuiltInRoles []interface{} - RegisterFixedRoles []interface{} + CloneUserToServiceAccount []interface{} + Evaluate []interface{} + GetUserPermissions []interface{} + GetUserRoles []interface{} + IsDisabled []interface{} + DeclareFixedRoles []interface{} + GetUserBuiltInRoles []interface{} + RegisterFixedRoles []interface{} + LinkAPIKeyToServiceAccount []interface{} } type Mock struct { + // Unless an override is provided, user will be returned by CloneUserToServiceAccount + createduser *models.User // Unless an override is provided, permissions will be returned by GetUserPermissions permissions []*accesscontrol.Permission + // Unless an override is provided, roles will be returned by GetUserRoles + roles []*accesscontrol.RoleDTO // Unless an override is provided, disabled will be returned by IsDisabled disabled bool // Unless an override is provided, builtInRoles will be returned by GetUserBuiltInRoles @@ -34,12 +41,15 @@ type Mock struct { Calls Calls // Override functions - EvaluateFunc func(context.Context, *models.SignedInUser, accesscontrol.Evaluator) (bool, error) - GetUserPermissionsFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.Permission, error) - IsDisabledFunc func() bool - DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error - GetUserBuiltInRolesFunc func(user *models.SignedInUser) []string - RegisterFixedRolesFunc func() error + CloneUserToServiceAccountFunc func(context.Context, *models.SignedInUser) (*models.User, error) + LinkAPIKeyToServiceAccountFunc func(context.Context, *models.ApiKey, *models.User) error + EvaluateFunc func(context.Context, *models.SignedInUser, accesscontrol.Evaluator) (bool, error) + GetUserPermissionsFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.Permission, error) + GetUserRolesFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.RoleDTO, error) + IsDisabledFunc func() bool + DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error + GetUserBuiltInRolesFunc func(user *models.SignedInUser) []string + RegisterFixedRolesFunc func() error } // Ensure the mock stays in line with the interface @@ -99,6 +109,36 @@ func (m *Mock) GetUserPermissions(ctx context.Context, user *models.SignedInUser return m.permissions, nil } +func (m *Mock) GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.RoleDTO, error) { + m.Calls.GetUserRoles = append(m.Calls.GetUserRoles, []interface{}{ctx, user}) + // Use override if provided + if m.GetUserRolesFunc != nil { + return m.GetUserRolesFunc(ctx, user) + } + // Otherwise return the Roles list + return m.roles, nil +} + +func (m *Mock) CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error) { + m.Calls.CloneUserToServiceAccount = append(m.Calls.CloneUserToServiceAccount, []interface{}{ctx, user}) + // Use override if provided + if m.CloneUserToServiceAccountFunc != nil { + return m.CloneUserToServiceAccountFunc(ctx, user) + } + // Otherwise return the user + return m.createduser, nil +} + +func (m *Mock) LinkAPIKeyToServiceAccount(ctx context.Context, apikey *models.ApiKey, service_account *models.User) error { + m.Calls.LinkAPIKeyToServiceAccount = append(m.Calls.LinkAPIKeyToServiceAccount, []interface{}{ctx, apikey, service_account}) + // Use override if provided + if m.LinkAPIKeyToServiceAccountFunc != nil { + return m.LinkAPIKeyToServiceAccountFunc(ctx, apikey, service_account) + } + // Otherwise return the default + return nil +} + // Middleware checks if service disabled or not to switch to fallback authorization. // This mock return m.disabled unless an override is provided. func (m *Mock) IsDisabled() bool { diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go index 18eade800d6..9ad49c05313 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go @@ -2,6 +2,7 @@ package ossaccesscontrol import ( "context" + "errors" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" @@ -71,6 +72,21 @@ func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.Si return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions)) } +// GetUserRoles returns user permissions based on built-in roles +func (ac *OSSAccessControlService) GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.RoleDTO, error) { + return nil, errors.New("unsupported function") //OSS users will continue to use builtin roles via GetUserPermissions +} + +// CloneUserToServiceAccount creates a service account with permissions based on a user +func (ac *OSSAccessControlService) CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error) { + return nil, errors.New("clone user not implemented yet in service accounts") //Please switch on Enterprise to test this +} + +// Link creates a service account with permissions based on a user +func (ac *OSSAccessControlService) LinkAPIKeyToServiceAccount(context.Context, *models.ApiKey, *models.User) error { + return errors.New("link SA not implemented yet in service accounts") //Please switch on Enterprise to test this +} + // GetUserPermissions returns user permissions based on built-in roles func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.Permission, error) { timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary) diff --git a/pkg/services/accesscontrol/scope.go b/pkg/services/accesscontrol/scope.go index 30c8ae877a2..e7f10bba45f 100644 --- a/pkg/services/accesscontrol/scope.go +++ b/pkg/services/accesscontrol/scope.go @@ -3,6 +3,8 @@ package accesscontrol import ( "fmt" "strings" + + "github.com/grafana/grafana/pkg/models" ) // Scope builds scope from parts @@ -29,3 +31,39 @@ func Parameter(key string) string { func Field(key string) string { return fmt.Sprintf(`{{ .%s }}`, key) } + +type KeywordScopeResolveFunc func(*models.SignedInUser) (string, error) + +// ScopeResolver contains a map of functions to resolve scope keywords such as `self` or `current` into `id` based scopes +type ScopeResolver struct { + keywordResolvers map[string]KeywordScopeResolveFunc +} + +func NewScopeResolver() ScopeResolver { + return ScopeResolver{ + keywordResolvers: map[string]KeywordScopeResolveFunc{ + "orgs:current": resolveCurrentOrg, + "users:self": resolveUserSelf, + }, + } +} + +func resolveCurrentOrg(u *models.SignedInUser) (string, error) { + return Scope("orgs", "id", fmt.Sprintf("%v", u.OrgId)), nil +} + +func resolveUserSelf(u *models.SignedInUser) (string, error) { + return Scope("users", "id", fmt.Sprintf("%v", u.UserId)), nil +} + +// ResolveKeyword resolves scope with keywords such as `self` or `current` into `id` based scopes +func (s *ScopeResolver) ResolveKeyword(user *models.SignedInUser, permission Permission) (*Permission, error) { + if fn, ok := s.keywordResolvers[permission.Scope]; ok { + resolvedScope, err := fn(user) + if err != nil { + return nil, fmt.Errorf("could not resolve %v: %v", permission.Scope, err) + } + permission.Scope = resolvedScope + } + return &permission, nil +} diff --git a/pkg/services/accesscontrol/scoperesolution_test.go b/pkg/services/accesscontrol/scope_test.go similarity index 100% rename from pkg/services/accesscontrol/scoperesolution_test.go rename to pkg/services/accesscontrol/scope_test.go diff --git a/pkg/services/accesscontrol/scoperesolution.go b/pkg/services/accesscontrol/scoperesolution.go deleted file mode 100644 index 144e7e00b02..00000000000 --- a/pkg/services/accesscontrol/scoperesolution.go +++ /dev/null @@ -1,43 +0,0 @@ -package accesscontrol - -import ( - "fmt" - - "github.com/grafana/grafana/pkg/models" -) - -type KeywordScopeResolveFunc func(*models.SignedInUser) (string, error) - -// ScopeResolver contains a map of functions to resolve scope keywords such as `self` or `current` into `id` based scopes -type ScopeResolver struct { - keywordResolvers map[string]KeywordScopeResolveFunc -} - -func NewScopeResolver() ScopeResolver { - return ScopeResolver{ - keywordResolvers: map[string]KeywordScopeResolveFunc{ - "orgs:current": resolveCurrentOrg, - "users:self": resolveUserSelf, - }, - } -} - -func resolveCurrentOrg(u *models.SignedInUser) (string, error) { - return Scope("orgs", "id", fmt.Sprintf("%v", u.OrgId)), nil -} - -func resolveUserSelf(u *models.SignedInUser) (string, error) { - return Scope("users", "id", fmt.Sprintf("%v", u.UserId)), nil -} - -// ResolveKeyword resolves scope with keywords such as `self` or `current` into `id` based scopes -func (s *ScopeResolver) ResolveKeyword(user *models.SignedInUser, permission Permission) (*Permission, error) { - if fn, ok := s.keywordResolvers[permission.Scope]; ok { - resolvedScope, err := fn(user) - if err != nil { - return nil, fmt.Errorf("could not resolve %v: %v", permission.Scope, err) - } - permission.Scope = resolvedScope - } - return &permission, nil -} diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index bdaf00c0a5c..4bf6dec8abd 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -382,15 +382,17 @@ func (sn *SlackNotifier) sendRequest(ctx context.Context, data []byte) error { return fmt.Errorf("failed to read response body: %w", err) } - if resp.StatusCode/100 == 2 { - var rslt map[string]interface{} + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { // Slack responds to some requests with a JSON document, that might contain an error + rslt := struct { + Ok bool `json:"ok"` + Err string `json:"error"` + }{} if err := json.Unmarshal(body, &rslt); err == nil { - if !rslt["ok"].(bool) { - errMsg := rslt["error"].(string) + if !rslt.Ok && rslt.Err != "" { sn.log.Warn("Sending Slack API request failed", "url", sn.url.String(), "statusCode", resp.Status, - "err", errMsg) - return fmt.Errorf("failed to make Slack API request: %s", errMsg) + "err", rslt.Err) + return fmt.Errorf("failed to make Slack API request: %s", rslt.Err) } } diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index 048df2af831..9852951677a 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -2,6 +2,9 @@ package notifiers import ( "context" + "fmt" + "net/http" + "net/http/httptest" "testing" "github.com/grafana/grafana/pkg/components/simplejson" @@ -223,3 +226,102 @@ func TestSlackNotifier(t *testing.T) { assert.Equal(t, "1ABCDE", slackNotifier.recipient) }) } + +func TestSendSlackRequest(t *testing.T) { + tests := []struct { + name string + slackResponse string + statusCode int + expectError bool + }{ + { + name: "Example error", + slackResponse: `{ + "ok": false, + "error": "too_many_attachments" + }`, + statusCode: http.StatusBadRequest, + expectError: true, + }, + { + name: "Non 200 status code, no response body", + statusCode: http.StatusMovedPermanently, + expectError: true, + }, + { + name: "Success case, normal response body", + slackResponse: `{ + "ok": true, + "channel": "C1H9RESGL", + "ts": "1503435956.000247", + "message": { + "text": "Here's a message for you", + "username": "ecto1", + "bot_id": "B19LU7CSY", + "attachments": [ + { + "text": "This is an attachment", + "id": 1, + "fallback": "This is an attachment's fallback" + } + ], + "type": "message", + "subtype": "bot_message", + "ts": "1503435956.000247" + } + }`, + statusCode: http.StatusOK, + expectError: false, + }, + { + name: "Success case, no response body", + statusCode: http.StatusOK, + expectError: false, + }, + { + name: "Success case, unexpected response body", + statusCode: http.StatusOK, + slackResponse: "{}", + expectError: false, + }, + { + name: "Success case, ok: true", + statusCode: http.StatusOK, + slackResponse: "{\"ok\": true}", + expectError: false, + }, + { + name: "200 status code, error in body", + statusCode: http.StatusOK, + slackResponse: `{"ok": false, "error": "test error"}`, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.statusCode) + _, err := w.Write([]byte(test.slackResponse)) + require.NoError(tt, err) + })) + + settingsJSON, err := simplejson.NewJson([]byte(fmt.Sprintf(`{"url": %q}`, server.URL))) + require.NoError(t, err) + model := &models.AlertNotification{ + Settings: settingsJSON, + } + + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + require.NoError(t, err) + slackNotifier := not.(*SlackNotifier) + + err = slackNotifier.sendRequest(context.TODO(), []byte("test")) + if !test.expectError { + require.NoError(tt, err) + } else { + require.Error(tt, err) + } + }) + } +} diff --git a/pkg/services/cleanup/cleanup_test.go b/pkg/services/cleanup/cleanup_test.go index 448123fa8a7..8d92c28d8f4 100644 --- a/pkg/services/cleanup/cleanup_test.go +++ b/pkg/services/cleanup/cleanup_test.go @@ -5,37 +5,33 @@ import ( "time" "github.com/grafana/grafana/pkg/setting" - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/require" ) func TestCleanUpTmpFiles(t *testing.T) { - Convey("Cleanup service tests", t, func() { - cfg := setting.Cfg{} - cfg.TempDataLifetime, _ = time.ParseDuration("24h") - service := CleanUpService{ - Cfg: &cfg, - } - now := time.Now() - secondAgo := now.Add(-time.Second) - twoDaysAgo := now.Add(-time.Second * 3600 * 24 * 2) - weekAgo := now.Add(-time.Second * 3600 * 24 * 7) + cfg := setting.Cfg{} + cfg.TempDataLifetime, _ = time.ParseDuration("24h") + service := CleanUpService{ + Cfg: &cfg, + } + now := time.Now() + secondAgo := now.Add(-time.Second) + twoDaysAgo := now.Add(-time.Second * 3600 * 24 * 2) + weekAgo := now.Add(-time.Second * 3600 * 24 * 7) + t.Run("Should not cleanup recent files", func(t *testing.T) { + require.False(t, service.shouldCleanupTempFile(secondAgo, now)) + }) + t.Run("Should cleanup older files", func(t *testing.T) { + require.True(t, service.shouldCleanupTempFile(twoDaysAgo, now)) + }) - Convey("Should not cleanup recent files", func() { - So(service.shouldCleanupTempFile(secondAgo, now), ShouldBeFalse) - }) + t.Run("After increasing temporary files lifetime, older files should be kept", func(t *testing.T) { + cfg.TempDataLifetime, _ = time.ParseDuration("1000h") + require.False(t, service.shouldCleanupTempFile(weekAgo, now)) + }) - Convey("Should cleanup older files", func() { - So(service.shouldCleanupTempFile(twoDaysAgo, now), ShouldBeTrue) - }) - - Convey("After increasing temporary files lifetime, older files should be kept", func() { - cfg.TempDataLifetime, _ = time.ParseDuration("1000h") - So(service.shouldCleanupTempFile(weekAgo, now), ShouldBeFalse) - }) - - Convey("If lifetime is 0, files should never be cleaned up", func() { - cfg.TempDataLifetime = 0 - So(service.shouldCleanupTempFile(weekAgo, now), ShouldBeFalse) - }) + t.Run("If lifetime is 0, files should never be cleaned up", func(t *testing.T) { + cfg.TempDataLifetime = 0 + require.False(t, service.shouldCleanupTempFile(weekAgo, now)) }) } diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index cec307e6a58..cf673755d17 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -218,11 +218,33 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo return true } + if apikey.ServiceAccountId < 1 { //There is no service account attached to the apikey + //Use the old APIkey method. This provides backwards compatibility. + reqContext.SignedInUser = &models.SignedInUser{} + reqContext.OrgRole = apikey.Role + reqContext.ApiKeyId = apikey.Id + reqContext.OrgId = apikey.OrgId + reqContext.IsSignedIn = true + return true + } + + //There is a service account attached to the API key + + //Use service account linked to API key as the signed in user + query := models.GetSignedInUserQuery{UserId: apikey.ServiceAccountId, OrgId: apikey.OrgId} + if err := bus.Dispatch(&query); err != nil { + reqContext.Logger.Error( + "Failed to link API key to service account in", + "id", query.UserId, + "org", query.OrgId, + "err", err, + ) + reqContext.JsonApiErr(500, "Unable to link API key to service account", err) + return true + } + reqContext.IsSignedIn = true - reqContext.SignedInUser = &models.SignedInUser{} - reqContext.OrgRole = apikey.Role - reqContext.ApiKeyId = apikey.Id - reqContext.OrgId = apikey.OrgId + reqContext.SignedInUser = query.Result return true } diff --git a/pkg/services/multildap/multildap_test.go b/pkg/services/multildap/multildap_test.go index d9f833e460a..f39f3123709 100644 --- a/pkg/services/multildap/multildap_test.go +++ b/pkg/services/multildap/multildap_test.go @@ -6,443 +6,442 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" - . "github.com/smartystreets/goconvey/convey" + + "github.com/stretchr/testify/require" ) func TestMultiLDAP(t *testing.T) { - Convey("Multildap", t, func() { - Convey("Ping()", func() { - Convey("Should return error for absent config list", func() { - setup() + t.Run("Ping()", func(t *testing.T) { + t.Run("Should return error for absent config list", func(t *testing.T) { + setup() - multi := New([]*ldap.ServerConfig{}) - _, err := multi.Ping() + multi := New([]*ldap.ServerConfig{}) + _, err := multi.Ping() - So(err, ShouldBeError) - So(err, ShouldEqual, ErrNoLDAPServers) + require.Error(t, err) + require.Equal(t, ErrNoLDAPServers, err) - teardown() - }) - Convey("Should return an unavailable status on dial error", func() { - mock := setup() - - expectedErr := errors.New("Dial error") - mock.dialErrReturn = expectedErr - - multi := New([]*ldap.ServerConfig{ - {Host: "10.0.0.1", Port: 361}, - }) - - statuses, err := multi.Ping() - - So(err, ShouldBeNil) - So(statuses[0].Host, ShouldEqual, "10.0.0.1") - So(statuses[0].Port, ShouldEqual, 361) - So(statuses[0].Available, ShouldBeFalse) - So(statuses[0].Error, ShouldEqual, expectedErr) - So(mock.closeCalledTimes, ShouldEqual, 0) - - teardown() - }) - Convey("Should get the LDAP server statuses", func() { - mock := setup() - - multi := New([]*ldap.ServerConfig{ - {Host: "10.0.0.1", Port: 361}, - }) - - statuses, err := multi.Ping() - - So(err, ShouldBeNil) - So(statuses[0].Host, ShouldEqual, "10.0.0.1") - So(statuses[0].Port, ShouldEqual, 361) - So(statuses[0].Available, ShouldBeTrue) - So(statuses[0].Error, ShouldBeNil) - So(mock.closeCalledTimes, ShouldEqual, 1) - - teardown() - }) + teardown() }) - Convey("Login()", func() { - Convey("Should return error for absent config list", func() { - setup() + t.Run("Should return an unavailable status on dial error", func(t *testing.T) { + mock := setup() - multi := New([]*ldap.ServerConfig{}) - _, err := multi.Login(&models.LoginUserQuery{}) + expectedErr := errors.New("Dial error") + mock.dialErrReturn = expectedErr - So(err, ShouldBeError) - So(err, ShouldEqual, ErrNoLDAPServers) - - teardown() + multi := New([]*ldap.ServerConfig{ + {Host: "10.0.0.1", Port: 361}, }) - Convey("Should return a dial error", func() { - mock := setup() + statuses, err := multi.Ping() - expected := errors.New("Dial error") - mock.dialErrReturn = expected + require.Nil(t, err) + require.Equal(t, "10.0.0.1", statuses[0].Host) + require.Equal(t, 361, statuses[0].Port) + require.False(t, statuses[0].Available) + require.Equal(t, expectedErr, statuses[0].Error) + require.Equal(t, 0, mock.closeCalledTimes) - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) + teardown() + }) + t.Run("Should get the LDAP server statuses", func(t *testing.T) { + mock := setup() - _, err := multi.Login(&models.LoginUserQuery{}) - - So(err, ShouldBeError) - So(err, ShouldEqual, expected) - - teardown() + multi := New([]*ldap.ServerConfig{ + {Host: "10.0.0.1", Port: 361}, }) - Convey("Should call underlying LDAP methods", func() { - mock := setup() + statuses, err := multi.Ping() - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, err := multi.Login(&models.LoginUserQuery{}) + require.Nil(t, err) + require.Equal(t, "10.0.0.1", statuses[0].Host) + require.Equal(t, 361, statuses[0].Port) + require.True(t, statuses[0].Available) + require.Nil(t, statuses[0].Error) + require.Equal(t, 1, mock.closeCalledTimes) - So(mock.dialCalledTimes, ShouldEqual, 2) - So(mock.loginCalledTimes, ShouldEqual, 2) - So(mock.closeCalledTimes, ShouldEqual, 2) + teardown() + }) + }) + t.Run("Login()", func(t *testing.T) { + t.Run("Should return error for absent config list", func(t *testing.T) { + setup() - So(err, ShouldEqual, ErrInvalidCredentials) + multi := New([]*ldap.ServerConfig{}) + _, err := multi.Login(&models.LoginUserQuery{}) - teardown() - }) + require.Error(t, err) + require.Equal(t, ErrNoLDAPServers, err) - Convey("Should get login result", func() { - mock := setup() - - mock.loginReturn = &models.ExternalUserInfo{ - Login: "killa", - } - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - result, err := multi.Login(&models.LoginUserQuery{}) - - So(mock.dialCalledTimes, ShouldEqual, 1) - So(mock.loginCalledTimes, ShouldEqual, 1) - So(mock.closeCalledTimes, ShouldEqual, 1) - - So(result.Login, ShouldEqual, "killa") - So(err, ShouldBeNil) - - teardown() - }) - - Convey("Should still call a second error for invalid not found error", func() { - mock := setup() - - mock.loginErrReturn = ErrCouldNotFindUser - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, err := multi.Login(&models.LoginUserQuery{}) - - So(mock.dialCalledTimes, ShouldEqual, 2) - So(mock.loginCalledTimes, ShouldEqual, 2) - So(mock.closeCalledTimes, ShouldEqual, 2) - - So(err, ShouldEqual, ErrInvalidCredentials) - - teardown() - }) - - Convey("Should still try to auth with the second server after receiving an invalid credentials error from the first", func() { - mock := setup() - - mock.loginErrReturn = ErrInvalidCredentials - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, err := multi.Login(&models.LoginUserQuery{}) - - So(mock.dialCalledTimes, ShouldEqual, 2) - So(mock.loginCalledTimes, ShouldEqual, 2) - So(mock.closeCalledTimes, ShouldEqual, 2) - - So(err, ShouldEqual, ErrInvalidCredentials) - - teardown() - }) - - Convey("Should still try to auth with the second server after receiving a dial error from the first", func() { - mock := setup() - - expectedError := errors.New("Dial error") - mock.dialErrReturn = expectedError - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, err := multi.Login(&models.LoginUserQuery{}) - - So(mock.dialCalledTimes, ShouldEqual, 2) - - So(err, ShouldEqual, expectedError) - - teardown() - }) - - Convey("Should return unknown error", func() { - mock := setup() - - expected := errors.New("Something unknown") - mock.loginErrReturn = expected - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, err := multi.Login(&models.LoginUserQuery{}) - - So(mock.dialCalledTimes, ShouldEqual, 1) - So(mock.loginCalledTimes, ShouldEqual, 1) - So(mock.closeCalledTimes, ShouldEqual, 1) - - So(err, ShouldEqual, expected) - - teardown() - }) + teardown() }) - Convey("User()", func() { - Convey("Should return error for absent config list", func() { - setup() + t.Run("Should return a dial error", func(t *testing.T) { + mock := setup() - multi := New([]*ldap.ServerConfig{}) - _, _, err := multi.User("test") + expected := errors.New("Dial error") + mock.dialErrReturn = expected - So(err, ShouldBeError) - So(err, ShouldEqual, ErrNoLDAPServers) - - teardown() + multi := New([]*ldap.ServerConfig{ + {}, {}, }) - Convey("Should return a dial error", func() { - mock := setup() + _, err := multi.Login(&models.LoginUserQuery{}) - expected := errors.New("Dial error") - mock.dialErrReturn = expected + require.Error(t, err) + require.Equal(t, expected, err) - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - - _, _, err := multi.User("test") - - So(err, ShouldBeError) - So(err, ShouldEqual, expected) - - teardown() - }) - - Convey("Should call underlying LDAP methods", func() { - mock := setup() - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, _, err := multi.User("test") - - So(mock.dialCalledTimes, ShouldEqual, 2) - So(mock.usersCalledTimes, ShouldEqual, 2) - So(mock.closeCalledTimes, ShouldEqual, 2) - - So(err, ShouldEqual, ErrDidNotFindUser) - - teardown() - }) - - Convey("Should return some error", func() { - mock := setup() - - expected := errors.New("Killa Gorilla") - mock.usersErrReturn = expected - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, _, err := multi.User("test") - - So(mock.dialCalledTimes, ShouldEqual, 1) - So(mock.usersCalledTimes, ShouldEqual, 1) - So(mock.closeCalledTimes, ShouldEqual, 1) - - So(err, ShouldEqual, expected) - - teardown() - }) - - Convey("Should get only one user", func() { - mock := setup() - - mock.usersFirstReturn = []*models.ExternalUserInfo{ - { - Login: "one", - }, - - { - Login: "two", - }, - } - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - user, _, err := multi.User("test") - - So(mock.dialCalledTimes, ShouldEqual, 1) - So(mock.usersCalledTimes, ShouldEqual, 1) - So(mock.closeCalledTimes, ShouldEqual, 1) - - So(err, ShouldBeNil) - So(user.Login, ShouldEqual, "one") - - teardown() - }) - - Convey("Should still try to auth with the second server after receiving a dial error from the first", func() { - mock := setup() - - expectedError := errors.New("Dial error") - mock.dialErrReturn = expectedError - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, _, err := multi.User("test") - - So(mock.dialCalledTimes, ShouldEqual, 2) - So(err, ShouldEqual, expectedError) - - teardown() - }) + teardown() }) - Convey("Users()", func() { - Convey("Should still try to auth with the second server after receiving a dial error from the first", func() { - mock := setup() + t.Run("Should call underlying LDAP methods", func(t *testing.T) { + mock := setup() - expectedError := errors.New("Dial error") - mock.dialErrReturn = expectedError - - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, err := multi.Users([]string{"test"}) - - So(mock.dialCalledTimes, ShouldEqual, 2) - So(err, ShouldEqual, expectedError) - - teardown() + multi := New([]*ldap.ServerConfig{ + {}, {}, }) - Convey("Should return error for absent config list", func() { - setup() + _, err := multi.Login(&models.LoginUserQuery{}) - multi := New([]*ldap.ServerConfig{}) - _, err := multi.Users([]string{"test"}) + require.Equal(t, 2, mock.dialCalledTimes) + require.Equal(t, 2, mock.loginCalledTimes) + require.Equal(t, 2, mock.closeCalledTimes) - So(err, ShouldBeError) - So(err, ShouldEqual, ErrNoLDAPServers) + require.Equal(t, ErrInvalidCredentials, err) - teardown() + teardown() + }) + + t.Run("Should get login result", func(t *testing.T) { + mock := setup() + + mock.loginReturn = &models.ExternalUserInfo{ + Login: "killa", + } + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + result, err := multi.Login(&models.LoginUserQuery{}) + + require.Equal(t, 1, mock.dialCalledTimes) + require.Equal(t, 1, mock.loginCalledTimes) + require.Equal(t, 1, mock.closeCalledTimes) + + require.Equal(t, "killa", result.Login) + require.Nil(t, err) + + teardown() + }) + + t.Run("Should still call a second error for invalid not found error", func(t *testing.T) { + mock := setup() + + mock.loginErrReturn = ErrCouldNotFindUser + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + _, err := multi.Login(&models.LoginUserQuery{}) + + require.Equal(t, 2, mock.dialCalledTimes) + require.Equal(t, 2, mock.loginCalledTimes) + require.Equal(t, 2, mock.closeCalledTimes) + + require.Equal(t, ErrInvalidCredentials, err) + + teardown() + }) + + t.Run("Should still try to auth with the second server after receiving an invalid credentials error from the first", func(t *testing.T) { + mock := setup() + + mock.loginErrReturn = ErrInvalidCredentials + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + _, err := multi.Login(&models.LoginUserQuery{}) + + require.Equal(t, 2, mock.dialCalledTimes) + require.Equal(t, 2, mock.loginCalledTimes) + require.Equal(t, 2, mock.closeCalledTimes) + + require.Equal(t, ErrInvalidCredentials, err) + + teardown() + }) + + t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) { + mock := setup() + + expectedError := errors.New("Dial error") + mock.dialErrReturn = expectedError + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + _, err := multi.Login(&models.LoginUserQuery{}) + + require.Equal(t, 2, mock.dialCalledTimes) + + require.Equal(t, expectedError, err) + + teardown() + }) + + t.Run("Should return unknown error", func(t *testing.T) { + mock := setup() + + expected := errors.New("Something unknown") + mock.loginErrReturn = expected + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + _, err := multi.Login(&models.LoginUserQuery{}) + + require.Equal(t, 1, mock.dialCalledTimes) + require.Equal(t, 1, mock.loginCalledTimes) + require.Equal(t, 1, mock.closeCalledTimes) + + require.Equal(t, expected, err) + + teardown() + }) + }) + + t.Run("User()", func(t *testing.T) { + t.Run("Should return error for absent config list", func(t *testing.T) { + setup() + + multi := New([]*ldap.ServerConfig{}) + _, _, err := multi.User("test") + + require.Error(t, err) + require.Equal(t, ErrNoLDAPServers, err) + + teardown() + }) + + t.Run("Should return a dial error", func(t *testing.T) { + mock := setup() + + expected := errors.New("Dial error") + mock.dialErrReturn = expected + + multi := New([]*ldap.ServerConfig{ + {}, {}, }) - Convey("Should return a dial error", func() { - mock := setup() + _, _, err := multi.User("test") - expected := errors.New("Dial error") - mock.dialErrReturn = expected + require.Error(t, err) + require.Equal(t, expected, err) - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) + teardown() + }) - _, err := multi.Users([]string{"test"}) + t.Run("Should call underlying LDAP methods", func(t *testing.T) { + mock := setup() - So(err, ShouldBeError) - So(err, ShouldEqual, expected) + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + _, _, err := multi.User("test") - teardown() + require.Equal(t, 2, mock.dialCalledTimes) + require.Equal(t, 2, mock.usersCalledTimes) + require.Equal(t, 2, mock.closeCalledTimes) + + require.Equal(t, ErrDidNotFindUser, err) + + teardown() + }) + + t.Run("Should return some error", func(t *testing.T) { + mock := setup() + + expected := errors.New("Killa Gorilla") + mock.usersErrReturn = expected + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + _, _, err := multi.User("test") + + require.Equal(t, 1, mock.dialCalledTimes) + require.Equal(t, 1, mock.usersCalledTimes) + require.Equal(t, 1, mock.closeCalledTimes) + + require.Equal(t, expected, err) + + teardown() + }) + + t.Run("Should get only one user", func(t *testing.T) { + mock := setup() + + mock.usersFirstReturn = []*models.ExternalUserInfo{ + { + Login: "one", + }, + + { + Login: "two", + }, + } + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + user, _, err := multi.User("test") + + require.Equal(t, 1, mock.dialCalledTimes) + require.Equal(t, 1, mock.usersCalledTimes) + require.Equal(t, 1, mock.closeCalledTimes) + + require.Nil(t, err) + require.Equal(t, "one", user.Login) + + teardown() + }) + + t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) { + mock := setup() + + expectedError := errors.New("Dial error") + mock.dialErrReturn = expectedError + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + _, _, err := multi.User("test") + + require.Equal(t, 2, mock.dialCalledTimes) + require.Equal(t, expectedError, err) + + teardown() + }) + }) + + t.Run("Users()", func(t *testing.T) { + t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) { + mock := setup() + + expectedError := errors.New("Dial error") + mock.dialErrReturn = expectedError + + multi := New([]*ldap.ServerConfig{ + {}, {}, + }) + _, err := multi.Users([]string{"test"}) + + require.Equal(t, 2, mock.dialCalledTimes) + require.Equal(t, expectedError, err) + + teardown() + }) + t.Run("Should return error for absent config list", func(t *testing.T) { + setup() + + multi := New([]*ldap.ServerConfig{}) + _, err := multi.Users([]string{"test"}) + + require.Error(t, err) + require.Equal(t, ErrNoLDAPServers, err) + + teardown() + }) + + t.Run("Should return a dial error", func(t *testing.T) { + mock := setup() + + expected := errors.New("Dial error") + mock.dialErrReturn = expected + + multi := New([]*ldap.ServerConfig{ + {}, {}, }) - Convey("Should call underlying LDAP methods", func() { - mock := setup() + _, err := multi.Users([]string{"test"}) - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, err := multi.Users([]string{"test"}) + require.Error(t, err) + require.Equal(t, expected, err) - So(mock.dialCalledTimes, ShouldEqual, 2) - So(mock.usersCalledTimes, ShouldEqual, 2) - So(mock.closeCalledTimes, ShouldEqual, 2) + teardown() + }) - So(err, ShouldBeNil) + t.Run("Should call underlying LDAP methods", func(t *testing.T) { + mock := setup() - teardown() + multi := New([]*ldap.ServerConfig{ + {}, {}, }) + _, err := multi.Users([]string{"test"}) - Convey("Should return some error", func() { - mock := setup() + require.Equal(t, 2, mock.dialCalledTimes) + require.Equal(t, 2, mock.usersCalledTimes) + require.Equal(t, 2, mock.closeCalledTimes) - expected := errors.New("Killa Gorilla") - mock.usersErrReturn = expected + require.Nil(t, err) - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - _, err := multi.Users([]string{"test"}) + teardown() + }) - So(mock.dialCalledTimes, ShouldEqual, 1) - So(mock.usersCalledTimes, ShouldEqual, 1) - So(mock.closeCalledTimes, ShouldEqual, 1) + t.Run("Should return some error", func(t *testing.T) { + mock := setup() - So(err, ShouldEqual, expected) + expected := errors.New("Killa Gorilla") + mock.usersErrReturn = expected - teardown() + multi := New([]*ldap.ServerConfig{ + {}, {}, }) + _, err := multi.Users([]string{"test"}) - Convey("Should get users", func() { - mock := setup() + require.Equal(t, 1, mock.dialCalledTimes) + require.Equal(t, 1, mock.usersCalledTimes) + require.Equal(t, 1, mock.closeCalledTimes) - mock.usersFirstReturn = []*models.ExternalUserInfo{ - { - Login: "one", - }, + require.Equal(t, expected, err) - { - Login: "two", - }, - } + teardown() + }) - mock.usersRestReturn = []*models.ExternalUserInfo{ - { - Login: "three", - }, - } + t.Run("Should get users", func(t *testing.T) { + mock := setup() - multi := New([]*ldap.ServerConfig{ - {}, {}, - }) - users, err := multi.Users([]string{"test"}) + mock.usersFirstReturn = []*models.ExternalUserInfo{ + { + Login: "one", + }, - So(mock.dialCalledTimes, ShouldEqual, 2) - So(mock.usersCalledTimes, ShouldEqual, 2) - So(mock.closeCalledTimes, ShouldEqual, 2) + { + Login: "two", + }, + } - So(err, ShouldBeNil) - So(users[0].Login, ShouldEqual, "one") - So(users[1].Login, ShouldEqual, "two") - So(users[2].Login, ShouldEqual, "three") + mock.usersRestReturn = []*models.ExternalUserInfo{ + { + Login: "three", + }, + } - teardown() + multi := New([]*ldap.ServerConfig{ + {}, {}, }) + users, err := multi.Users([]string{"test"}) + + require.Equal(t, 2, mock.dialCalledTimes) + require.Equal(t, 2, mock.usersCalledTimes) + require.Equal(t, 2, mock.closeCalledTimes) + + require.Nil(t, err) + require.Equal(t, "one", users[0].Login) + require.Equal(t, "two", users[1].Login) + require.Equal(t, "three", users[2].Login) + + teardown() }) }) } diff --git a/pkg/services/ngalert/notifier/channels/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index 6184741eb26..65e53049f9d 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -219,19 +219,21 @@ var sendSlackRequest = func(request *http.Request, logger log.Logger) error { return fmt.Errorf("failed to read response body: %w", err) } - if resp.StatusCode/100 != 2 { + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { logger.Warn("Slack API request failed", "url", request.URL.String(), "statusCode", resp.Status, "body", string(body)) return fmt.Errorf("request to Slack API failed with status code %d", resp.StatusCode) } - var rslt map[string]interface{} // Slack responds to some requests with a JSON document, that might contain an error + rslt := struct { + Ok bool `json:"ok"` + Err string `json:"error"` + }{} if err := json.Unmarshal(body, &rslt); err == nil { - if !rslt["ok"].(bool) { - errMsg := rslt["error"].(string) + if !rslt.Ok && rslt.Err != "" { logger.Warn("Sending Slack API request failed", "url", request.URL.String(), "statusCode", resp.Status, - "err", errMsg) - return fmt.Errorf("failed to make Slack API request: %s", errMsg) + "err", rslt.Err) + return fmt.Errorf("failed to make Slack API request: %s", rslt.Err) } } diff --git a/pkg/services/ngalert/notifier/channels/slack_test.go b/pkg/services/ngalert/notifier/channels/slack_test.go index 32e876d2c9f..5ae0fac36a4 100644 --- a/pkg/services/ngalert/notifier/channels/slack_test.go +++ b/pkg/services/ngalert/notifier/channels/slack_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "io" "net/http" + "net/http/httptest" "net/url" "testing" @@ -221,3 +222,94 @@ func TestSlackNotifier(t *testing.T) { }) } } + +func TestSendSlackRequest(t *testing.T) { + tests := []struct { + name string + slackResponse string + statusCode int + expectError bool + }{ + { + name: "Example error", + slackResponse: `{ + "ok": false, + "error": "too_many_attachments" + }`, + statusCode: http.StatusBadRequest, + expectError: true, + }, + { + name: "Non 200 status code, no response body", + statusCode: http.StatusMovedPermanently, + expectError: true, + }, + { + name: "Success case, normal response body", + slackResponse: `{ + "ok": true, + "channel": "C1H9RESGL", + "ts": "1503435956.000247", + "message": { + "text": "Here's a message for you", + "username": "ecto1", + "bot_id": "B19LU7CSY", + "attachments": [ + { + "text": "This is an attachment", + "id": 1, + "fallback": "This is an attachment's fallback" + } + ], + "type": "message", + "subtype": "bot_message", + "ts": "1503435956.000247" + } + }`, + statusCode: http.StatusOK, + expectError: false, + }, + { + name: "Success case, no response body", + statusCode: http.StatusOK, + expectError: false, + }, + { + name: "Success case, unexpected response body", + statusCode: http.StatusOK, + slackResponse: "{}", + expectError: false, + }, + { + name: "Success case, ok: true", + statusCode: http.StatusOK, + slackResponse: "{\"ok\": true}", + expectError: false, + }, + { + name: "200 status code, error in body", + statusCode: http.StatusOK, + slackResponse: `{"ok": false, "error": "test error"}`, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.statusCode) + _, err := w.Write([]byte(test.slackResponse)) + require.NoError(tt, err) + })) + req, err := http.NewRequest(http.MethodGet, server.URL, nil) + require.NoError(tt, err) + + err = sendSlackRequest(req, log.New("test")) + if !test.expectError { + require.NoError(tt, err) + } else { + require.Error(tt, err) + } + }) + } +} diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 18ba9df943d..bec171a4c6c 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -13,400 +13,423 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + + "github.com/stretchr/testify/require" ) func TestAlertNotificationSQLAccess(t *testing.T) { - Convey("Testing Alert notification sql access", t, func() { + var sqlStore *SQLStore + setup := func() { sqlStore := InitTestDB(t) // Set up bus handlers bus.AddHandler("deleteAlertNotification", func(cmd *models.DeleteAlertNotificationCommand) error { return sqlStore.DeleteAlertNotification(cmd) }) + } - Convey("Alert notification state", func() { - var alertID int64 = 7 - var orgID int64 = 5 - var notifierID int64 = 10 - oldTimeNow := timeNow - now := time.Date(2018, 9, 30, 0, 0, 0, 0, time.UTC) - timeNow = func() time.Time { return now } + t.Run("Alert notification state", func(t *testing.T) { + setup() + var alertID int64 = 7 + var orgID int64 = 5 + var notifierID int64 = 10 + oldTimeNow := timeNow + now := time.Date(2018, 9, 30, 0, 0, 0, 0, time.UTC) + timeNow = func() time.Time { return now } - Convey("Get no existing state should create a new state", func() { - query := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} - err := sqlStore.GetOrCreateAlertNotificationState(context.Background(), query) - So(err, ShouldBeNil) - So(query.Result, ShouldNotBeNil) - So(query.Result.State, ShouldEqual, "unknown") - So(query.Result.Version, ShouldEqual, 0) - So(query.Result.UpdatedAt, ShouldEqual, now.Unix()) + defer func() { timeNow = oldTimeNow }() - Convey("Get existing state should not create a new state", func() { - query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} - err := sqlStore.GetOrCreateAlertNotificationState(context.Background(), query2) - So(err, ShouldBeNil) - So(query2.Result, ShouldNotBeNil) - So(query2.Result.Id, ShouldEqual, query.Result.Id) - So(query2.Result.UpdatedAt, ShouldEqual, now.Unix()) + t.Run("Get no existing state should create a new state", func(t *testing.T) { + query := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err := sqlStore.GetOrCreateAlertNotificationState(context.Background(), query) + require.Nil(t, err) + require.NotNil(t, query.Result) + require.Equal(t, models.AlertNotificationStateUnknown, query.Result.State) + require.Equal(t, int64(0), query.Result.Version) + require.Equal(t, now.Unix(), query.Result.UpdatedAt) + + t.Run("Get existing state should not create a new state", func(t *testing.T) { + query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err := sqlStore.GetOrCreateAlertNotificationState(context.Background(), query2) + require.Nil(t, err) + require.NotNil(t, query2.Result) + require.Equal(t, query.Result.Id, query2.Result.Id) + require.Equal(t, now.Unix(), query2.Result.UpdatedAt) + }) + + t.Run("Update existing state to pending with correct version should update database", func(t *testing.T) { + s := *query.Result + + cmd := models.SetAlertNotificationStateToPendingCommand{ + Id: s.Id, + Version: s.Version, + AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, + } + + err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) + require.Nil(t, err) + require.Equal(t, int64(1), cmd.ResultVersion) + + query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query2) + require.Nil(t, err) + require.Equal(t, int64(1), query2.Result.Version) + require.Equal(t, models.AlertNotificationStatePending, query2.Result.State) + require.Equal(t, now.Unix(), query2.Result.UpdatedAt) + + t.Run("Update existing state to completed should update database", func(t *testing.T) { + s := *query.Result + setStateCmd := models.SetAlertNotificationStateToCompleteCommand{ + Id: s.Id, + Version: cmd.ResultVersion, + } + err := sqlStore.SetAlertNotificationStateToCompleteCommand(context.Background(), &setStateCmd) + require.Nil(t, err) + + query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query3) + require.Nil(t, err) + require.Equal(t, int64(2), query3.Result.Version) + require.Equal(t, models.AlertNotificationStateCompleted, query3.Result.State) + require.Equal(t, now.Unix(), query3.Result.UpdatedAt) }) - Convey("Update existing state to pending with correct version should update database", func() { + t.Run("Update existing state to completed should update database. regardless of version", func(t *testing.T) { s := *query.Result - - cmd := models.SetAlertNotificationStateToPendingCommand{ - Id: s.Id, - Version: s.Version, - AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, + unknownVersion := int64(1000) + cmd := models.SetAlertNotificationStateToCompleteCommand{ + Id: s.Id, + Version: unknownVersion, } + err := sqlStore.SetAlertNotificationStateToCompleteCommand(context.Background(), &cmd) + require.Nil(t, err) - err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) - So(err, ShouldBeNil) - So(cmd.ResultVersion, ShouldEqual, 1) - - query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} - err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query2) - So(err, ShouldBeNil) - So(query2.Result.Version, ShouldEqual, 1) - So(query2.Result.State, ShouldEqual, models.AlertNotificationStatePending) - So(query2.Result.UpdatedAt, ShouldEqual, now.Unix()) - - Convey("Update existing state to completed should update database", func() { - s := *query.Result - setStateCmd := models.SetAlertNotificationStateToCompleteCommand{ - Id: s.Id, - Version: cmd.ResultVersion, - } - err := sqlStore.SetAlertNotificationStateToCompleteCommand(context.Background(), &setStateCmd) - So(err, ShouldBeNil) - - query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} - err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query3) - So(err, ShouldBeNil) - So(query3.Result.Version, ShouldEqual, 2) - So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted) - So(query3.Result.UpdatedAt, ShouldEqual, now.Unix()) - }) - - Convey("Update existing state to completed should update database. regardless of version", func() { - s := *query.Result - unknownVersion := int64(1000) - cmd := models.SetAlertNotificationStateToCompleteCommand{ - Id: s.Id, - Version: unknownVersion, - } - err := sqlStore.SetAlertNotificationStateToCompleteCommand(context.Background(), &cmd) - So(err, ShouldBeNil) - - query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} - err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query3) - So(err, ShouldBeNil) - So(query3.Result.Version, ShouldEqual, unknownVersion+1) - So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted) - So(query3.Result.UpdatedAt, ShouldEqual, now.Unix()) - }) - }) - - Convey("Update existing state to pending with incorrect version should return version mismatch error", func() { - s := *query.Result - s.Version = 1000 - cmd := models.SetAlertNotificationStateToPendingCommand{ - Id: s.NotifierId, - Version: s.Version, - AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, - } - err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) - So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict) - }) - - Convey("Updating existing state to pending with incorrect version since alert rule state update version is higher", func() { - s := *query.Result - cmd := models.SetAlertNotificationStateToPendingCommand{ - Id: s.Id, - Version: s.Version, - AlertRuleStateUpdatedVersion: 1000, - } - err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) - So(err, ShouldBeNil) - - So(cmd.ResultVersion, ShouldEqual, 1) - }) - - Convey("different version and same alert state change version should return error", func() { - s := *query.Result - s.Version = 1000 - cmd := models.SetAlertNotificationStateToPendingCommand{ - Id: s.Id, - Version: s.Version, - AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, - } - err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) - So(err, ShouldNotBeNil) + query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID} + err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query3) + require.Nil(t, err) + require.Equal(t, unknownVersion+1, query3.Result.Version) + require.Equal(t, models.AlertNotificationStateCompleted, query3.Result.State) + require.Equal(t, now.Unix(), query3.Result.UpdatedAt) }) }) - Reset(func() { - timeNow = oldTimeNow + t.Run("Update existing state to pending with incorrect version should return version mismatch error", func(t *testing.T) { + s := *query.Result + s.Version = 1000 + cmd := models.SetAlertNotificationStateToPendingCommand{ + Id: s.NotifierId, + Version: s.Version, + AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, + } + err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) + require.Equal(t, models.ErrAlertNotificationStateVersionConflict, err) + }) + + t.Run("Updating existing state to pending with incorrect version since alert rule state update version is higher", func(t *testing.T) { + s := *query.Result + cmd := models.SetAlertNotificationStateToPendingCommand{ + Id: s.Id, + Version: s.Version, + AlertRuleStateUpdatedVersion: 1000, + } + err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) + require.Nil(t, err) + + require.Equal(t, int64(1), cmd.ResultVersion) + }) + + t.Run("different version and same alert state change version should return error", func(t *testing.T) { + s := *query.Result + s.Version = 1000 + cmd := models.SetAlertNotificationStateToPendingCommand{ + Id: s.Id, + Version: s.Version, + AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion, + } + err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd) + require.Error(t, err) }) }) + }) - Convey("Alert notifications should be empty", func() { - cmd := &models.GetAlertNotificationsQuery{ - OrgId: 2, - Name: "email", - } + t.Run("Alert notifications should be empty", func(t *testing.T) { + setup() + cmd := &models.GetAlertNotificationsQuery{ + OrgId: 2, + Name: "email", + } - err := sqlStore.GetAlertNotifications(cmd) - So(err, ShouldBeNil) - So(cmd.Result, ShouldBeNil) + err := sqlStore.GetAlertNotifications(cmd) + require.Nil(t, err) + require.Nil(t, cmd.Result) + }) + + t.Run("Cannot save alert notifier with send reminder = true", func(t *testing.T) { + setup() + cmd := &models.CreateAlertNotificationCommand{ + Name: "ops", + Type: "email", + OrgId: 1, + SendReminder: true, + Settings: simplejson.New(), + } + + t.Run("and missing frequency", func(t *testing.T) { + err := sqlStore.CreateAlertNotificationCommand(cmd) + require.Equal(t, models.ErrNotificationFrequencyNotFound, err) }) - Convey("Cannot save alert notifier with send reminder = true", func() { - cmd := &models.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", + t.Run("invalid frequency", func(t *testing.T) { + cmd.Frequency = "invalid duration" + + err := sqlStore.CreateAlertNotificationCommand(cmd) + require.True(t, regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString( + err.Error())) + }) + }) + + t.Run("Cannot update alert notifier with send reminder = false", func(t *testing.T) { + setup() + cmd := &models.CreateAlertNotificationCommand{ + Name: "ops update", + Type: "email", + OrgId: 1, + SendReminder: false, + Settings: simplejson.New(), + } + + err := sqlStore.CreateAlertNotificationCommand(cmd) + require.Nil(t, err) + + updateCmd := &models.UpdateAlertNotificationCommand{ + Id: cmd.Result.Id, + SendReminder: true, + } + + t.Run("and missing frequency", func(t *testing.T) { + err := sqlStore.UpdateAlertNotification(updateCmd) + require.Equal(t, models.ErrNotificationFrequencyNotFound, err) + }) + + t.Run("invalid frequency", func(t *testing.T) { + updateCmd.Frequency = "invalid duration" + + err := sqlStore.UpdateAlertNotification(updateCmd) + require.Error(t, err) + require.True(t, regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString( + err.Error())) + }) + }) + + t.Run("Can save Alert Notification", func(t *testing.T) { + setup() + cmd := &models.CreateAlertNotificationCommand{ + Name: "ops", + Type: "email", + OrgId: 1, + SendReminder: true, + Frequency: "10s", + Settings: simplejson.New(), + } + + err := sqlStore.CreateAlertNotificationCommand(cmd) + require.Nil(t, err) + require.NotEqual(t, 0, cmd.Result.Id) + require.NotEqual(t, 0, cmd.Result.OrgId) + require.Equal(t, "email", cmd.Result.Type) + require.Equal(t, 10*time.Second, cmd.Result.Frequency) + require.False(t, cmd.Result.DisableResolveMessage) + require.NotEmpty(t, cmd.Result.Uid) + + t.Run("Cannot save Alert Notification with the same name", func(t *testing.T) { + err = sqlStore.CreateAlertNotificationCommand(cmd) + require.Error(t, err) + }) + t.Run("Cannot save Alert Notification with the same name and another uid", func(t *testing.T) { + anotherUidCmd := &models.CreateAlertNotificationCommand{ + Name: cmd.Name, + Type: cmd.Type, OrgId: 1, - SendReminder: true, - Settings: simplejson.New(), + SendReminder: cmd.SendReminder, + Frequency: cmd.Frequency, + Settings: cmd.Settings, + Uid: "notifier1", } - - Convey("and missing frequency", func() { - err := sqlStore.CreateAlertNotificationCommand(cmd) - So(err, ShouldEqual, models.ErrNotificationFrequencyNotFound) - }) - - Convey("invalid frequency", func() { - cmd.Frequency = "invalid duration" - - err := sqlStore.CreateAlertNotificationCommand(cmd) - So(regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString( - err.Error()), ShouldBeTrue) - }) + err = sqlStore.CreateAlertNotificationCommand(anotherUidCmd) + require.Error(t, err) + }) + t.Run("Can save Alert Notification with another name and another uid", func(t *testing.T) { + anotherUidCmd := &models.CreateAlertNotificationCommand{ + Name: "another ops", + Type: cmd.Type, + OrgId: 1, + SendReminder: cmd.SendReminder, + Frequency: cmd.Frequency, + Settings: cmd.Settings, + Uid: "notifier2", + } + err = sqlStore.CreateAlertNotificationCommand(anotherUidCmd) + require.Nil(t, err) }) - Convey("Cannot update alert notifier with send reminder = false", func() { - cmd := &models.CreateAlertNotificationCommand{ - Name: "ops update", - Type: "email", - OrgId: 1, + t.Run("Can update alert notification", func(t *testing.T) { + newCmd := &models.UpdateAlertNotificationCommand{ + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + SendReminder: true, + DisableResolveMessage: true, + Frequency: "60s", + Settings: simplejson.New(), + Id: cmd.Result.Id, + } + err := sqlStore.UpdateAlertNotification(newCmd) + require.Nil(t, err) + require.Equal(t, "NewName", newCmd.Result.Name) + require.Equal(t, 60*time.Second, newCmd.Result.Frequency) + require.True(t, newCmd.Result.DisableResolveMessage) + }) + + t.Run("Can update alert notification to disable sending of reminders", func(t *testing.T) { + newCmd := &models.UpdateAlertNotificationCommand{ + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, SendReminder: false, Settings: simplejson.New(), - } - - err := sqlStore.CreateAlertNotificationCommand(cmd) - So(err, ShouldBeNil) - - updateCmd := &models.UpdateAlertNotificationCommand{ Id: cmd.Result.Id, - SendReminder: true, + } + err := sqlStore.UpdateAlertNotification(newCmd) + require.Nil(t, err) + require.False(t, newCmd.Result.SendReminder) + }) + }) + + t.Run("Can search using an array of ids", func(t *testing.T) { + setup() + cmd1 := models.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd2 := models.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd3 := models.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd4 := models.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + + otherOrg := models.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + + require.Nil(t, sqlStore.CreateAlertNotificationCommand(&cmd1)) + require.Nil(t, sqlStore.CreateAlertNotificationCommand(&cmd2)) + require.Nil(t, sqlStore.CreateAlertNotificationCommand(&cmd3)) + require.Nil(t, sqlStore.CreateAlertNotificationCommand(&cmd4)) + require.Nil(t, sqlStore.CreateAlertNotificationCommand(&otherOrg)) + + t.Run("search", func(t *testing.T) { + query := &models.GetAlertNotificationsWithUidToSendQuery{ + Uids: []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"}, + OrgId: 1, } - Convey("and missing frequency", func() { - err := sqlStore.UpdateAlertNotification(updateCmd) - So(err, ShouldEqual, models.ErrNotificationFrequencyNotFound) - }) - - Convey("invalid frequency", func() { - updateCmd.Frequency = "invalid duration" - - err := sqlStore.UpdateAlertNotification(updateCmd) - So(err, ShouldNotBeNil) - So(regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString( - err.Error()), ShouldBeTrue) - }) + err := sqlStore.GetAlertNotificationsWithUidToSend(query) + require.Nil(t, err) + require.Equal(t, 3, len(query.Result)) }) - Convey("Can save Alert Notification", func() { - cmd := &models.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - SendReminder: true, - Frequency: "10s", - Settings: simplejson.New(), + t.Run("all", func(t *testing.T) { + query := &models.GetAllAlertNotificationsQuery{ + OrgId: 1, } - err := sqlStore.CreateAlertNotificationCommand(cmd) - So(err, ShouldBeNil) - So(cmd.Result.Id, ShouldNotEqual, 0) - So(cmd.Result.OrgId, ShouldNotEqual, 0) - So(cmd.Result.Type, ShouldEqual, "email") - So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) - So(cmd.Result.DisableResolveMessage, ShouldBeFalse) - So(cmd.Result.Uid, ShouldNotBeEmpty) - - Convey("Cannot save Alert Notification with the same name", func() { - err = sqlStore.CreateAlertNotificationCommand(cmd) - So(err, ShouldNotBeNil) - }) - Convey("Cannot save Alert Notification with the same name and another uid", func() { - anotherUidCmd := &models.CreateAlertNotificationCommand{ - Name: cmd.Name, - Type: cmd.Type, - OrgId: 1, - SendReminder: cmd.SendReminder, - Frequency: cmd.Frequency, - Settings: cmd.Settings, - Uid: "notifier1", - } - err = sqlStore.CreateAlertNotificationCommand(anotherUidCmd) - So(err, ShouldNotBeNil) - }) - Convey("Can save Alert Notification with another name and another uid", func() { - anotherUidCmd := &models.CreateAlertNotificationCommand{ - Name: "another ops", - Type: cmd.Type, - OrgId: 1, - SendReminder: cmd.SendReminder, - Frequency: cmd.Frequency, - Settings: cmd.Settings, - Uid: "notifier2", - } - err = sqlStore.CreateAlertNotificationCommand(anotherUidCmd) - So(err, ShouldBeNil) - }) - - Convey("Can update alert notification", func() { - newCmd := &models.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - SendReminder: true, - DisableResolveMessage: true, - Frequency: "60s", - Settings: simplejson.New(), - Id: cmd.Result.Id, - } - err := sqlStore.UpdateAlertNotification(newCmd) - So(err, ShouldBeNil) - So(newCmd.Result.Name, ShouldEqual, "NewName") - So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second) - So(newCmd.Result.DisableResolveMessage, ShouldBeTrue) - }) - - Convey("Can update alert notification to disable sending of reminders", func() { - newCmd := &models.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - SendReminder: false, - Settings: simplejson.New(), - Id: cmd.Result.Id, - } - err := sqlStore.UpdateAlertNotification(newCmd) - So(err, ShouldBeNil) - So(newCmd.Result.SendReminder, ShouldBeFalse) - }) + err := sqlStore.GetAllAlertNotifications(query) + require.Nil(t, err) + require.Equal(t, 4, len(query.Result)) + require.Equal(t, cmd4.Name, query.Result[0].Name) + require.Equal(t, cmd1.Name, query.Result[1].Name) + require.Equal(t, cmd3.Name, query.Result[2].Name) + require.Equal(t, cmd2.Name, query.Result[3].Name) }) + }) - Convey("Can search using an array of ids", func() { - cmd1 := models.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - cmd2 := models.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - cmd3 := models.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - cmd4 := models.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + t.Run("Notification Uid by Id Caching", func(t *testing.T) { + setup() + ss := InitTestDB(t) - otherOrg := models.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + notification := &models.CreateAlertNotificationCommand{Uid: "aNotificationUid", OrgId: 1, Name: "aNotificationUid"} + err := sqlStore.CreateAlertNotificationCommand(notification) + require.Nil(t, err) - So(sqlStore.CreateAlertNotificationCommand(&cmd1), ShouldBeNil) - So(sqlStore.CreateAlertNotificationCommand(&cmd2), ShouldBeNil) - So(sqlStore.CreateAlertNotificationCommand(&cmd3), ShouldBeNil) - So(sqlStore.CreateAlertNotificationCommand(&cmd4), ShouldBeNil) - So(sqlStore.CreateAlertNotificationCommand(&otherOrg), ShouldBeNil) + byUidQuery := &models.GetAlertNotificationsWithUidQuery{ + Uid: notification.Uid, + OrgId: notification.OrgId, + } - Convey("search", func() { - query := &models.GetAlertNotificationsWithUidToSendQuery{ - Uids: []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"}, - OrgId: 1, - } + notificationByUidErr := sqlStore.GetAlertNotificationsWithUid(byUidQuery) + require.Nil(t, notificationByUidErr) - err := sqlStore.GetAlertNotificationsWithUidToSend(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 3) - }) - - Convey("all", func() { - query := &models.GetAllAlertNotificationsQuery{ - OrgId: 1, - } - - err := sqlStore.GetAllAlertNotifications(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 4) - So(query.Result[0].Name, ShouldEqual, cmd4.Name) - So(query.Result[1].Name, ShouldEqual, cmd1.Name) - So(query.Result[2].Name, ShouldEqual, cmd3.Name) - So(query.Result[3].Name, ShouldEqual, cmd2.Name) - }) - }) - - Convey("Notification Uid by Id Caching", func() { - ss := InitTestDB(t) - - notification := &models.CreateAlertNotificationCommand{Uid: "aNotificationUid", OrgId: 1, Name: "aNotificationUid"} - err := sqlStore.CreateAlertNotificationCommand(notification) - So(err, ShouldBeNil) - - byUidQuery := &models.GetAlertNotificationsWithUidQuery{ - Uid: notification.Uid, - OrgId: notification.OrgId, + t.Run("Can cache notification Uid", func(t *testing.T) { + byIdQuery := &models.GetAlertNotificationUidQuery{ + Id: byUidQuery.Result.Id, + OrgId: byUidQuery.Result.OrgId, } - notificationByUidErr := sqlStore.GetAlertNotificationsWithUid(byUidQuery) - So(notificationByUidErr, ShouldBeNil) + cacheKey := newAlertNotificationUidCacheKey(byIdQuery.OrgId, byIdQuery.Id) - Convey("Can cache notification Uid", func() { - byIdQuery := &models.GetAlertNotificationUidQuery{ - Id: byUidQuery.Result.Id, - OrgId: byUidQuery.Result.OrgId, - } + resultBeforeCaching, foundBeforeCaching := ss.CacheService.Get(cacheKey) + require.False(t, foundBeforeCaching) + require.Nil(t, resultBeforeCaching) - cacheKey := newAlertNotificationUidCacheKey(byIdQuery.OrgId, byIdQuery.Id) + notificationByIdErr := ss.GetAlertNotificationUidWithId(byIdQuery) + require.Nil(t, notificationByIdErr) - resultBeforeCaching, foundBeforeCaching := ss.CacheService.Get(cacheKey) - So(foundBeforeCaching, ShouldBeFalse) - So(resultBeforeCaching, ShouldBeNil) - - notificationByIdErr := ss.GetAlertNotificationUidWithId(byIdQuery) - So(notificationByIdErr, ShouldBeNil) - - resultAfterCaching, foundAfterCaching := ss.CacheService.Get(cacheKey) - So(foundAfterCaching, ShouldBeTrue) - So(resultAfterCaching, ShouldEqual, notification.Uid) - }) - - Convey("Retrieves from cache when exists", func() { - query := &models.GetAlertNotificationUidQuery{ - Id: 999, - OrgId: 100, - } - cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id) - ss.CacheService.Set(cacheKey, "a-cached-uid", -1) - - err := ss.GetAlertNotificationUidWithId(query) - So(err, ShouldBeNil) - So(query.Result, ShouldEqual, "a-cached-uid") - }) - - Convey("Returns an error without populating cache when the notification doesn't exist in the database", func() { - query := &models.GetAlertNotificationUidQuery{ - Id: -1, - OrgId: 100, - } - - err := ss.GetAlertNotificationUidWithId(query) - So(query.Result, ShouldEqual, "") - So(err, ShouldNotBeNil) - So(errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID), ShouldBeTrue) - - cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id) - result, found := ss.CacheService.Get(cacheKey) - So(found, ShouldBeFalse) - So(result, ShouldBeNil) - }) + resultAfterCaching, foundAfterCaching := ss.CacheService.Get(cacheKey) + require.True(t, foundAfterCaching) + require.Equal(t, notification.Uid, resultAfterCaching) }) - Convey("Cannot update non-existing Alert Notification", func() { - updateCmd := &models.UpdateAlertNotificationCommand{ + t.Run("Retrieves from cache when exists", func(t *testing.T) { + query := &models.GetAlertNotificationUidQuery{ + Id: 999, + OrgId: 100, + } + cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id) + ss.CacheService.Set(cacheKey, "a-cached-uid", -1) + + err := ss.GetAlertNotificationUidWithId(query) + require.Nil(t, err) + require.Equal(t, "a-cached-uid", query.Result) + }) + + t.Run("Returns an error without populating cache when the notification doesn't exist in the database", func(t *testing.T) { + query := &models.GetAlertNotificationUidQuery{ + Id: -1, + OrgId: 100, + } + + err := ss.GetAlertNotificationUidWithId(query) + require.Equal(t, "", query.Result) + require.Error(t, err) + require.True(t, errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID)) + + cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id) + result, found := ss.CacheService.Get(cacheKey) + require.False(t, found) + require.Nil(t, result) + }) + }) + + t.Run("Cannot update non-existing Alert Notification", func(t *testing.T) { + setup() + updateCmd := &models.UpdateAlertNotificationCommand{ + Name: "NewName", + Type: "webhook", + OrgId: 1, + SendReminder: true, + DisableResolveMessage: true, + Frequency: "60s", + Settings: simplejson.New(), + Id: 1, + } + err := sqlStore.UpdateAlertNotification(updateCmd) + require.Equal(t, models.ErrAlertNotificationNotFound, err) + + t.Run("using UID", func(t *testing.T) { + updateWithUidCmd := &models.UpdateAlertNotificationWithUidCommand{ Name: "NewName", Type: "webhook", OrgId: 1, @@ -414,78 +437,65 @@ func TestAlertNotificationSQLAccess(t *testing.T) { DisableResolveMessage: true, Frequency: "60s", Settings: simplejson.New(), - Id: 1, + Uid: "uid", + NewUid: "newUid", } - err := sqlStore.UpdateAlertNotification(updateCmd) - So(err, ShouldEqual, models.ErrAlertNotificationNotFound) - - Convey("using UID", func() { - updateWithUidCmd := &models.UpdateAlertNotificationWithUidCommand{ - Name: "NewName", - Type: "webhook", - OrgId: 1, - SendReminder: true, - DisableResolveMessage: true, - Frequency: "60s", - Settings: simplejson.New(), - Uid: "uid", - NewUid: "newUid", - } - err := sqlStore.UpdateAlertNotificationWithUid(updateWithUidCmd) - So(err, ShouldEqual, models.ErrAlertNotificationNotFound) - }) + err := sqlStore.UpdateAlertNotificationWithUid(updateWithUidCmd) + require.Equal(t, models.ErrAlertNotificationNotFound, err) }) + }) - Convey("Can delete Alert Notification", func() { - cmd := &models.CreateAlertNotificationCommand{ - Name: "ops update", - Type: "email", - OrgId: 1, - SendReminder: false, - Settings: simplejson.New(), - } + t.Run("Can delete Alert Notification", func(t *testing.T) { + setup() + cmd := &models.CreateAlertNotificationCommand{ + Name: "ops update", + Type: "email", + OrgId: 1, + SendReminder: false, + Settings: simplejson.New(), + } + err := sqlStore.CreateAlertNotificationCommand(cmd) + require.Nil(t, err) + + deleteCmd := &models.DeleteAlertNotificationCommand{ + Id: cmd.Result.Id, + OrgId: 1, + } + err = sqlStore.DeleteAlertNotification(deleteCmd) + require.Nil(t, err) + + t.Run("using UID", func(t *testing.T) { err := sqlStore.CreateAlertNotificationCommand(cmd) - So(err, ShouldBeNil) + require.Nil(t, err) - deleteCmd := &models.DeleteAlertNotificationCommand{ - Id: cmd.Result.Id, + deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{ + Uid: cmd.Result.Uid, OrgId: 1, } - err = sqlStore.DeleteAlertNotification(deleteCmd) - So(err, ShouldBeNil) - Convey("using UID", func() { - err := sqlStore.CreateAlertNotificationCommand(cmd) - So(err, ShouldBeNil) - - deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{ - Uid: cmd.Result.Uid, - OrgId: 1, - } - - err = sqlStore.DeleteAlertNotificationWithUid(deleteWithUidCmd) - So(err, ShouldBeNil) - So(deleteWithUidCmd.DeletedAlertNotificationId, ShouldEqual, cmd.Result.Id) - }) + err = sqlStore.DeleteAlertNotificationWithUid(deleteWithUidCmd) + require.Nil(t, err) + require.Equal(t, cmd.Result.Id, deleteWithUidCmd.DeletedAlertNotificationId) }) + }) - Convey("Cannot delete non-existing Alert Notification", func() { - deleteCmd := &models.DeleteAlertNotificationCommand{ - Id: 1, + t.Run("Cannot delete non-existing Alert Notification", func(t *testing.T) { + setup() + deleteCmd := &models.DeleteAlertNotificationCommand{ + Id: 1, + OrgId: 1, + } + err := sqlStore.DeleteAlertNotification(deleteCmd) + require.Equal(t, models.ErrAlertNotificationNotFound, err) + + t.Run("using UID", func(t *testing.T) { + deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{ + Uid: "uid", OrgId: 1, } - err := sqlStore.DeleteAlertNotification(deleteCmd) - So(err, ShouldEqual, models.ErrAlertNotificationNotFound) - - Convey("using UID", func() { - deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{ - Uid: "uid", - OrgId: 1, - } - err = sqlStore.DeleteAlertNotificationWithUid(deleteWithUidCmd) - So(err, ShouldEqual, models.ErrAlertNotificationNotFound) - }) + err = sqlStore.DeleteAlertNotificationWithUid(deleteWithUidCmd) + require.Equal(t, models.ErrAlertNotificationNotFound, err) }) }) } diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index d461de62a35..84b4cf9ec60 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -10,7 +10,8 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + + "github.com/stretchr/testify/require" ) func mockTimeNow() { @@ -31,13 +32,18 @@ func TestAlertingDataAccess(t *testing.T) { mockTimeNow() defer resetTimeNow() - Convey("Testing Alerting data access", t, func() { - sqlStore := InitTestDB(t) + var sqlStore *SQLStore + var testDash *models.Dashboard + var cmd models.SaveAlertsCommand + var items []*models.Alert - testDash := insertTestDashboard(t, sqlStore, "dashboard with alerts", 1, 0, false, "alert") + setup := func(t *testing.T) { + sqlStore = InitTestDB(t) + + testDash = insertTestDashboard(t, sqlStore, "dashboard with alerts", 1, 0, false, "alert") evalData, err := simplejson.NewJson([]byte(`{"test": "test"}`)) - So(err, ShouldBeNil) - items := []*models.Alert{ + require.Nil(t, err) + items = []*models.Alert{ { PanelId: 1, DashboardId: testDash.Id, @@ -50,7 +56,7 @@ func TestAlertingDataAccess(t *testing.T) { }, } - cmd := models.SaveAlertsCommand{ + cmd = models.SaveAlertsCommand{ Alerts: items, DashboardId: testDash.Id, OrgId: 1, @@ -58,234 +64,237 @@ func TestAlertingDataAccess(t *testing.T) { } err = SaveAlerts(&cmd) + require.Nil(t, err) + } - Convey("Can create one alert", func() { - So(err, ShouldBeNil) + t.Run("Can set new states", func(t *testing.T) { + setup(t) + + // Get alert so we can use its ID in tests + alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} + err2 := HandleAlertsQuery(&alertQuery) + require.Nil(t, err2) + + insertedAlert := alertQuery.Result[0] + + t.Run("new state ok", func(t *testing.T) { + cmd := &models.SetAlertStateCommand{ + AlertId: insertedAlert.Id, + State: models.AlertStateOK, + } + + err := SetAlertState(cmd) + require.Nil(t, err) }) - Convey("Can set new states", func() { + alert, _ := getAlertById(t, insertedAlert.Id) + stateDateBeforePause := alert.NewStateDate - // Get alert so we can use its ID in tests - alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} - err2 := HandleAlertsQuery(&alertQuery) - So(err2, ShouldBeNil) + t.Run("can pause all alerts", func(t *testing.T) { + err := pauseAllAlerts(t, true) + require.Nil(t, err) - insertedAlert := alertQuery.Result[0] - - Convey("new state ok", func() { + t.Run("cannot updated paused alert", func(t *testing.T) { cmd := &models.SetAlertStateCommand{ AlertId: insertedAlert.Id, State: models.AlertStateOK, } err = SetAlertState(cmd) - So(err, ShouldBeNil) + require.Error(t, err) }) - alert, _ := getAlertById(insertedAlert.Id) - stateDateBeforePause := alert.NewStateDate + t.Run("alert is paused", func(t *testing.T) { + alert, _ = getAlertById(t, insertedAlert.Id) + currentState := alert.State + require.Equal(t, models.AlertStatePaused, currentState) + }) - Convey("can pause all alerts", func() { - err := pauseAllAlerts(true) - So(err, ShouldBeNil) + t.Run("pausing alerts should update their NewStateDate", func(t *testing.T) { + alert, _ = getAlertById(t, insertedAlert.Id) + stateDateAfterPause := alert.NewStateDate + require.True(t, stateDateBeforePause.Before(stateDateAfterPause)) + }) - Convey("cannot updated paused alert", func() { - cmd := &models.SetAlertStateCommand{ - AlertId: insertedAlert.Id, - State: models.AlertStateOK, - } + t.Run("unpausing alerts should update their NewStateDate again", func(t *testing.T) { + err := pauseAllAlerts(t, false) + require.Nil(t, err) + alert, _ = getAlertById(t, insertedAlert.Id) + stateDateAfterUnpause := alert.NewStateDate + require.True(t, stateDateBeforePause.Before(stateDateAfterUnpause)) + }) + }) + }) - err = SetAlertState(cmd) - So(err, ShouldNotBeNil) - }) + t.Run("Can read properties", func(t *testing.T) { + setup(t) + alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} + err2 := HandleAlertsQuery(&alertQuery) - Convey("alert is paused", func() { - alert, _ = getAlertById(insertedAlert.Id) - currentState := alert.State - So(currentState, ShouldEqual, "paused") - }) + alert := alertQuery.Result[0] + require.Nil(t, err2) + require.Greater(t, alert.Id, int64(0)) + require.Equal(t, testDash.Id, alert.DashboardId) + require.Equal(t, int64(1), alert.PanelId) + require.Equal(t, "Alerting title", alert.Name) + require.Equal(t, models.AlertStateUnknown, alert.State) + require.NotNil(t, alert.NewStateDate) + require.NotNil(t, alert.EvalData) + require.Equal(t, "test", alert.EvalData.Get("test").MustString()) + require.NotNil(t, alert.EvalDate) + require.Equal(t, "", alert.ExecutionError) + require.NotNil(t, alert.DashboardUid) + require.Equal(t, "dashboard-with-alerts", alert.DashboardSlug) + }) - Convey("pausing alerts should update their NewStateDate", func() { - alert, _ = getAlertById(insertedAlert.Id) - stateDateAfterPause := alert.NewStateDate - So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause) - }) + t.Run("Viewer can read alerts", func(t *testing.T) { + setup(t) + viewerUser := &models.SignedInUser{OrgRole: models.ROLE_VIEWER, OrgId: 1} + alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser} + err2 := HandleAlertsQuery(&alertQuery) - Convey("unpausing alerts should update their NewStateDate again", func() { - err := pauseAllAlerts(false) - So(err, ShouldBeNil) - alert, _ = getAlertById(insertedAlert.Id) - stateDateAfterUnpause := alert.NewStateDate - So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterUnpause) - }) + require.Nil(t, err2) + require.Equal(t, 1, len(alertQuery.Result)) + }) + + t.Run("Alerts with same dashboard id and panel id should update", func(t *testing.T) { + setup(t) + modifiedItems := items + modifiedItems[0].Name = "Name" + + modifiedCmd := models.SaveAlertsCommand{ + DashboardId: testDash.Id, + OrgId: 1, + UserId: 1, + Alerts: modifiedItems, + } + + err := SaveAlerts(&modifiedCmd) + + t.Run("Can save alerts with same dashboard and panel id", func(t *testing.T) { + require.Nil(t, err) + }) + + t.Run("Alerts should be updated", func(t *testing.T) { + query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} + err2 := HandleAlertsQuery(&query) + + require.Nil(t, err2) + require.Equal(t, 1, len(query.Result)) + require.Equal(t, "Name", query.Result[0].Name) + + t.Run("Alert state should not be updated", func(t *testing.T) { + require.Equal(t, models.AlertStateUnknown, query.Result[0].State) }) }) - Convey("Can read properties", func() { - alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} - err2 := HandleAlertsQuery(&alertQuery) - - alert := alertQuery.Result[0] - So(err2, ShouldBeNil) - So(alert.Id, ShouldBeGreaterThan, 0) - So(alert.DashboardId, ShouldEqual, testDash.Id) - So(alert.PanelId, ShouldEqual, 1) - So(alert.Name, ShouldEqual, "Alerting title") - So(alert.State, ShouldEqual, models.AlertStateUnknown) - So(alert.NewStateDate, ShouldNotBeNil) - So(alert.EvalData, ShouldNotBeNil) - So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test") - So(alert.EvalDate, ShouldNotBeNil) - So(alert.ExecutionError, ShouldEqual, "") - So(alert.DashboardUid, ShouldNotBeNil) - So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts") + t.Run("Updates without changes should be ignored", func(t *testing.T) { + err3 := SaveAlerts(&modifiedCmd) + require.Nil(t, err3) }) + }) - Convey("Viewer can read alerts", func() { - viewerUser := &models.SignedInUser{OrgRole: models.ROLE_VIEWER, OrgId: 1} - alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser} - err2 := HandleAlertsQuery(&alertQuery) - - So(err2, ShouldBeNil) - So(alertQuery.Result, ShouldHaveLength, 1) - }) - - Convey("Alerts with same dashboard id and panel id should update", func() { - modifiedItems := items - modifiedItems[0].Name = "Name" - - modifiedCmd := models.SaveAlertsCommand{ + t.Run("Multiple alerts per dashboard", func(t *testing.T) { + setup(t) + multipleItems := []*models.Alert{ + { DashboardId: testDash.Id, + PanelId: 1, + Name: "1", OrgId: 1, - UserId: 1, - Alerts: modifiedItems, - } + Settings: simplejson.New(), + }, + { + DashboardId: testDash.Id, + PanelId: 2, + Name: "2", + OrgId: 1, + Settings: simplejson.New(), + }, + { + DashboardId: testDash.Id, + PanelId: 3, + Name: "3", + OrgId: 1, + Settings: simplejson.New(), + }, + } - err := SaveAlerts(&modifiedCmd) + cmd.Alerts = multipleItems + err := SaveAlerts(&cmd) - Convey("Can save alerts with same dashboard and panel id", func() { - So(err, ShouldBeNil) - }) + t.Run("Should save 3 dashboards", func(t *testing.T) { + require.Nil(t, err) - Convey("Alerts should be updated", func() { - query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} - err2 := HandleAlertsQuery(&query) + queryForDashboard := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} + err2 := HandleAlertsQuery(&queryForDashboard) - So(err2, ShouldBeNil) - So(len(query.Result), ShouldEqual, 1) - So(query.Result[0].Name, ShouldEqual, "Name") - - Convey("Alert state should not be updated", func() { - So(query.Result[0].State, ShouldEqual, models.AlertStateUnknown) - }) - }) - - Convey("Updates without changes should be ignored", func() { - err3 := SaveAlerts(&modifiedCmd) - So(err3, ShouldBeNil) - }) + require.Nil(t, err2) + require.Equal(t, 3, len(queryForDashboard.Result)) }) - Convey("Multiple alerts per dashboard", func() { - multipleItems := []*models.Alert{ - { - DashboardId: testDash.Id, - PanelId: 1, - Name: "1", - OrgId: 1, - Settings: simplejson.New(), - }, - { - DashboardId: testDash.Id, - PanelId: 2, - Name: "2", - OrgId: 1, - Settings: simplejson.New(), - }, - { - DashboardId: testDash.Id, - PanelId: 3, - Name: "3", - OrgId: 1, - Settings: simplejson.New(), - }, - } + t.Run("should updated two dashboards and delete one", func(t *testing.T) { + missingOneAlert := multipleItems[:2] - cmd.Alerts = multipleItems + cmd.Alerts = missingOneAlert err = SaveAlerts(&cmd) - Convey("Should save 3 dashboards", func() { - So(err, ShouldBeNil) - - queryForDashboard := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} - err2 := HandleAlertsQuery(&queryForDashboard) - - So(err2, ShouldBeNil) - So(len(queryForDashboard.Result), ShouldEqual, 3) - }) - - Convey("should updated two dashboards and delete one", func() { - missingOneAlert := multipleItems[:2] - - cmd.Alerts = missingOneAlert - err = SaveAlerts(&cmd) - - Convey("should delete the missing alert", func() { - query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} - err2 := HandleAlertsQuery(&query) - So(err2, ShouldBeNil) - So(len(query.Result), ShouldEqual, 2) - }) - }) - }) - - Convey("When dashboard is removed", func() { - items := []*models.Alert{ - { - PanelId: 1, - DashboardId: testDash.Id, - Name: "Alerting title", - Message: "Alerting message", - }, - } - - cmd := models.SaveAlertsCommand{ - Alerts: items, - DashboardId: testDash.Id, - OrgId: 1, - UserId: 1, - } - - err = SaveAlerts(&cmd) - So(err, ShouldBeNil) - - err = DeleteDashboard(context.Background(), &models.DeleteDashboardCommand{ - OrgId: 1, - Id: testDash.Id, - }) - So(err, ShouldBeNil) - - Convey("Alerts should be removed", func() { + t.Run("should delete the missing alert", func(t *testing.T) { query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} err2 := HandleAlertsQuery(&query) - - So(err2, ShouldBeNil) - So(len(query.Result), ShouldEqual, 0) + require.Nil(t, err2) + require.Equal(t, 2, len(query.Result)) }) }) }) + + t.Run("When dashboard is removed", func(t *testing.T) { + setup(t) + items := []*models.Alert{ + { + PanelId: 1, + DashboardId: testDash.Id, + Name: "Alerting title", + Message: "Alerting message", + }, + } + + cmd := models.SaveAlertsCommand{ + Alerts: items, + DashboardId: testDash.Id, + OrgId: 1, + UserId: 1, + } + + err := SaveAlerts(&cmd) + require.Nil(t, err) + + err = DeleteDashboard(context.Background(), &models.DeleteDashboardCommand{ + OrgId: 1, + Id: testDash.Id, + }) + require.Nil(t, err) + + t.Run("Alerts should be removed", func(t *testing.T) { + query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} + err2 := HandleAlertsQuery(&query) + + require.Nil(t, err2) + require.Equal(t, 0, len(query.Result)) + }) + }) } func TestPausingAlerts(t *testing.T) { mockTimeNow() defer resetTimeNow() - Convey("Given an alert", t, func() { + t.Run("Given an alert", func(t *testing.T) { sqlStore := InitTestDB(t) testDash := insertTestDashboard(t, sqlStore, "dashboard with alerts", 1, 0, false, "alert") alert, err := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New()) - So(err, ShouldBeNil) + require.Nil(t, err) stateDateBeforePause := alert.NewStateDate stateDateAfterPause := stateDateBeforePause @@ -293,45 +302,45 @@ func TestPausingAlerts(t *testing.T) { // Get alert so we can use its ID in tests alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}} err2 := HandleAlertsQuery(&alertQuery) - So(err2, ShouldBeNil) + require.Nil(t, err2) insertedAlert := alertQuery.Result[0] - Convey("when paused", func() { - _, err := pauseAlert(testDash.OrgId, insertedAlert.Id, true) - So(err, ShouldBeNil) + t.Run("when paused", func(t *testing.T) { + _, err := pauseAlert(t, testDash.OrgId, insertedAlert.Id, true) + require.Nil(t, err) - Convey("the NewStateDate should be updated", func() { - alert, err := getAlertById(insertedAlert.Id) - So(err, ShouldBeNil) + t.Run("the NewStateDate should be updated", func(t *testing.T) { + alert, err := getAlertById(t, insertedAlert.Id) + require.Nil(t, err) stateDateAfterPause = alert.NewStateDate - So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause) + require.True(t, stateDateBeforePause.Before(stateDateAfterPause)) }) }) - Convey("when unpaused", func() { - _, err := pauseAlert(testDash.OrgId, insertedAlert.Id, false) - So(err, ShouldBeNil) + t.Run("when unpaused", func(t *testing.T) { + _, err := pauseAlert(t, testDash.OrgId, insertedAlert.Id, false) + require.Nil(t, err) - Convey("the NewStateDate should be updated again", func() { - alert, err := getAlertById(insertedAlert.Id) - So(err, ShouldBeNil) + t.Run("the NewStateDate should be updated again", func(t *testing.T) { + alert, err := getAlertById(t, insertedAlert.Id) + require.Nil(t, err) stateDateAfterUnpause := alert.NewStateDate - So(stateDateAfterPause, ShouldHappenBefore, stateDateAfterUnpause) + require.True(t, stateDateAfterPause.Before(stateDateAfterUnpause)) }) }) }) } -func pauseAlert(orgId int64, alertId int64, pauseState bool) (int64, error) { +func pauseAlert(t *testing.T, orgId int64, alertId int64, pauseState bool) (int64, error) { cmd := &models.PauseAlertCommand{ OrgId: orgId, AlertIds: []int64{alertId}, Paused: pauseState, } err := PauseAlert(cmd) - So(err, ShouldBeNil) + require.Nil(t, err) return cmd.ResultCount, err } func insertTestAlert(title string, message string, orgId int64, dashId int64, settings *simplejson.Json) (*models.Alert, error) { @@ -358,20 +367,20 @@ func insertTestAlert(title string, message string, orgId int64, dashId int64, se return cmd.Alerts[0], err } -func getAlertById(id int64) (*models.Alert, error) { +func getAlertById(t *testing.T, id int64) (*models.Alert, error) { q := &models.GetAlertByIdQuery{ Id: id, } err := GetAlertById(q) - So(err, ShouldBeNil) + require.Nil(t, err) return q.Result, err } -func pauseAllAlerts(pauseState bool) error { +func pauseAllAlerts(t *testing.T, pauseState bool) error { cmd := &models.PauseAllAlertCommand{ Paused: pauseState, } err := PauseAllAlerts(cmd) - So(err, ShouldBeNil) + require.Nil(t, err) return err } diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index fa578ae1302..04dab446b16 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -76,14 +76,16 @@ func AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand) error { } else if cmd.SecondsToLive < 0 { return models.ErrInvalidApiKeyExpiration } + t := models.ApiKey{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Role: cmd.Role, - Key: cmd.Key, - Created: updated, - Updated: updated, - Expires: expires, + OrgId: cmd.OrgId, + Name: cmd.Name, + Role: cmd.Role, + Key: cmd.Key, + Created: updated, + Updated: updated, + Expires: expires, + ServiceAccountId: cmd.ServiceAccountId, } if _, err := sess.Insert(&t); err != nil { diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index 24d6aea954d..2f346221b0e 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -8,236 +8,241 @@ import ( "testing" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + + "github.com/stretchr/testify/require" ) func TestDashboardAclDataAccess(t *testing.T) { - Convey("Testing DB", t, func() { - sqlStore := InitTestDB(t) + var sqlStore *SQLStore + var currentUser models.User + var savedFolder, childDash *models.Dashboard - Convey("Given a dashboard folder and a user", func() { - currentUser := createUser(t, sqlStore, "viewer", "Viewer", false) - savedFolder := insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - childDash := insertTestDashboard(t, sqlStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") + setup := func(t *testing.T) { + sqlStore = InitTestDB(t) + currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) + savedFolder = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp") + childDash = insertTestDashboard(t, sqlStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") + } - Convey("When adding dashboard permission with userId and teamId set to 0", func() { - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ - OrgID: 1, - DashboardID: savedFolder.Id, - Permission: models.PERMISSION_EDIT, - }) - So(err, ShouldEqual, models.ErrDashboardAclInfoMissing) + t.Run("Dashboard permission with userId and teamId set to 0", func(t *testing.T) { + setup(t) + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + OrgID: 1, + DashboardID: savedFolder.Id, + Permission: models.PERMISSION_EDIT, + }) + require.Equal(t, models.ErrDashboardAclInfoMissing, err) + }) + + t.Run("Folder acl should include default acl", func(t *testing.T) { + setup(t) + query := models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + + err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 2, len(query.Result)) + defaultPermissionsId := int64(-1) + require.Equal(t, defaultPermissionsId, query.Result[0].DashboardId) + require.Equal(t, models.ROLE_VIEWER, *query.Result[0].Role) + require.False(t, query.Result[0].Inherited) + require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId) + require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role) + require.False(t, query.Result[1].Inherited) + }) + + t.Run("Dashboard acl should include acl for parent folder", func(t *testing.T) { + setup(t) + query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + + err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 2, len(query.Result)) + defaultPermissionsId := int64(-1) + require.Equal(t, defaultPermissionsId, query.Result[0].DashboardId) + require.Equal(t, models.ROLE_VIEWER, *query.Result[0].Role) + require.True(t, query.Result[0].Inherited) + require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId) + require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role) + require.True(t, query.Result[1].Inherited) + }) + + t.Run("Folder with removed default permissions returns no acl items", func(t *testing.T) { + setup(t) + err := sqlStore.UpdateDashboardACL(savedFolder.Id, nil) + require.Nil(t, err) + + query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + err = sqlStore.GetDashboardAclInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 0, len(query.Result)) + }) + + t.Run("Given a dashboard folder and a user", func(t *testing.T) { + + t.Run("Given dashboard folder permission", func(t *testing.T) { + setup(t) + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + OrgID: 1, + UserID: currentUser.Id, + DashboardID: savedFolder.Id, + Permission: models.PERMISSION_EDIT, + }) + require.Nil(t, err) + + t.Run("When reading dashboard acl should include acl for parent folder", func(t *testing.T) { + query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + + err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 1, len(query.Result)) + require.Equal(t, savedFolder.Id, query.Result[0].DashboardId) }) - Convey("Given dashboard folder with default permissions", func() { - Convey("When reading folder acl should include default acl", func() { - query := models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) - So(err, ShouldBeNil) - - So(len(query.Result), ShouldEqual, 2) - defaultPermissionsId := -1 - So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) - So(*query.Result[0].Role, ShouldEqual, models.ROLE_VIEWER) - So(query.Result[0].Inherited, ShouldBeFalse) - So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) - So(*query.Result[1].Role, ShouldEqual, models.ROLE_EDITOR) - So(query.Result[1].Inherited, ShouldBeFalse) - }) - - Convey("When reading dashboard acl should include acl for parent folder", func() { - query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} - - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) - So(err, ShouldBeNil) - - So(len(query.Result), ShouldEqual, 2) - defaultPermissionsId := -1 - So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) - So(*query.Result[0].Role, ShouldEqual, models.ROLE_VIEWER) - So(query.Result[0].Inherited, ShouldBeTrue) - So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) - So(*query.Result[1].Role, ShouldEqual, models.ROLE_EDITOR) - So(query.Result[1].Inherited, ShouldBeTrue) - }) - }) - - Convey("Given dashboard folder with removed default permissions", func() { - err := sqlStore.UpdateDashboardACL(savedFolder.Id, nil) - So(err, ShouldBeNil) - - Convey("When reading dashboard acl should return no acl items", func() { - query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} - - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) - So(err, ShouldBeNil) - - So(len(query.Result), ShouldEqual, 0) - }) - }) - - Convey("Given dashboard folder permission", func() { - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ - OrgID: 1, - UserID: currentUser.Id, - DashboardID: savedFolder.Id, - Permission: models.PERMISSION_EDIT, - }) - So(err, ShouldBeNil) - - Convey("When reading dashboard acl should include acl for parent folder", func() { - query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} - - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) - So(err, ShouldBeNil) - - So(len(query.Result), ShouldEqual, 1) - So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id) - }) - - Convey("Given child dashboard permission", func() { - err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ - OrgID: 1, - UserID: currentUser.Id, - DashboardID: childDash.Id, - Permission: models.PERMISSION_EDIT, - }) - So(err, ShouldBeNil) - - Convey("When reading dashboard acl should include acl for parent folder and child", func() { - query := models.GetDashboardAclInfoListQuery{OrgID: 1, DashboardID: childDash.Id} - - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) - So(err, ShouldBeNil) - - So(len(query.Result), ShouldEqual, 2) - So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id) - So(query.Result[0].Inherited, ShouldBeTrue) - So(query.Result[1].DashboardId, ShouldEqual, childDash.Id) - So(query.Result[1].Inherited, ShouldBeFalse) - }) - }) - }) - - Convey("Given child dashboard permission in folder with no permissions", func() { + t.Run("Given child dashboard permission", func(t *testing.T) { err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: childDash.Id, Permission: models.PERMISSION_EDIT, }) - So(err, ShouldBeNil) + require.Nil(t, err) - Convey("When reading dashboard acl should include default acl for parent folder and the child acl", func() { + t.Run("When reading dashboard acl should include acl for parent folder and child", func(t *testing.T) { query := models.GetDashboardAclInfoListQuery{OrgID: 1, DashboardID: childDash.Id} err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) - So(err, ShouldBeNil) + require.Nil(t, err) - defaultPermissionsId := -1 - So(len(query.Result), ShouldEqual, 3) - So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) - So(*query.Result[0].Role, ShouldEqual, models.ROLE_VIEWER) - So(query.Result[0].Inherited, ShouldBeTrue) - So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) - So(*query.Result[1].Role, ShouldEqual, models.ROLE_EDITOR) - So(query.Result[1].Inherited, ShouldBeTrue) - So(query.Result[2].DashboardId, ShouldEqual, childDash.Id) - So(query.Result[2].Inherited, ShouldBeFalse) - }) - }) - - Convey("Should be able to add dashboard permission", func() { - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ - OrgID: 1, - UserID: currentUser.Id, - DashboardID: savedFolder.Id, - Permission: models.PERMISSION_EDIT, - }) - So(err, ShouldBeNil) - - q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), q1) - So(err, ShouldBeNil) - - So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) - So(q1.Result[0].Permission, ShouldEqual, models.PERMISSION_EDIT) - So(q1.Result[0].PermissionName, ShouldEqual, "Edit") - So(q1.Result[0].UserId, ShouldEqual, currentUser.Id) - So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login) - So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email) - - Convey("Should be able to delete an existing permission", func() { - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id) - So(err, ShouldBeNil) - - q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), q3) - So(err, ShouldBeNil) - So(len(q3.Result), ShouldEqual, 0) - }) - }) - - Convey("Given a team", func() { - team1, err := sqlStore.CreateTeam("group1 name", "", 1) - So(err, ShouldBeNil) - - Convey("Should be able to add a user permission for a team", func() { - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ - OrgID: 1, - TeamID: team1.Id, - DashboardID: savedFolder.Id, - Permission: models.PERMISSION_EDIT, - }) - So(err, ShouldBeNil) - - q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), q1) - So(err, ShouldBeNil) - So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) - So(q1.Result[0].Permission, ShouldEqual, models.PERMISSION_EDIT) - So(q1.Result[0].TeamId, ShouldEqual, team1.Id) - }) - - Convey("Should be able to update an existing permission for a team", func() { - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ - OrgID: 1, - TeamID: team1.Id, - DashboardID: savedFolder.Id, - Permission: models.PERMISSION_ADMIN, - }) - So(err, ShouldBeNil) - - q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), q3) - So(err, ShouldBeNil) - So(len(q3.Result), ShouldEqual, 1) - So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id) - So(q3.Result[0].Permission, ShouldEqual, models.PERMISSION_ADMIN) - So(q3.Result[0].TeamId, ShouldEqual, team1.Id) + require.Equal(t, 2, len(query.Result)) + require.Equal(t, savedFolder.Id, query.Result[0].DashboardId) + require.True(t, query.Result[0].Inherited) + require.Equal(t, childDash.Id, query.Result[1].DashboardId) + require.False(t, query.Result[1].Inherited) }) }) }) - Convey("Given a root folder", func() { - var rootFolderId int64 = 0 - - Convey("When reading dashboard acl should return default permissions", func() { - query := models.GetDashboardAclInfoListQuery{DashboardID: rootFolderId, OrgID: 1} - - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) - So(err, ShouldBeNil) - - So(len(query.Result), ShouldEqual, 2) - defaultPermissionsId := -1 - So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) - So(*query.Result[0].Role, ShouldEqual, models.ROLE_VIEWER) - So(query.Result[0].Inherited, ShouldBeFalse) - So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) - So(*query.Result[1].Role, ShouldEqual, models.ROLE_EDITOR) - So(query.Result[1].Inherited, ShouldBeFalse) + t.Run("Reading dashboard acl should include default acl for parent folder and the child acl", func(t *testing.T) { + setup(t) + err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ + OrgID: 1, + UserID: currentUser.Id, + DashboardID: childDash.Id, + Permission: models.PERMISSION_EDIT, }) + require.Nil(t, err) + + query := models.GetDashboardAclInfoListQuery{OrgID: 1, DashboardID: childDash.Id} + + err = sqlStore.GetDashboardAclInfoList(context.Background(), &query) + require.Nil(t, err) + + defaultPermissionsId := int64(-1) + require.Equal(t, 3, len(query.Result)) + require.Equal(t, defaultPermissionsId, query.Result[0].DashboardId) + require.Equal(t, models.ROLE_VIEWER, *query.Result[0].Role) + require.True(t, query.Result[0].Inherited) + require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId) + require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role) + require.True(t, query.Result[1].Inherited) + require.Equal(t, childDash.Id, query.Result[2].DashboardId) + require.False(t, query.Result[2].Inherited) + }) + + t.Run("Add and delete dashboard permission", func(t *testing.T) { + setup(t) + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + OrgID: 1, + UserID: currentUser.Id, + DashboardID: savedFolder.Id, + Permission: models.PERMISSION_EDIT, + }) + require.Nil(t, err) + + q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + err = sqlStore.GetDashboardAclInfoList(context.Background(), q1) + require.Nil(t, err) + + require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId) + require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission) + require.Equal(t, "Edit", q1.Result[0].PermissionName) + require.Equal(t, currentUser.Id, q1.Result[0].UserId) + require.Equal(t, currentUser.Login, q1.Result[0].UserLogin) + require.Equal(t, currentUser.Email, q1.Result[0].UserEmail) + + err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id) + require.Nil(t, err) + + q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + err = sqlStore.GetDashboardAclInfoList(context.Background(), q3) + require.Nil(t, err) + require.Equal(t, 0, len(q3.Result)) + }) + + t.Run("Should be able to add a user permission for a team", func(t *testing.T) { + setup(t) + team1, err := sqlStore.CreateTeam("group1 name", "", 1) + require.Nil(t, err) + + err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + OrgID: 1, + TeamID: team1.Id, + DashboardID: savedFolder.Id, + Permission: models.PERMISSION_EDIT, + }) + require.Nil(t, err) + + q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + err = sqlStore.GetDashboardAclInfoList(context.Background(), q1) + require.Nil(t, err) + require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId) + require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission) + require.Equal(t, team1.Id, q1.Result[0].TeamId) + }) + + t.Run("Should be able to update an existing permission for a team", func(t *testing.T) { + setup(t) + team1, err := sqlStore.CreateTeam("group1 name", "", 1) + require.Nil(t, err) + err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + OrgID: 1, + TeamID: team1.Id, + DashboardID: savedFolder.Id, + Permission: models.PERMISSION_ADMIN, + }) + require.Nil(t, err) + + q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + err = sqlStore.GetDashboardAclInfoList(context.Background(), q3) + require.Nil(t, err) + require.Equal(t, 1, len(q3.Result)) + require.Equal(t, savedFolder.Id, q3.Result[0].DashboardId) + require.Equal(t, models.PERMISSION_ADMIN, q3.Result[0].Permission) + require.Equal(t, team1.Id, q3.Result[0].TeamId) }) }) + + t.Run("Default permissions for root folder dashboards", func(t *testing.T) { + setup(t) + var rootFolderId int64 = 0 + sqlStore := InitTestDB(t) + + query := models.GetDashboardAclInfoListQuery{DashboardID: rootFolderId, OrgID: 1} + + err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 2, len(query.Result)) + defaultPermissionsId := int64(-1) + require.Equal(t, defaultPermissionsId, query.Result[0].DashboardId) + require.Equal(t, models.ROLE_VIEWER, *query.Result[0].Role) + require.False(t, query.Result[0].Inherited) + require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId) + require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role) + require.False(t, query.Result[1].Inherited) + }) } diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 40731d4dc56..b58b62e573b 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -10,132 +10,130 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + + "github.com/stretchr/testify/require" ) func TestDashboardProvisioningTest(t *testing.T) { - Convey("Testing Dashboard provisioning", t, func() { - sqlStore := InitTestDB(t) + sqlStore := InitTestDB(t) - folderCmd := models.SaveDashboardCommand{ - OrgId: 1, - FolderId: 0, - IsFolder: true, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": "test dashboard", - }), + folderCmd := models.SaveDashboardCommand{ + OrgId: 1, + FolderId: 0, + IsFolder: true, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test dashboard", + }), + } + + dash, err := sqlStore.SaveDashboard(folderCmd) + require.Nil(t, err) + + saveDashboardCmd := models.SaveDashboardCommand{ + OrgId: 1, + IsFolder: false, + FolderId: dash.Id, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test dashboard", + }), + } + + t.Run("Saving dashboards with provisioning meta data", func(t *testing.T) { + now := time.Now() + + provisioning := &models.DashboardProvisioning{ + Name: "default", + ExternalId: "/var/grafana.json", + Updated: now.Unix(), } - dash, err := sqlStore.SaveDashboard(folderCmd) - So(err, ShouldBeNil) - - saveDashboardCmd := models.SaveDashboardCommand{ - OrgId: 1, - IsFolder: false, - FolderId: dash.Id, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": "test dashboard", - }), - } - - Convey("Saving dashboards with provisioning meta data", func() { - now := time.Now() + dash, err := sqlStore.SaveProvisionedDashboard(saveDashboardCmd, provisioning) + require.Nil(t, err) + require.NotNil(t, dash) + require.NotEqual(t, 0, dash.Id) + dashId := dash.Id + t.Run("Deleting orphaned provisioned dashboards", func(t *testing.T) { + saveCmd := models.SaveDashboardCommand{ + OrgId: 1, + IsFolder: false, + FolderId: dash.Id, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "another_dashboard", + }), + } provisioning := &models.DashboardProvisioning{ - Name: "default", + Name: "another_reader", ExternalId: "/var/grafana.json", Updated: now.Unix(), } - dash, err := sqlStore.SaveProvisionedDashboard(saveDashboardCmd, provisioning) - So(err, ShouldBeNil) - So(dash, ShouldNotBeNil) - So(dash.Id, ShouldNotEqual, 0) - dashId := dash.Id + anotherDash, err := sqlStore.SaveProvisionedDashboard(saveCmd, provisioning) + require.Nil(t, err) - Convey("Deleting orphaned provisioned dashboards", func() { - saveCmd := models.SaveDashboardCommand{ - OrgId: 1, - IsFolder: false, - FolderId: dash.Id, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": "another_dashboard", - }), - } - provisioning := &models.DashboardProvisioning{ - Name: "another_reader", - ExternalId: "/var/grafana.json", - Updated: now.Unix(), - } + query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherDash.Id}} + err = GetDashboards(context.Background(), query) + require.Nil(t, err) + require.NotNil(t, query.Result) - anotherDash, err := sqlStore.SaveProvisionedDashboard(saveCmd, provisioning) - So(err, ShouldBeNil) + deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}} + require.Nil(t, DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd)) - query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherDash.Id}} - err = GetDashboards(context.Background(), query) - So(err, ShouldBeNil) - So(query.Result, ShouldNotBeNil) + query = &models.GetDashboardsQuery{DashboardIds: []int64{dash.Id, anotherDash.Id}} + err = GetDashboards(context.Background(), query) + require.Nil(t, err) - deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}} - So(DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd), ShouldBeNil) + require.Equal(t, 1, len(query.Result)) + require.Equal(t, dashId, query.Result[0].Id) + }) - query = &models.GetDashboardsQuery{DashboardIds: []int64{dash.Id, anotherDash.Id}} - err = GetDashboards(context.Background(), query) - So(err, ShouldBeNil) + t.Run("Can query for provisioned dashboards", func(t *testing.T) { + rslt, err := sqlStore.GetProvisionedDashboardData("default") + require.Nil(t, err) - So(len(query.Result), ShouldEqual, 1) - So(query.Result[0].Id, ShouldEqual, dashId) - }) + require.Equal(t, 1, len(rslt)) + require.Equal(t, dashId, rslt[0].DashboardId) + require.Equal(t, now.Unix(), rslt[0].Updated) + }) - Convey("Can query for provisioned dashboards", func() { - rslt, err := sqlStore.GetProvisionedDashboardData("default") - So(err, ShouldBeNil) + t.Run("Can query for one provisioned dashboard", func(t *testing.T) { + data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id) + require.Nil(t, err) + require.NotNil(t, data) + }) - So(len(rslt), ShouldEqual, 1) - So(rslt[0].DashboardId, ShouldEqual, dashId) - So(rslt[0].Updated, ShouldEqual, now.Unix()) - }) + t.Run("Can query for none provisioned dashboard", func(t *testing.T) { + data, err := sqlStore.GetProvisionedDataByDashboardID(3000) + require.Nil(t, err) + require.Nil(t, data) + }) - Convey("Can query for one provisioned dashboard", func() { - data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id) - So(err, ShouldBeNil) + t.Run("Deleting folder should delete provision meta data", func(t *testing.T) { + deleteCmd := &models.DeleteDashboardCommand{ + Id: dash.Id, + OrgId: 1, + } - So(data, ShouldNotBeNil) - }) + require.Nil(t, DeleteDashboard(context.Background(), deleteCmd)) - Convey("Can query for none provisioned dashboard", func() { - data, err := sqlStore.GetProvisionedDataByDashboardID(3000) - So(err, ShouldBeNil) - So(data, ShouldBeNil) - }) + data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id) + require.Nil(t, err) + require.Nil(t, data) + }) - Convey("Deleting folder should delete provision meta data", func() { - deleteCmd := &models.DeleteDashboardCommand{ - Id: dash.Id, - OrgId: 1, - } + t.Run("UnprovisionDashboard should delete provisioning metadata", func(t *testing.T) { + unprovisionCmd := &models.UnprovisionDashboardCommand{ + Id: dashId, + } - So(DeleteDashboard(context.Background(), deleteCmd), ShouldBeNil) + require.Nil(t, UnprovisionDashboard(context.Background(), unprovisionCmd)) - data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id) - So(err, ShouldBeNil) - So(data, ShouldBeNil) - }) - - Convey("UnprovisionDashboard should delete provisioning metadata", func() { - unprovisionCmd := &models.UnprovisionDashboardCommand{ - Id: dashId, - } - - So(UnprovisionDashboard(context.Background(), unprovisionCmd), ShouldBeNil) - - data, err := sqlStore.GetProvisionedDataByDashboardID(dashId) - So(err, ShouldBeNil) - So(data, ShouldBeNil) - }) + data, err := sqlStore.GetProvisionedDataByDashboardID(dashId) + require.Nil(t, err) + require.Nil(t, data) }) }) } diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index d44618fd735..949dc26414c 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -8,11 +8,10 @@ import ( "reflect" "testing" - . "github.com/smartystreets/goconvey/convey" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" ) func updateTestDashboard(t *testing.T, sqlStore *SQLStore, dashboard *models.Dashboard, data map[string]interface{}) { @@ -26,151 +25,152 @@ func updateTestDashboard(t *testing.T, sqlStore *SQLStore, dashboard *models.Das Dashboard: simplejson.NewFromAny(data), } _, err := sqlStore.SaveDashboard(saveCmd) - So(err, ShouldBeNil) + require.Nil(t, err) } func TestGetDashboardVersion(t *testing.T) { - Convey("Testing dashboard version retrieval", t, func() { - sqlStore := InitTestDB(t) + sqlStore := InitTestDB(t) - Convey("Get a Dashboard ID and version ID", func() { - savedDash := insertTestDashboard(t, sqlStore, "test dash 26", 1, 0, false, "diff") + t.Run("Get a Dashboard ID and version ID", func(t *testing.T) { + savedDash := insertTestDashboard(t, sqlStore, "test dash 26", 1, 0, false, "diff") - query := models.GetDashboardVersionQuery{ - DashboardId: savedDash.Id, - Version: savedDash.Version, - OrgId: 1, - } + query := models.GetDashboardVersionQuery{ + DashboardId: savedDash.Id, + Version: savedDash.Version, + OrgId: 1, + } - err := GetDashboardVersion(&query) - So(err, ShouldBeNil) - So(savedDash.Id, ShouldEqual, query.DashboardId) - So(savedDash.Version, ShouldEqual, query.Version) + err := GetDashboardVersion(&query) + require.Nil(t, err) + require.Equal(t, query.DashboardId, savedDash.Id) + require.Equal(t, query.Version, savedDash.Version) - dashCmd := models.GetDashboardQuery{ - OrgId: savedDash.OrgId, - Uid: savedDash.Uid, - } + dashCmd := models.GetDashboardQuery{ + OrgId: savedDash.OrgId, + Uid: savedDash.Uid, + } - err = GetDashboard(context.Background(), &dashCmd) - So(err, ShouldBeNil) - eq := reflect.DeepEqual(dashCmd.Result.Data, query.Result.Data) - So(eq, ShouldEqual, true) - }) + err = GetDashboard(context.Background(), &dashCmd) + require.Nil(t, err) + eq := reflect.DeepEqual(dashCmd.Result.Data, query.Result.Data) + require.Equal(t, true, eq) + }) - Convey("Attempt to get a version that doesn't exist", func() { - query := models.GetDashboardVersionQuery{ - DashboardId: int64(999), - Version: 123, - OrgId: 1, - } + t.Run("Attempt to get a version that doesn't exist", func(t *testing.T) { + query := models.GetDashboardVersionQuery{ + DashboardId: int64(999), + Version: 123, + OrgId: 1, + } - err := GetDashboardVersion(&query) - So(err, ShouldNotBeNil) - So(err, ShouldEqual, models.ErrDashboardVersionNotFound) - }) + err := GetDashboardVersion(&query) + require.Error(t, err) + require.Equal(t, models.ErrDashboardVersionNotFound, err) }) } func TestGetDashboardVersions(t *testing.T) { - Convey("Testing dashboard versions retrieval", t, func() { - sqlStore := InitTestDB(t) - savedDash := insertTestDashboard(t, sqlStore, "test dash 43", 1, 0, false, "diff-all") + sqlStore := InitTestDB(t) + savedDash := insertTestDashboard(t, sqlStore, "test dash 43", 1, 0, false, "diff-all") - Convey("Get all versions for a given Dashboard ID", func() { - query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} + t.Run("Get all versions for a given Dashboard ID", func(t *testing.T) { + query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} - err := GetDashboardVersions(&query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 1) + err := GetDashboardVersions(&query) + require.Nil(t, err) + require.Equal(t, 1, len(query.Result)) + }) + + t.Run("Attempt to get the versions for a non-existent Dashboard ID", func(t *testing.T) { + query := models.GetDashboardVersionsQuery{DashboardId: int64(999), OrgId: 1} + + err := GetDashboardVersions(&query) + require.Error(t, err) + require.Equal(t, models.ErrNoVersionsForDashboardId, err) + require.Equal(t, 0, len(query.Result)) + }) + + t.Run("Get all versions for an updated dashboard", func(t *testing.T) { + updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ + "tags": "different-tag", }) - Convey("Attempt to get the versions for a non-existent Dashboard ID", func() { - query := models.GetDashboardVersionsQuery{DashboardId: int64(999), OrgId: 1} + query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} + err := GetDashboardVersions(&query) - err := GetDashboardVersions(&query) - So(err, ShouldNotBeNil) - So(err, ShouldEqual, models.ErrNoVersionsForDashboardId) - So(len(query.Result), ShouldEqual, 0) - }) - - Convey("Get all versions for an updated dashboard", func() { - updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ - "tags": "different-tag", - }) - - query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} - err := GetDashboardVersions(&query) - - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 2) - }) + require.Nil(t, err) + require.Equal(t, 2, len(query.Result)) }) } func TestDeleteExpiredVersions(t *testing.T) { - Convey("Testing dashboard versions clean up", t, func() { - sqlStore := InitTestDB(t) - versionsToKeep := 5 - versionsToWrite := 10 - setting.DashboardVersionsToKeep = versionsToKeep + versionsToKeep := 5 + versionsToWrite := 10 + setting.DashboardVersionsToKeep = versionsToKeep - savedDash := insertTestDashboard(t, sqlStore, "test dash 53", 1, 0, false, "diff-all") + var sqlStore *SQLStore + var savedDash *models.Dashboard + setup := func(t *testing.T) { + sqlStore = InitTestDB(t) + savedDash = insertTestDashboard(t, sqlStore, "test dash 53", 1, 0, false, "diff-all") for i := 0; i < versionsToWrite-1; i++ { updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ "tags": "different-tag", }) } + } - Convey("Clean up old dashboard versions", func() { - err := DeleteExpiredVersions(&models.DeleteExpiredVersionsCommand{}) - So(err, ShouldBeNil) + t.Run("Clean up old dashboard versions", func(t *testing.T) { + setup(t) + err := DeleteExpiredVersions(&models.DeleteExpiredVersionsCommand{}) + require.Nil(t, err) - query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} - err = GetDashboardVersions(&query) - So(err, ShouldBeNil) + query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} + err = GetDashboardVersions(&query) + require.Nil(t, err) - So(len(query.Result), ShouldEqual, versionsToKeep) - // Ensure latest versions were kept - So(query.Result[versionsToKeep-1].Version, ShouldEqual, versionsToWrite-versionsToKeep+1) - So(query.Result[0].Version, ShouldEqual, versionsToWrite) - }) + require.Equal(t, versionsToKeep, len(query.Result)) + // Ensure latest versions were kept + require.Equal(t, versionsToWrite-versionsToKeep+1, query.Result[versionsToKeep-1].Version) + require.Equal(t, versionsToWrite, query.Result[0].Version) + }) - Convey("Don't delete anything if there are no expired versions", func() { - setting.DashboardVersionsToKeep = versionsToWrite + t.Run("Don't delete anything if there are no expired versions", func(t *testing.T) { + setup(t) + setting.DashboardVersionsToKeep = versionsToWrite - err := DeleteExpiredVersions(&models.DeleteExpiredVersionsCommand{}) - So(err, ShouldBeNil) + err := DeleteExpiredVersions(&models.DeleteExpiredVersionsCommand{}) + require.Nil(t, err) - query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWrite} - err = GetDashboardVersions(&query) - So(err, ShouldBeNil) + query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWrite} + err = GetDashboardVersions(&query) + require.Nil(t, err) - So(len(query.Result), ShouldEqual, versionsToWrite) - }) + require.Equal(t, versionsToWrite, len(query.Result)) + }) - Convey("Don't delete more than MAX_VERSIONS_TO_DELETE_PER_BATCH * MAX_VERSION_DELETION_BATCHES per iteration", func() { - perBatch := 10 - maxBatches := 10 + t.Run("Don't delete more than MAX_VERSIONS_TO_DELETE_PER_BATCH * MAX_VERSION_DELETION_BATCHES per iteration", func(t *testing.T) { + setup(t) + perBatch := 10 + maxBatches := 10 - versionsToWriteBigNumber := perBatch*maxBatches + versionsToWrite - for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ { - updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ - "tags": "different-tag", - }) - } + versionsToWriteBigNumber := perBatch*maxBatches + versionsToWrite + for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ { + updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ + "tags": "different-tag", + }) + } - err := deleteExpiredVersions(&models.DeleteExpiredVersionsCommand{}, perBatch, maxBatches) - So(err, ShouldBeNil) + err := deleteExpiredVersions(&models.DeleteExpiredVersionsCommand{}, perBatch, maxBatches) + require.Nil(t, err) - query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWriteBigNumber} - err = GetDashboardVersions(&query) - So(err, ShouldBeNil) + query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWriteBigNumber} + err = GetDashboardVersions(&query) + require.Nil(t, err) - // Ensure we have at least versionsToKeep versions - So(len(query.Result), ShouldBeGreaterThanOrEqualTo, versionsToKeep) - // Ensure we haven't deleted more than perBatch * maxBatches rows - So(versionsToWriteBigNumber-len(query.Result), ShouldBeLessThanOrEqualTo, perBatch*maxBatches) - }) + // Ensure we have at least versionsToKeep versions + require.GreaterOrEqual(t, len(query.Result), versionsToKeep) + // Ensure we haven't deleted more than perBatch * maxBatches rows + require.LessOrEqual(t, versionsToWriteBigNumber-len(query.Result), perBatch*maxBatches) }) } diff --git a/pkg/services/sqlstore/login_attempt_test.go b/pkg/services/sqlstore/login_attempt_test.go index 90db30672ed..42ec4b40caa 100644 --- a/pkg/services/sqlstore/login_attempt_test.go +++ b/pkg/services/sqlstore/login_attempt_test.go @@ -8,7 +8,8 @@ import ( "time" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + + "github.com/stretchr/testify/require" ) func mockTime(mock time.Time) time.Time { @@ -17,112 +18,116 @@ func mockTime(mock time.Time) time.Time { } func TestLoginAttempts(t *testing.T) { - Convey("Testing Login Attempts DB Access", t, func() { + var beginningOfTime, timePlusOneMinute, timePlusTwoMinutes time.Time + user := "user" + + setup := func(t *testing.T) { InitTestDB(t) - - user := "user" - beginningOfTime := mockTime(time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local)) - + beginningOfTime = mockTime(time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local)) err := CreateLoginAttempt(&models.CreateLoginAttemptCommand{ Username: user, IpAddress: "192.168.0.1", }) - So(err, ShouldBeNil) - - timePlusOneMinute := mockTime(beginningOfTime.Add(time.Minute * 1)) - + require.Nil(t, err) + timePlusOneMinute = mockTime(beginningOfTime.Add(time.Minute * 1)) err = CreateLoginAttempt(&models.CreateLoginAttemptCommand{ Username: user, IpAddress: "192.168.0.1", }) - So(err, ShouldBeNil) - - timePlusTwoMinutes := mockTime(beginningOfTime.Add(time.Minute * 2)) - + require.Nil(t, err) + timePlusTwoMinutes = mockTime(beginningOfTime.Add(time.Minute * 2)) err = CreateLoginAttempt(&models.CreateLoginAttemptCommand{ Username: user, IpAddress: "192.168.0.1", }) - So(err, ShouldBeNil) + require.Nil(t, err) + } - Convey("Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s", func() { - query := models.GetUserLoginAttemptCountQuery{ - Username: user, - Since: timePlusTwoMinutes.Add(time.Second * 1), - } - err := GetUserLoginAttemptCount(&query) - So(err, ShouldBeNil) - So(query.Result, ShouldEqual, 0) - }) + t.Run("Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s", func(t *testing.T) { + setup(t) + query := models.GetUserLoginAttemptCountQuery{ + Username: user, + Since: timePlusTwoMinutes.Add(time.Second * 1), + } + err := GetUserLoginAttemptCount(&query) + require.Nil(t, err) + require.Equal(t, int64(0), query.Result) + }) - Convey("Should return the total count of login attempts since beginning of time", func() { - query := models.GetUserLoginAttemptCountQuery{ - Username: user, - Since: beginningOfTime, - } - err := GetUserLoginAttemptCount(&query) - So(err, ShouldBeNil) - So(query.Result, ShouldEqual, 3) - }) + t.Run("Should return the total count of login attempts since beginning of time", func(t *testing.T) { + setup(t) + query := models.GetUserLoginAttemptCountQuery{ + Username: user, + Since: beginningOfTime, + } + err := GetUserLoginAttemptCount(&query) + require.Nil(t, err) + require.Equal(t, int64(3), query.Result) + }) - Convey("Should return the total count of login attempts since beginning of time + 1min", func() { - query := models.GetUserLoginAttemptCountQuery{ - Username: user, - Since: timePlusOneMinute, - } - err := GetUserLoginAttemptCount(&query) - So(err, ShouldBeNil) - So(query.Result, ShouldEqual, 2) - }) + t.Run("Should return the total count of login attempts since beginning of time + 1min", func(t *testing.T) { + setup(t) + query := models.GetUserLoginAttemptCountQuery{ + Username: user, + Since: timePlusOneMinute, + } + err := GetUserLoginAttemptCount(&query) + require.Nil(t, err) + require.Equal(t, int64(2), query.Result) + }) - Convey("Should return the total count of login attempts since beginning of time + 2min", func() { - query := models.GetUserLoginAttemptCountQuery{ - Username: user, - Since: timePlusTwoMinutes, - } - err := GetUserLoginAttemptCount(&query) - So(err, ShouldBeNil) - So(query.Result, ShouldEqual, 1) - }) + t.Run("Should return the total count of login attempts since beginning of time + 2min", func(t *testing.T) { + setup(t) + query := models.GetUserLoginAttemptCountQuery{ + Username: user, + Since: timePlusTwoMinutes, + } + err := GetUserLoginAttemptCount(&query) + require.Nil(t, err) + require.Equal(t, int64(1), query.Result) + }) - Convey("Should return deleted rows older than beginning of time", func() { - cmd := models.DeleteOldLoginAttemptsCommand{ - OlderThan: beginningOfTime, - } - err := DeleteOldLoginAttempts(&cmd) + t.Run("Should return deleted rows older than beginning of time", func(t *testing.T) { + setup(t) + cmd := models.DeleteOldLoginAttemptsCommand{ + OlderThan: beginningOfTime, + } + err := DeleteOldLoginAttempts(&cmd) - So(err, ShouldBeNil) - So(cmd.DeletedRows, ShouldEqual, 0) - }) + require.Nil(t, err) + require.Equal(t, int64(0), cmd.DeletedRows) + }) - Convey("Should return deleted rows older than beginning of time + 1min", func() { - cmd := models.DeleteOldLoginAttemptsCommand{ - OlderThan: timePlusOneMinute, - } - err := DeleteOldLoginAttempts(&cmd) + t.Run("Should return deleted rows older than beginning of time + 1min", func(t *testing.T) { + setup(t) + cmd := models.DeleteOldLoginAttemptsCommand{ + OlderThan: timePlusOneMinute, + } + err := DeleteOldLoginAttempts(&cmd) - So(err, ShouldBeNil) - So(cmd.DeletedRows, ShouldEqual, 1) - }) + require.Nil(t, err) + require.Equal(t, int64(1), cmd.DeletedRows) + }) - Convey("Should return deleted rows older than beginning of time + 2min", func() { - cmd := models.DeleteOldLoginAttemptsCommand{ - OlderThan: timePlusTwoMinutes, - } - err := DeleteOldLoginAttempts(&cmd) + t.Run("Should return deleted rows older than beginning of time + 2min", func(t *testing.T) { + setup(t) + cmd := models.DeleteOldLoginAttemptsCommand{ + OlderThan: timePlusTwoMinutes, + } + err := DeleteOldLoginAttempts(&cmd) - So(err, ShouldBeNil) - So(cmd.DeletedRows, ShouldEqual, 2) - }) + require.Nil(t, err) + require.Equal(t, int64(2), cmd.DeletedRows) + }) - Convey("Should return deleted rows older than beginning of time + 2min and 1s", func() { - cmd := models.DeleteOldLoginAttemptsCommand{ - OlderThan: timePlusTwoMinutes.Add(time.Second * 1), - } - err := DeleteOldLoginAttempts(&cmd) + t.Run("Should return deleted rows older than beginning of time + 2min and 1s", func(t *testing.T) { + setup(t) + cmd := models.DeleteOldLoginAttemptsCommand{ + OlderThan: timePlusTwoMinutes.Add(time.Second * 1), + } + err := DeleteOldLoginAttempts(&cmd) - So(err, ShouldBeNil) - So(cmd.DeletedRows, ShouldEqual, 3) - }) + require.Nil(t, err) + require.Equal(t, int64(3), cmd.DeletedRows) }) } diff --git a/pkg/services/sqlstore/migrations/apikey_mig.go b/pkg/services/sqlstore/migrations/apikey_mig.go index bc3de8ef4c4..f68bae4c7ac 100644 --- a/pkg/services/sqlstore/migrations/apikey_mig.go +++ b/pkg/services/sqlstore/migrations/apikey_mig.go @@ -82,4 +82,8 @@ func addApiKeyMigrations(mg *Migrator) { mg.AddMigration("Add expires to api_key table", NewAddColumnMigration(apiKeyV2, &Column{ Name: "expires", Type: DB_BigInt, Nullable: true, })) + + mg.AddMigration("Add service account foreign key", NewAddColumnMigration(apiKeyV2, &Column{ + Name: "service_account_id", Type: DB_BigInt, Nullable: true, + })) } diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index c3ae7ed5212..83b7d1545ce 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -320,7 +320,6 @@ func TestAccountDataAccess(t *testing.T) { query := models.GetOrgUsersQuery{OrgId: ac1.OrgId} err = sqlStore.GetOrgUsers(context.Background(), &query) require.NoError(t, err) - fmt.Println(query.Result) // require.Equal(t, len(query.Result), 3) dash1 := insertTestDashboard(t, sqlStore, "1 test dash", ac1.OrgId, 0, false, "prod", "webapp") diff --git a/pkg/services/sqlstore/tags_test.go b/pkg/services/sqlstore/tags_test.go index aa11b9c8d4d..f1b986ded62 100644 --- a/pkg/services/sqlstore/tags_test.go +++ b/pkg/services/sqlstore/tags_test.go @@ -8,22 +8,21 @@ import ( "testing" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + + "github.com/stretchr/testify/require" ) func TestSavingTags(t *testing.T) { - Convey("Testing tags saving", t, func() { - InitTestDB(t) + InitTestDB(t) - tagPairs := []*models.Tag{ - {Key: "outage"}, - {Key: "type", Value: "outage"}, - {Key: "server", Value: "server-1"}, - {Key: "error"}, - } - tags, err := EnsureTagsExist(newSession(context.Background()), tagPairs) + tagPairs := []*models.Tag{ + {Key: "outage"}, + {Key: "type", Value: "outage"}, + {Key: "server", Value: "server-1"}, + {Key: "error"}, + } + tags, err := EnsureTagsExist(newSession(context.Background()), tagPairs) - So(err, ShouldBeNil) - So(len(tags), ShouldEqual, 4) - }) + require.Nil(t, err) + require.Equal(t, 4, len(tags)) } diff --git a/pkg/services/sqlstore/temp_user_test.go b/pkg/services/sqlstore/temp_user_test.go index 2c2b6b542fa..40b11524991 100644 --- a/pkg/services/sqlstore/temp_user_test.go +++ b/pkg/services/sqlstore/temp_user_test.go @@ -8,82 +8,86 @@ import ( "time" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + + "github.com/stretchr/testify/require" ) func TestTempUserCommandsAndQueries(t *testing.T) { - Convey("Testing Temp User commands & queries", t, func() { + cmd := models.CreateTempUserCommand{ + OrgId: 2256, + Name: "hello", + Code: "asd", + Email: "e@as.co", + Status: models.TmpUserInvitePending, + } + setup := func(t *testing.T) { InitTestDB(t) + err := CreateTempUser(&cmd) + require.Nil(t, err) + } - Convey("Given saved api key", func() { - cmd := models.CreateTempUserCommand{ - OrgId: 2256, - Name: "hello", - Code: "asd", - Email: "e@as.co", - Status: models.TmpUserInvitePending, - } - err := CreateTempUser(&cmd) - So(err, ShouldBeNil) + t.Run("Should be able to get temp users by org id", func(t *testing.T) { + setup(t) + query := models.GetTempUsersQuery{OrgId: 2256, Status: models.TmpUserInvitePending} + err := GetTempUsersQuery(&query) - Convey("Should be able to get temp users by org id", func() { - query := models.GetTempUsersQuery{OrgId: 2256, Status: models.TmpUserInvitePending} - err = GetTempUsersQuery(&query) + require.Nil(t, err) + require.Equal(t, 1, len(query.Result)) + }) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 1) - }) + t.Run("Should be able to get temp users by email", func(t *testing.T) { + setup(t) + query := models.GetTempUsersQuery{Email: "e@as.co", Status: models.TmpUserInvitePending} + err := GetTempUsersQuery(&query) - Convey("Should be able to get temp users by email", func() { - query := models.GetTempUsersQuery{Email: "e@as.co", Status: models.TmpUserInvitePending} - err = GetTempUsersQuery(&query) + require.Nil(t, err) + require.Equal(t, 1, len(query.Result)) + }) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 1) - }) + t.Run("Should be able to get temp users by code", func(t *testing.T) { + setup(t) + query := models.GetTempUserByCodeQuery{Code: "asd"} + err := GetTempUserByCode(&query) - Convey("Should be able to get temp users by code", func() { - query := models.GetTempUserByCodeQuery{Code: "asd"} - err = GetTempUserByCode(&query) + require.Nil(t, err) + require.Equal(t, "hello", query.Result.Name) + }) - So(err, ShouldBeNil) - So(query.Result.Name, ShouldEqual, "hello") - }) + t.Run("Should be able update status", func(t *testing.T) { + setup(t) + cmd2 := models.UpdateTempUserStatusCommand{Code: "asd", Status: models.TmpUserRevoked} + err := UpdateTempUserStatus(&cmd2) + require.Nil(t, err) + }) - Convey("Should be able update status", func() { - cmd2 := models.UpdateTempUserStatusCommand{Code: "asd", Status: models.TmpUserRevoked} - err := UpdateTempUserStatus(&cmd2) - So(err, ShouldBeNil) - }) + t.Run("Should be able update email sent and email sent on", func(t *testing.T) { + setup(t) + cmd2 := models.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code} + err := UpdateTempUserWithEmailSent(&cmd2) + require.Nil(t, err) - Convey("Should be able update email sent and email sent on", func() { - cmd2 := models.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code} - err := UpdateTempUserWithEmailSent(&cmd2) - So(err, ShouldBeNil) + query := models.GetTempUsersQuery{OrgId: 2256, Status: models.TmpUserInvitePending} + err = GetTempUsersQuery(&query) - query := models.GetTempUsersQuery{OrgId: 2256, Status: models.TmpUserInvitePending} - err = GetTempUsersQuery(&query) + require.Nil(t, err) + require.True(t, query.Result[0].EmailSent) + require.False(t, query.Result[0].EmailSentOn.UTC().Before(query.Result[0].Created.UTC())) + }) - So(err, ShouldBeNil) - So(query.Result[0].EmailSent, ShouldBeTrue) - So(query.Result[0].EmailSentOn.UTC(), ShouldHappenOnOrAfter, query.Result[0].Created.UTC()) - }) + t.Run("Should be able expire temp user", func(t *testing.T) { + setup(t) + createdAt := time.Unix(cmd.Result.Created, 0) + cmd2 := models.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)} + err := ExpireOldUserInvites(&cmd2) + require.Nil(t, err) + require.Equal(t, int64(1), cmd2.NumExpired) - Convey("Should be able expire temp user", func() { - createdAt := time.Unix(cmd.Result.Created, 0) - cmd2 := models.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)} - err := ExpireOldUserInvites(&cmd2) - So(err, ShouldBeNil) - So(cmd2.NumExpired, ShouldEqual, int64(1)) - - Convey("Should do nothing when no temp users to expire", func() { - createdAt := time.Unix(cmd.Result.Created, 0) - cmd2 := models.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)} - err := ExpireOldUserInvites(&cmd2) - So(err, ShouldBeNil) - So(cmd2.NumExpired, ShouldEqual, int64(0)) - }) - }) + t.Run("Should do nothing when no temp users to expire", func(t *testing.T) { + createdAt := time.Unix(cmd.Result.Created, 0) + cmd2 := models.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)} + err := ExpireOldUserInvites(&cmd2) + require.Nil(t, err) + require.Equal(t, int64(0), cmd2.NumExpired) }) }) } diff --git a/pkg/services/sqlstore/transactions_test.go b/pkg/services/sqlstore/transactions_test.go index 78ab4f14fc3..c8f2f1a1dc0 100644 --- a/pkg/services/sqlstore/transactions_test.go +++ b/pkg/services/sqlstore/transactions_test.go @@ -8,9 +8,8 @@ import ( "errors" "testing" - . "github.com/smartystreets/goconvey/convey" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/require" ) var ErrProvokedError = errors.New("testing error") @@ -18,40 +17,40 @@ var ErrProvokedError = errors.New("testing error") func TestTransaction(t *testing.T) { ss := InitTestDB(t) - Convey("InTransaction", t, func() { - cmd := &models.AddApiKeyCommand{Key: "secret-key", Name: "key", OrgId: 1} - + cmd := &models.AddApiKeyCommand{Key: "secret-key", Name: "key", OrgId: 1} + t.Run("can update key", func(t *testing.T) { err := AddAPIKey(context.Background(), cmd) - So(err, ShouldBeNil) + require.Nil(t, err) - Convey("can update key", func() { - err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { - return deleteAPIKey(sess, cmd.Result.Id, 1) - }) - - So(err, ShouldBeNil) - - query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id} - err = GetApiKeyById(query) - So(err, ShouldEqual, models.ErrInvalidApiKey) + err = ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + return deleteAPIKey(sess, cmd.Result.Id, 1) }) - Convey("won't update if one handler fails", func() { - err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { - err := deleteAPIKey(sess, cmd.Result.Id, 1) - if err != nil { - return err - } + require.Nil(t, err) - return ErrProvokedError - }) + query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id} + err = GetApiKeyById(query) + require.Equal(t, models.ErrInvalidApiKey, err) + }) - So(err, ShouldEqual, ErrProvokedError) + t.Run("won't update if one handler fails", func(t *testing.T) { + err := AddAPIKey(context.Background(), cmd) + require.Nil(t, err) - query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id} - err = GetApiKeyById(query) - So(err, ShouldBeNil) - So(query.Result.Id, ShouldEqual, cmd.Result.Id) + err = ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + err := deleteAPIKey(sess, cmd.Result.Id, 1) + if err != nil { + return err + } + + return ErrProvokedError }) + + require.Equal(t, ErrProvokedError, err) + + query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id} + err = GetApiKeyById(query) + require.Nil(t, err) + require.Equal(t, cmd.Result.Id, query.Result.Id) }) } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 94897e39e21..32f4fc890d2 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -159,7 +159,9 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) if query.RangeQuery { rangeResponse, _, err := client.QueryRange(ctx, query.Expr, timeRange) if err != nil { - return &result, fmt.Errorf("query: %s failed with: %v", query.Expr, err) + plog.Error("Range query", query.Expr, "failed with", err) + result.Responses[query.RefId] = backend.DataResponse{Error: err} + continue } response[RangeQueryType] = rangeResponse } @@ -167,16 +169,19 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) if query.InstantQuery { instantResponse, _, err := client.Query(ctx, query.Expr, query.End) if err != nil { - return &result, fmt.Errorf("query: %s failed with: %v", query.Expr, err) + plog.Error("Instant query", query.Expr, "failed with", err) + result.Responses[query.RefId] = backend.DataResponse{Error: err} + continue } response[InstantQueryType] = instantResponse } - // For now, we ignore exemplar errors and continue with processing of other results + if query.ExemplarQuery { exemplarResponse, err := client.QueryExemplars(ctx, query.Expr, timeRange.Start, timeRange.End) if err != nil { - exemplarResponse = nil plog.Error("Exemplar query", query.Expr, "failed with", err) + result.Responses[query.RefId] = backend.DataResponse{Error: err} + continue } response[ExemplarQueryType] = exemplarResponse } diff --git a/public/app/angular/panel/panel_directive.ts b/public/app/angular/panel/panel_directive.ts index 7f0c5b642e8..90142b54c0e 100644 --- a/public/app/angular/panel/panel_directive.ts +++ b/public/app/angular/panel/panel_directive.ts @@ -4,8 +4,9 @@ import { PanelEvents } from '@grafana/data'; import { PanelModel } from '../../features/dashboard/state'; import { PanelCtrl } from './panel_ctrl'; import { Subscription } from 'rxjs'; -import { PanelDirectiveReadyEvent, RefreshEvent, RenderEvent } from 'app/types/events'; +import { PanelDirectiveReadyEvent, RenderEvent } from 'app/types/events'; import { coreModule } from 'app/core/core_module'; +import { RefreshEvent } from '@grafana/runtime'; const panelTemplate = ` diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index ac205449093..dcdd9bb6628 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -100,11 +100,12 @@ class AddPermissions extends Component { {() => ( +
+ {loadingLogsVolumeAvailable && ( + + )} + +
{ margin: ${theme.spacing(2, 0, 1)}; border: 1px solid ${theme.colors.border.medium}; `, - flipButton: css` + headerButton: css` margin: ${theme.spacing(0.5, 0, 0, 1)}; `, radioButtons: css` diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index e9035b9cd6e..d9e44cbaa47 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -6,6 +6,7 @@ import { AbsoluteTimeRange, Field, hasLogsContextSupport, + hasLogsVolumeSupport, LoadingState, LogRowModel, RawTimeRange, @@ -13,7 +14,7 @@ import { import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; import { splitOpen } from './state/main'; -import { addResultsToCache, clearCache } from './state/query'; +import { addResultsToCache, clearCache, loadLogsVolumeData } from './state/query'; import { updateTimeRange } from './state/time'; import { getTimeZone } from '../profile/state/selectors'; import { LiveLogsWithTheme } from './LiveLogs'; @@ -67,6 +68,7 @@ export class LogsContainer extends PureComponent { render() { const { + datasourceInstance, loading, loadingState, logRows, @@ -87,6 +89,8 @@ export class LogsContainer extends PureComponent { exploreId, addResultsToCache, clearCache, + logsVolumeDataProvider, + loadLogsVolumeData, } = this.props; if (!logRows) { @@ -146,6 +150,8 @@ export class LogsContainer extends PureComponent { getFieldLinks={this.getFieldLinks} addResultsToCache={() => addResultsToCache(exploreId)} clearCache={() => clearCache(exploreId)} + loadingLogsVolumeAvailable={hasLogsVolumeSupport(datasourceInstance) && !!logsVolumeDataProvider} + onClickLoadLogsVolume={() => loadLogsVolumeData(exploreId)} /> @@ -158,7 +164,18 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string } const explore = state.explore; // @ts-ignore const item: ExploreItemState = explore[exploreId]; - const { logsResult, loading, scanning, datasourceInstance, isLive, isPaused, range, absoluteRange } = item; + const { + logsResult, + loading, + scanning, + datasourceInstance, + isLive, + isPaused, + range, + absoluteRange, + logsVolumeDataProvider, + logsVolumeData, + } = item; const timeZone = getTimeZone(state.user); return { @@ -175,6 +192,8 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string } isPaused, range, absoluteRange, + logsVolumeDataProvider, + logsVolumeData, }; } @@ -183,6 +202,7 @@ const mapDispatchToProps = { splitOpen, addResultsToCache, clearCache, + loadLogsVolumeData, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/explore/SecondaryActions.test.tsx b/public/app/features/explore/SecondaryActions.test.tsx index 9bf7566f02b..6b55113f6d8 100644 --- a/public/app/features/explore/SecondaryActions.test.tsx +++ b/public/app/features/explore/SecondaryActions.test.tsx @@ -6,7 +6,6 @@ import { SecondaryActions } from './SecondaryActions'; const addQueryRowButtonSelector = '[aria-label="Add row button"]'; const richHistoryButtonSelector = '[aria-label="Rich history button"]'; const queryInspectorButtonSelector = '[aria-label="Query inspector button"]'; -const onClickLoadLogsVolumeSelector = '[aria-label="Load logs volume button"]'; describe('SecondaryActions', () => { it('should render component two buttons', () => { @@ -15,7 +14,6 @@ describe('SecondaryActions', () => { onClickAddQueryRowButton={noop} onClickRichHistoryButton={noop} onClickQueryInspectorButton={noop} - onClickLoadLogsVolume={noop} /> ); expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(1); @@ -29,7 +27,6 @@ describe('SecondaryActions', () => { onClickAddQueryRowButton={noop} onClickRichHistoryButton={noop} onClickQueryInspectorButton={noop} - onClickLoadLogsVolume={noop} /> ); expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(0); @@ -43,7 +40,6 @@ describe('SecondaryActions', () => { onClickAddQueryRowButton={noop} onClickRichHistoryButton={noop} onClickQueryInspectorButton={noop} - onClickLoadLogsVolume={noop} /> ); expect(wrapper.find(addQueryRowButtonSelector).props().disabled).toBe(true); @@ -53,14 +49,11 @@ describe('SecondaryActions', () => { const onClickAddRow = jest.fn(); const onClickHistory = jest.fn(); const onClickQueryInspector = jest.fn(); - const onClickLoadLogsVolumeInspector = jest.fn(); const wrapper = shallow( ); @@ -72,8 +65,5 @@ describe('SecondaryActions', () => { wrapper.find(queryInspectorButtonSelector).simulate('click'); expect(onClickQueryInspector).toBeCalled(); - - wrapper.find(onClickLoadLogsVolumeSelector).simulate('click'); - expect(onClickQueryInspector).toBeCalled(); }); }); diff --git a/public/app/features/explore/SecondaryActions.tsx b/public/app/features/explore/SecondaryActions.tsx index 53aacd54cff..4f56f8c8b4e 100644 --- a/public/app/features/explore/SecondaryActions.tsx +++ b/public/app/features/explore/SecondaryActions.tsx @@ -8,12 +8,10 @@ type Props = { addQueryRowButtonHidden?: boolean; richHistoryButtonActive?: boolean; queryInspectorButtonActive?: boolean; - loadingLogsVolumeAvailable?: boolean; onClickAddQueryRowButton: () => void; onClickRichHistoryButton: () => void; onClickQueryInspectorButton: () => void; - onClickLoadLogsVolume: () => void; }; const getStyles = (theme: GrafanaTheme2) => { @@ -58,16 +56,6 @@ export function SecondaryActions(props: Props) { > Inspector - {props.loadingLogsVolumeAvailable && ( - - )}
); diff --git a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap index fc9e55f9bf0..193e62eaaa0 100644 --- a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap @@ -20,9 +20,7 @@ exports[`Explore should render component 1`] = ` vo return { url, panel, - datasource, get, getDataSourceSrv, getTimeSrv, @@ -51,13 +50,6 @@ describe('navigateToExplore', () => { expect(getDataSourceSrv).toHaveBeenCalledTimes(1); }); - it('then getDataSourceSrv.get should have been called with correct arguments', async () => { - const { get, panel } = await getNavigateToExploreContext(openInNewWindow); - - expect(get).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledWith(panel.datasource); - }); - it('then getTimeSrv should have been called once', async () => { const { getTimeSrv } = await getNavigateToExploreContext(openInNewWindow); @@ -65,15 +57,13 @@ describe('navigateToExplore', () => { }); it('then getExploreUrl should have been called with correct arguments', async () => { - const { getExploreUrl, panel, datasource, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext( + const { getExploreUrl, panel, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext( openInNewWindow ); expect(getExploreUrl).toHaveBeenCalledTimes(1); expect(getExploreUrl).toHaveBeenCalledWith({ panel, - panelTargets: panel.targets, - panelDatasource: datasource, datasourceSrv: getDataSourceSrv(), timeSrv: getTimeSrv(), }); @@ -94,13 +84,6 @@ describe('navigateToExplore', () => { expect(getDataSourceSrv).toHaveBeenCalledTimes(1); }); - it('then getDataSourceSrv.get should have been called with correct arguments', async () => { - const { get, panel } = await getNavigateToExploreContext(openInNewWindow); - - expect(get).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledWith(panel.datasource); - }); - it('then getTimeSrv should have been called once', async () => { const { getTimeSrv } = await getNavigateToExploreContext(openInNewWindow); @@ -108,15 +91,13 @@ describe('navigateToExplore', () => { }); it('then getExploreUrl should have been called with correct arguments', async () => { - const { getExploreUrl, panel, datasource, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext( + const { getExploreUrl, panel, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext( openInNewWindow ); expect(getExploreUrl).toHaveBeenCalledTimes(1); expect(getExploreUrl).toHaveBeenCalledWith({ panel, - panelTargets: panel.targets, - panelDatasource: datasource, datasourceSrv: getDataSourceSrv(), timeSrv: getTimeSrv(), }); diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 12b7e9de156..96de72611ea 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -138,11 +138,8 @@ export const navigateToExplore = ( return async (dispatch) => { const { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow } = dependencies; const datasourceSrv = getDataSourceSrv(); - const datasource = await datasourceSrv.get(panel.datasource); const path = await getExploreUrl({ panel, - panelTargets: panel.targets, - panelDatasource: datasource, datasourceSrv, timeSrv: getTimeSrv(), }); diff --git a/public/app/features/inspector/QueryInspector.tsx b/public/app/features/inspector/QueryInspector.tsx index e2546a0d4dd..226eb1e6941 100644 --- a/public/app/features/inspector/QueryInspector.tsx +++ b/public/app/features/inspector/QueryInspector.tsx @@ -8,11 +8,10 @@ import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipb import { PanelModel } from 'app/features/dashboard/state'; import { getPanelInspectorStyles } from './styles'; import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils'; -import { config } from '@grafana/runtime'; +import { config, RefreshEvent } from '@grafana/runtime'; import { css } from '@emotion/css'; import { Subscription } from 'rxjs'; import { backendSrv } from 'app/core/services/backend_srv'; -import { RefreshEvent } from 'app/types/events'; interface DsQuery { isLoading: boolean; diff --git a/public/app/features/plugins/admin/components/PluginListItem.test.tsx b/public/app/features/plugins/admin/components/PluginListItem.test.tsx index 30ee6565c7e..f23c2f9a3c2 100644 --- a/public/app/features/plugins/admin/components/PluginListItem.test.tsx +++ b/public/app/features/plugins/admin/components/PluginListItem.test.tsx @@ -4,7 +4,32 @@ import { PluginErrorCode, PluginSignatureStatus, PluginType } from '@grafana/dat import { PluginListItem } from './PluginListItem'; import { CatalogPlugin, PluginListDisplayMode } from '../types'; +/** + * The whole Icon component needs to be mock + * currently is using react-inlinesvg that does not render the icon svg in the test. + * + * There is solution to mock the library on __mocks__ + * https://github.com/gilbarbara/react-inlinesvg/issues/145 + * But unfortunately that causes conflict with DashboardSearch.test.tsx + */ + +jest.mock('@grafana/ui', () => { + const IconMock = ({ title }: { title: string }) => { + return ( + + {title} + + ); + }; + IconMock.displayName = 'Icon'; + return Object.assign({}, jest.requireActual('@grafana/ui'), { Icon: IconMock }); +}); + describe('PluginListItem', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + const plugin: CatalogPlugin = { description: 'The test plugin', downloads: 5, @@ -49,21 +74,21 @@ describe('PluginListItem', () => { const datasourcePlugin = { ...plugin, type: PluginType.datasource }; render(); - expect(screen.getByLabelText(/datasource plugin icon/i)).toBeVisible(); + expect(screen.getByTitle(/datasource plugin/i)).toBeInTheDocument(); }); it('renders a panel plugin with correct icon', () => { const panelPlugin = { ...plugin, type: PluginType.panel }; render(); - expect(screen.getByLabelText(/panel plugin icon/i)).toBeVisible(); + expect(screen.getByTitle(/panel plugin/i)).toBeInTheDocument(); }); it('renders an app plugin with correct icon', () => { const appPlugin = { ...plugin, type: PluginType.app }; render(); - expect(screen.getByLabelText(/app plugin icon/i)).toBeVisible(); + expect(screen.getByTitle(/app plugin/i)).toBeInTheDocument(); }); it('renders a disabled plugin with a badge to indicate its error', () => { @@ -92,21 +117,21 @@ describe('PluginListItem', () => { const datasourcePlugin = { ...plugin, type: PluginType.datasource }; render(); - expect(screen.getByLabelText(/datasource plugin icon/i)).toBeVisible(); + expect(screen.getByTitle(/datasource plugin/i)).toBeInTheDocument(); }); it('renders a panel plugin with correct icon', () => { const panelPlugin = { ...plugin, type: PluginType.panel }; render(); - expect(screen.getByLabelText(/panel plugin icon/i)).toBeVisible(); + expect(screen.getByTitle(/panel plugin/i)).toBeInTheDocument(); }); it('renders an app plugin with correct icon', () => { const appPlugin = { ...plugin, type: PluginType.app }; render(); - expect(screen.getByLabelText(/app plugin icon/i)).toBeVisible(); + expect(screen.getByTitle(/app plugin/i)).toBeInTheDocument(); }); it('renders a disabled plugin with a badge to indicate its error', () => { diff --git a/public/app/features/plugins/admin/components/PluginListItem.tsx b/public/app/features/plugins/admin/components/PluginListItem.tsx index 0fd69691be0..0928d689b66 100644 --- a/public/app/features/plugins/admin/components/PluginListItem.tsx +++ b/public/app/features/plugins/admin/components/PluginListItem.tsx @@ -30,7 +30,7 @@ export function PluginListItem({ plugin, pathName, displayMode = PluginListDispl
- {plugin.type && } + {plugin.type && }
); diff --git a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts index 8061be2dfee..b7756e6295c 100644 --- a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts +++ b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts @@ -16,7 +16,7 @@ import { AnnotationsWorker } from './AnnotationsWorker'; import { getAnnotationsByPanelId } from './utils'; import { DashboardModel } from '../../../dashboard/state'; import { getTimeSrv, TimeSrv } from '../../../dashboard/services/TimeSrv'; -import { RefreshEvent } from '../../../../types/events'; +import { RefreshEvent } from '@grafana/runtime'; import { config } from 'app/core/config'; import { UnifiedAlertStatesWorker } from './UnifiedAlertStatesWorker'; @@ -31,7 +31,7 @@ class DashboardQueryRunnerImpl implements DashboardQueryRunner { private readonly dashboard: DashboardModel, private readonly timeSrv: TimeSrv = getTimeSrv(), private readonly workers: DashboardQueryRunnerWorker[] = [ - config.featureToggles.ngalert ? new UnifiedAlertStatesWorker() : new AlertStatesWorker(), + config.unifiedAlertingEnabled ? new UnifiedAlertStatesWorker() : new AlertStatesWorker(), new SnapshotWorker(), new AnnotationsWorker(), ] diff --git a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx index 1dd0b441721..b2b56b1ccf8 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx @@ -75,57 +75,78 @@ export const LabelFilter: FunctionComponent = ({ noFillEnd={filters.length > 1} > - {filters.map(({ key, operator, value, condition }, index) => ( - - - onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f)))) - } - menuPlacement="bottom" - renderControl={OperatorButton} - /> - `Use label key: ${v}`} + value={key} + options={options} + onChange={({ value: key = '' }) => { + onChange( + filtersToStringArray( + filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f)) + ) + ); + }} + /> + `Use label value: ${v}`} + allowCustomValue + value={value} + placeholder="add filter value" + options={valueOptions} + onChange={({ value = '' }) => + onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f)))) + } + /> + + {index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && } + + ); + })} {!filters.length && } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index de2b5be890b..64c6421fbe2 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -573,6 +573,16 @@ describe('AzureMonitorDatasource', () => { expect(results[0].value).toEqual('nodeapp'); }); }); + + it('should return ignore letter case', () => { + return ctx.ds + .getResourceNames('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodeapp', 'microsoft.insights/Components') + .then((results: Array<{ text: string; value: string }>) => { + expect(results.length).toEqual(1); + expect(results[0].text).toEqual('nodeapp'); + expect(results[0].value).toEqual('nodeapp'); + }); + }); }); describe('and the metric definition is blobServices', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts index 126f8c69d1e..c2011e53967 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts @@ -40,7 +40,10 @@ export default class ResponseParser { } for (let i = 0; i < result.value.length; i++) { - if (result.value[i].type === metricDefinition) { + if ( + typeof result.value[i].type === 'string' && + result.value[i].type.toLocaleLowerCase() === metricDefinition.toLocaleLowerCase() + ) { list.push({ text: result.value[i].name, value: result.value[i].name, diff --git a/public/app/plugins/datasource/influxdb/components/FluxQueryEditor.tsx b/public/app/plugins/datasource/influxdb/components/FluxQueryEditor.tsx index 86bbd78d0ab..19211ed4381 100644 --- a/public/app/plugins/datasource/influxdb/components/FluxQueryEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/FluxQueryEditor.tsx @@ -167,7 +167,7 @@ export class FluxQueryEditor extends PureComponent { const helpTooltip = (
Type: ctrl+space to show template variable suggestions
- Many queries can be copied from chronograph + Many queries can be copied from Chronograf
); diff --git a/public/app/plugins/datasource/tempo/graphTransform.test.ts b/public/app/plugins/datasource/tempo/graphTransform.test.ts index 3f622cf35f4..43f595b9471 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.test.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.test.ts @@ -73,7 +73,7 @@ describe('mapPromMetricsToServiceMap', () => { { name: 'id', values: new ArrayVector(['db', 'app', 'lb']) }, { name: 'title', values: new ArrayVector(['db', 'app', 'lb']) }, { name: 'mainStat', values: new ArrayVector([1000, 2000, NaN]) }, - { name: 'secondaryStat', values: new ArrayVector([10, 20, NaN]) }, + { name: 'secondaryStat', values: new ArrayVector([0.17, 0.33, NaN]) }, ]); expect(edges.fields).toMatchObject([ { name: 'id', values: new ArrayVector(['app_db', 'lb_app']) }, diff --git a/public/app/plugins/datasource/tempo/graphTransform.ts b/public/app/plugins/datasource/tempo/graphTransform.ts index c8a95482e11..3de895b2cf9 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.ts @@ -167,18 +167,18 @@ function createServiceMapDataFrames() { const nodes = createDF('Nodes', [ { name: Fields.id }, { name: Fields.title }, - { name: Fields.mainStat, config: { unit: 'ms/t', displayName: 'Average response time' } }, + { name: Fields.mainStat, config: { unit: 'ms/r', displayName: 'Average response time' } }, { name: Fields.secondaryStat, - config: { unit: 't/min', displayName: 'Transactions per minute' }, + config: { unit: 'r/sec', displayName: 'Requests per second' }, }, ]); const edges = createDF('Edges', [ { name: Fields.id }, { name: Fields.source }, { name: Fields.target }, - { name: Fields.mainStat, config: { unit: 't', displayName: 'Transactions' } }, - { name: Fields.secondaryStat, config: { unit: 'ms/t', displayName: 'Average response time' } }, + { name: Fields.mainStat, config: { unit: 'r', displayName: 'Requests' } }, + { name: Fields.secondaryStat, config: { unit: 'ms/r', displayName: 'Average response time' } }, ]); return [nodes, edges]; @@ -257,8 +257,8 @@ function convertToDataFrames( title: nodeId, // NaN will not be shown in the node graph. This happens for a root client node which did not process // any requests itself. - mainStat: node.total ? (node.seconds / node.total) * 1000 : Number.NaN, - secondaryStat: node.total ? node.total / (rangeMs / (1000 * 60)) : Number.NaN, + mainStat: node.total ? (node.seconds / node.total) * 1000 : Number.NaN, // Average response time + secondaryStat: node.total ? Math.round((node.total / (rangeMs / 1000)) * 100) / 100 : Number.NaN, // Request per second (to 2 decimals) }); } for (const edgeId of Object.keys(edgesMap)) { @@ -267,8 +267,8 @@ function convertToDataFrames( id: edgeId, source: edge.source, target: edge.target, - mainStat: edge.total, - secondaryStat: edge.total ? (edge.seconds / edge.total) * 1000 : Number.NaN, + mainStat: edge.total, // Requests + secondaryStat: edge.total ? (edge.seconds / edge.total) * 1000 : Number.NaN, // Average response time }); } diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index 0aae1c9cd43..330019f0d8f 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -37,7 +37,7 @@ export class CanvasPanel extends Component { // Only the initial options are ever used. // later changes are all controlled by the scene - this.scene = new Scene(this.props.options.root, this.onUpdateScene); + this.scene = new Scene(this.props.options.root, this.props.options.inlineEditing, this.onUpdateScene); this.scene.updateSize(props.width, props.height); this.scene.updateData(props.data); @@ -109,13 +109,20 @@ export class CanvasPanel extends Component { changed = true; } - // After editing, the options are valid, but the scene was in a different panel - if (this.needsReload && this.props.options !== nextProps.options) { + // After editing, the options are valid, but the scene was in a different panel or inline editing mode has changed + const shouldUpdateSceneAndPanel = + (this.needsReload && this.props.options !== nextProps.options) || + this.props.options.inlineEditing !== nextProps.options.inlineEditing; + if (shouldUpdateSceneAndPanel) { this.needsReload = false; - this.scene.load(nextProps.options.root); + this.scene.load(nextProps.options.root, nextProps.options.inlineEditing); this.scene.updateSize(nextProps.width, nextProps.height); this.scene.updateData(nextProps.data); changed = true; + + if (this.props.options.inlineEditing) { + this.scene.selecto?.destroy(); + } } return changed; diff --git a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx index 3a4e6fa7a43..a374bdbcf33 100644 --- a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { css, cx } from '@emotion/css'; import { Button, Container, Icon, IconButton, stylesFactory, ValuePicker } from '@grafana/ui'; -import { GrafanaTheme, SelectableValue, StandardEditorProps } from '@grafana/data'; +import { AppEvents, GrafanaTheme, SelectableValue, StandardEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; @@ -9,6 +9,7 @@ import { PanelOptions } from '../models.gen'; import { InstanceState } from '../CanvasPanel'; import { LayerActionID } from '../types'; import { canvasElementRegistry } from 'app/features/canvas'; +import appEvents from 'app/core/app_events'; type Props = StandardEditorProps; @@ -36,7 +37,11 @@ export class LayerElementListEditor extends PureComponent { const { settings } = this.props.item; if (settings?.scene && settings?.scene?.selecto) { - settings.scene.selecto.clickTarget(item, item?.div); + try { + settings.scene.selecto.clickTarget(item, item?.div); + } catch (error) { + appEvents.emit(AppEvents.alertError, ['Unable to select element with inline editing disabled']); + } } }; diff --git a/public/app/plugins/panel/canvas/module.tsx b/public/app/plugins/panel/canvas/module.tsx index 28bb10cb5a1..0525400ee02 100644 --- a/public/app/plugins/panel/canvas/module.tsx +++ b/public/app/plugins/panel/canvas/module.tsx @@ -14,7 +14,7 @@ export const plugin = new PanelPlugin(CanvasPanel) builder.addBooleanSwitch({ path: 'inlineEditing', name: 'Inline editing', - description: 'Enable editing while the panel is in dashboard mode', + description: 'Enable editing the panel directly', defaultValue: true, }); diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index 9fb20b9c735..7afb615eb17 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -12,7 +12,7 @@ export class ThresholdFormCtrl { $onInit() { this.panel = this.panelCtrl.panel; - if (this.panel.alert && !config.featureToggles.ngalert) { + if (this.panel.alert && !config.unifiedAlertingEnabled) { this.disabled = true; } diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index a195b13d4f5..42e26003331 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -11,6 +11,7 @@ import { setLegacyAngularInjector, setLocationSrv, locationService, + setAppEvents, } from '@grafana/runtime'; import config from 'app/core/config'; import coreModule from 'app/core/core_module'; @@ -56,6 +57,7 @@ export class GrafanaCtrl { datasourceSrv.init(config.datasources, config.defaultDatasource); setLocationSrv(locationService); + setAppEvents(appEvents); initGrafanaLive(); diff --git a/public/app/types/events.ts b/public/app/types/events.ts index 2da73d11ae7..7311feb048f 100644 --- a/public/app/types/events.ts +++ b/public/app/types/events.ts @@ -1,11 +1,4 @@ -import { - AnnotationQuery, - BusEventBase, - BusEventWithPayload, - eventFactory, - GrafanaTheme2, - TimeRange, -} from '@grafana/data'; +import { AnnotationQuery, BusEventBase, BusEventWithPayload, eventFactory } from '@grafana/data'; import { IconName } from '@grafana/ui'; /** @@ -132,10 +125,6 @@ export class DashboardPanelsChangedEvent extends BusEventBase { static type = 'dashboard-panels-changed'; } -export class RefreshEvent extends BusEventBase { - static type = 'refresh'; -} - export class PanelDirectiveReadyEvent extends BusEventBase { static type = 'panel-directive-ready'; } @@ -144,10 +133,6 @@ export class RenderEvent extends BusEventBase { static type = 'render'; } -export class ThemeChangedEvent extends BusEventWithPayload { - static type = 'theme-changed'; -} - export class ZoomOutEvent extends BusEventWithPayload { static type = 'zoom-out'; } @@ -198,10 +183,6 @@ export class AnnotationQueryFinished extends BusEventWithPayload { - static type = 'time-range-updated'; -} - export class PanelEditEnteredEvent extends BusEventWithPayload { static type = 'panel-edit-started'; } diff --git a/public/sass/.sass-lint.yml b/public/sass/.sass-lint.yml deleted file mode 100644 index c04f5d98797..00000000000 --- a/public/sass/.sass-lint.yml +++ /dev/null @@ -1,100 +0,0 @@ -options: - formatter: stylish -rules: - quotes: - - 0 - - - style: 'single' - - brace-style: - - 2 - - - style: '1tbs' - - space-before-brace: - - 2 - - no-duplicate-properties: - - 0 - - - exclude: - - 'font-size' - - 'word-break' - - empty-line-between-blocks: - - 0 - - - allow-single-line-rulesets: 1 - - # Extends - extends-before-mixins: 0 - extends-before-declarations: 0 - placeholder-in-extend: 0 - # Mixins - mixins-before-declarations: 0 - - # Line Spacing - one-declaration-per-line: 0 - single-line-per-selector: 0 - - # Disallows - no-color-keywords: 0 - no-color-literals: 0 - no-css-comments: 0 - no-debug: 0 - no-empty-rulesets: 1 - no-extends: 0 - no-ids: 0 - no-important: 0 - no-invalid-hex: 0 - no-mergeable-selectors: 0 - no-misspelled-properties: 0 - no-qualifying-elements: 0 - no-trailing-zero: 0 - no-transition-all: 0 - no-url-protocols: 0 - no-vendor-prefixes: 0 - no-warn: 0 - property-units: 0 - pseudo-element: 0 - - # Nesting - force-attribute-nesting: 0 - force-element-nesting: 0 - force-pseudo-nesting: 0 - - # Name Formats - class-name-format: 0 - function-name-format: 0 - id-name-format: 0 - mixin-name-format: 0 - placeholder-name-format: 0 - variable-name-format: 0 - - # Style Guide - bem-depth: 0 - border-zero: 0 - clean-import-paths: 0 - empty-args: 0 - hex-length: 0 - hex-notation: 0 - indentation: 0 - leading-zero: 0 - nesting-depth: 0 - property-sort-order: 0 - shorthand-values: 0 - url-quotes: 0 - variable-for-property: 0 - zero-unit: 0 - - # Inner Spacing - space-after-comma: 0 - space-before-colon: 0 - space-after-colon: 0 - space-before-bang: 0 - space-after-bang: 0 - space-between-parens: 0 - space-around-operator: 0 - - # Final Items - final-newline: 0 diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index af637c71f0e..9e65b0f3067 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -71,7 +71,7 @@ @import 'components/query_part'; @import 'components/jsontree'; @import 'components/edit_sidemenu'; -@import 'components/row.scss'; +@import 'components/row'; @import 'components/json_explorer'; @import 'components/code_editor'; @import 'components/dashboard_grid'; @@ -79,7 +79,7 @@ @import 'components/page_header'; @import 'components/dashboard_settings'; @import 'components/toolbar'; -@import 'components/add_data_source.scss'; +@import 'components/add_data_source'; @import 'components/page_loader'; @import 'components/toggle_button_group'; @import 'components/popover-box'; @@ -100,4 +100,4 @@ @import 'pages/explore'; @import 'pages/plugins'; @import 'old_responsive'; -@import 'components/view_states.scss'; +@import 'components/view_states'; diff --git a/public/sass/_variables.generated.scss b/public/sass/_variables.generated.scss index c1dbcf8814d..43ead5a4489 100644 --- a/public/sass/_variables.generated.scss +++ b/public/sass/_variables.generated.scss @@ -98,8 +98,9 @@ $height-lg: 48; // Typography // ------------------------- - +/* stylelint-disable-next-line string-quotes */ $font-family-sans-serif: "Roboto", "Helvetica", "Arial", sans-serif; +/* stylelint-disable-next-line string-quotes */ $font-family-monospace: 'Roboto Mono', monospace; $font-size-base: 14px !default; diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index ac1deda711e..cda0323f055 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -7,7 +7,6 @@ background-repeat: no-repeat; background-position: center; background-size: contain; - display: inline-block; vertical-align: middle; } diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss index 1a1323435d4..6679b6050e1 100644 --- a/public/sass/components/_dropdown.scss +++ b/public/sass/components/_dropdown.scss @@ -184,11 +184,11 @@ // Open state for the dropdown // --------------------------- .open { - & > .dropdown-menu { + > .dropdown-menu { display: block; } - & > .dropdown > .dropdown-menu { + > .dropdown > .dropdown-menu { // Panel menu. TODO: See if we can merge this with above display: block; } diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index dc5a71cdf3c..d2741fb1920 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -304,7 +304,7 @@ $input-border: 1px solid $input-border-color; top: 50%; margin-top: -7px; - & + .gf-form-input { + + .gf-form-input { position: relative; z-index: 2; padding-left: $space-xl; diff --git a/public/sass/components/_jsontree.scss b/public/sass/components/_jsontree.scss index 3808ba99e5b..79a485ac519 100644 --- a/public/sass/components/_jsontree.scss +++ b/public/sass/components/_jsontree.scss @@ -12,7 +12,7 @@ json-tree { } &::before, - & > .json-tree-key { + > .json-tree-key { cursor: pointer; } } diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss index 5c3039c0962..f24907752af 100644 --- a/public/sass/components/_slate_editor.scss +++ b/public/sass/components/_slate_editor.scss @@ -13,7 +13,6 @@ padding: 6px 8px; min-height: $input-height; width: 100%; - line-height: $line-height-base; color: $text-color; background-color: $input-bg; background-image: none; diff --git a/public/sass/pages/_history.scss b/public/sass/pages/_history.scss index aeb20548d6a..f32ca900bd5 100644 --- a/public/sass/pages/_history.scss +++ b/public/sass/pages/_history.scss @@ -57,7 +57,7 @@ .diff-json-added { color: $diff-json-changed-fg; - & .diff-line-number { + .diff-line-number { color: $diff-json-changed-num; } } @@ -108,7 +108,7 @@ padding: 10px 15px; margin: $space-md 0; - & .diff-group { + .diff-group { padding: 0 5px; } } @@ -128,7 +128,7 @@ .diff-list { padding-left: 40px; - & .diff-list { + .diff-list { padding-left: 0; } } @@ -137,7 +137,7 @@ color: $gray-2; line-height: 2.5; - & > div { + > div { display: inline; } } diff --git a/public/sass/utils/_hacks.scss b/public/sass/utils/_hacks.scss index 31d25c658c2..a3d5c528dd7 100644 --- a/public/sass/utils/_hacks.scss +++ b/public/sass/utils/_hacks.scss @@ -1,5 +1,4 @@ // <3: https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7 -// sass-lint:disable no-empty-rulesets @keyframes onAutoFillStart { from { /**/ diff --git a/scripts/ci-check-strict.sh b/scripts/ci-check-strict.sh index 2a70a11f7c3..0d35063a448 100755 --- a/scripts/ci-check-strict.sh +++ b/scripts/ci-check-strict.sh @@ -3,7 +3,7 @@ set -e echo -e "Collecting code stats (typescript errors & more)" -ERROR_COUNT_LIMIT=12 +ERROR_COUNT_LIMIT=6 ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --strict true | grep -oP 'Found \K(\d+)')" if [ "$ERROR_COUNT" -gt $ERROR_COUNT_LIMIT ]; then diff --git a/scripts/drone/pipelines/main.star b/scripts/drone/pipelines/main.star index 384870e9d3b..50c651d8f01 100644 --- a/scripts/drone/pipelines/main.star +++ b/scripts/drone/pipelines/main.star @@ -10,7 +10,6 @@ load( 'build_backend_step', 'build_frontend_step', 'build_plugins_step', - 'gen_version_step', 'package_step', 'e2e_tests_server_step', 'e2e_tests_step', @@ -69,7 +68,6 @@ def get_steps(edition, is_downstream=False): ensure_cuetsified_step(), ] - # Have to insert Enterprise2 steps before they're depended on (in the gen-version step) if include_enterprise2: edition2 = 'enterprise2' steps.extend([ @@ -81,8 +79,7 @@ def get_steps(edition, is_downstream=False): # Insert remaining steps steps.extend([ - gen_version_step(ver_mode=ver_mode, is_downstream=is_downstream, include_enterprise2=include_enterprise2), - package_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), + package_step(edition=edition, ver_mode=ver_mode, include_enterprise2=include_enterprise2, is_downstream=is_downstream), e2e_tests_server_step(edition=edition), e2e_tests_step(edition=edition), build_storybook_step(edition=edition, ver_mode=ver_mode), @@ -109,7 +106,7 @@ def get_steps(edition, is_downstream=False): if include_enterprise2: edition2 = 'enterprise2' steps.extend([ - package_step(edition=edition2, ver_mode=ver_mode, variants=['linux-x64'], is_downstream=is_downstream), + package_step(edition=edition2, ver_mode=ver_mode, include_enterprise2=include_enterprise2, variants=['linux-x64'], is_downstream=is_downstream), e2e_tests_server_step(edition=edition2, port=3002), e2e_tests_step(edition=edition2, port=3002), upload_packages_step(edition=edition2, ver_mode=ver_mode, is_downstream=is_downstream), diff --git a/scripts/drone/pipelines/pr.star b/scripts/drone/pipelines/pr.star index 23d7eccddf6..fdaa852afac 100644 --- a/scripts/drone/pipelines/pr.star +++ b/scripts/drone/pipelines/pr.star @@ -10,7 +10,6 @@ load( 'test_backend_step', 'test_backend_integration_step', 'test_frontend_step', - 'gen_version_step', 'package_step', 'e2e_tests_server_step', 'e2e_tests_step', @@ -61,7 +60,6 @@ def pr_pipelines(edition): ensure_cuetsified_step(), ] - # Have to insert Enterprise2 steps before they're depended on (in the gen-version step) if include_enterprise2: edition2 = 'enterprise2' steps.append(benchmark_ldap_step()) @@ -75,8 +73,7 @@ def pr_pipelines(edition): # Insert remaining steps steps.extend([ - gen_version_step(ver_mode=ver_mode, include_enterprise2=include_enterprise2), - package_step(edition=edition, ver_mode=ver_mode, variants=variants), + package_step(edition=edition, ver_mode=ver_mode, include_enterprise2=include_enterprise2, variants=variants), e2e_tests_server_step(edition=edition), e2e_tests_step(edition=edition), build_storybook_step(edition=edition, ver_mode=ver_mode), @@ -93,7 +90,7 @@ def pr_pipelines(edition): steps.extend([ redis_integration_tests_step(), memcached_integration_tests_step(), - package_step(edition=edition2, ver_mode=ver_mode, variants=['linux-x64']), + package_step(edition=edition2, ver_mode=ver_mode, include_enterprise2=include_enterprise2, variants=['linux-x64']), e2e_tests_server_step(edition=edition2, port=3002), e2e_tests_step(edition=edition2, port=3002), ]) diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star index 4829a093faa..40590d9f0f0 100644 --- a/scripts/drone/pipelines/release.star +++ b/scripts/drone/pipelines/release.star @@ -13,7 +13,6 @@ load( 'build_backend_step', 'build_frontend_step', 'build_plugins_step', - 'gen_version_step', 'package_step', 'e2e_tests_server_step', 'e2e_tests_step', @@ -97,7 +96,6 @@ def get_steps(edition, ver_mode): ensure_cuetsified_step(), ] - # Have to insert Enterprise2 steps before they're depended on (in the gen-version step) if include_enterprise2: edition2 = 'enterprise2' steps.extend([ @@ -109,8 +107,7 @@ def get_steps(edition, ver_mode): # Insert remaining steps steps.extend([ - gen_version_step(ver_mode=ver_mode, include_enterprise2=include_enterprise2), - package_step(edition=edition, ver_mode=ver_mode), + package_step(edition=edition, ver_mode=ver_mode, include_enterprise2=include_enterprise2), e2e_tests_server_step(edition=edition), e2e_tests_step(edition=edition, tries=3), copy_packages_for_docker_step(), @@ -142,7 +139,7 @@ def get_steps(edition, ver_mode): if include_enterprise2: edition2 = 'enterprise2' steps.extend([ - package_step(edition=edition2, ver_mode=ver_mode, variants=['linux-x64']), + package_step(edition=edition2, ver_mode=ver_mode, include_enterprise2=include_enterprise2, variants=['linux-x64']), e2e_tests_server_step(edition=edition2, port=3002), e2e_tests_step(edition=edition2, port=3002, tries=3), upload_cdn_step(edition=edition2), diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 13cb807e034..e76344c7345 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,6 +1,6 @@ load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token') -grabpl_version = '2.5.2' +grabpl_version = '2.5.5' build_image = 'grafana/build-container:1.4.3' publish_image = 'grafana/grafana-ci-deploy:1.3.1' grafana_docker_image = 'grafana/drone-grafana-docker:0.3.2' @@ -48,9 +48,17 @@ def initialize_step(edition, platform, ver_mode, is_downstream=False, install_de ] if ver_mode == 'release': + args = '${DRONE_TAG}' common_cmds.append('./bin/grabpl verify-version ${DRONE_TAG}') elif ver_mode == 'test-release': + args = test_release_ver common_cmds.append('./bin/grabpl verify-version {}'.format(test_release_ver)) + else: + if not is_downstream: + build_no = '${DRONE_BUILD_NUMBER}' + else: + build_no = '$${SOURCE_BUILD_NUMBER}' + args = '--build-id {}'.format(build_no) identify_runner_step = { 'name': 'identify-runner', @@ -62,6 +70,7 @@ def initialize_step(edition, platform, ver_mode, is_downstream=False, install_de if install_deps: common_cmds.extend([ + './bin/grabpl gen-version {}'.format(args), 'yarn install --immutable', ]) if edition in ('enterprise', 'enterprise2'): @@ -184,7 +193,7 @@ def build_storybook_step(edition, ver_mode): 'image': build_image, 'depends_on': [ # Best to ensure that this step doesn't mess with what's getting built and packaged - 'package', + 'build-frontend', ], 'environment': { 'NODE_OPTIONS': '--max_old_space_size=4096', @@ -500,13 +509,11 @@ def shellcheck_step(): ], } -def gen_version_step(ver_mode, include_enterprise2=False, is_downstream=False): +def package_step(edition, ver_mode, include_enterprise2=False, variants=None, is_downstream=False): deps = [ 'build-plugins', 'build-backend', 'build-frontend', - 'codespell', - 'shellcheck', ] if include_enterprise2: sfx = '-enterprise2' @@ -515,28 +522,6 @@ def gen_version_step(ver_mode, include_enterprise2=False, is_downstream=False): 'test-backend' + sfx, ]) - if ver_mode == 'release': - args = '${DRONE_TAG}' - elif ver_mode == 'test-release': - args = test_release_ver - else: - if not is_downstream: - build_no = '${DRONE_BUILD_NUMBER}' - else: - build_no = '$${SOURCE_BUILD_NUMBER}' - args = '--build-id {}'.format(build_no) - - return { - 'name': 'gen-version', - 'image': build_image, - 'depends_on': deps, - 'commands': [ - './bin/grabpl gen-version {}'.format(args), - ], - } - - -def package_step(edition, ver_mode, variants=None, is_downstream=False): variants_str = '' if variants: variants_str = ' --variants {}'.format(','.join(variants)) @@ -584,11 +569,7 @@ def package_step(edition, ver_mode, variants=None, is_downstream=False): return { 'name': 'package' + enterprise2_suffix(edition), 'image': build_image, - 'depends_on': [ - # This step should have all the dependencies required for packaging, and should generate - # dist/grafana.version - 'gen-version', - ], + 'depends_on': deps, 'environment': env, 'commands': cmds, } @@ -661,7 +642,7 @@ def copy_packages_for_docker_step(): 'name': 'copy-packages-for-docker', 'image': build_image, 'depends_on': [ - 'end-to-end-tests-server', + 'package', ], 'commands': [ 'ls dist/*.tar.gz*', diff --git a/stylelint.config.js b/stylelint.config.js new file mode 100644 index 00000000000..ae077001dcd --- /dev/null +++ b/stylelint.config.js @@ -0,0 +1,45 @@ +module.exports = { + extends: ['stylelint-config-sass-guidelines', 'stylelint-config-prettier'], + ignoreFiles: ['**/node_modules/**/*.scss'], + rules: { + 'at-rule-no-vendor-prefix': null, + 'color-hex-case': null, + 'color-hex-length': null, + 'color-named': null, + 'declaration-block-no-duplicate-properties': [ + true, + { + ignore: 'consecutive-duplicates-with-different-values', + ignoreProperties: ['font-size', 'word-break'], + }, + ], + // Disable equivalent "borderZero" sass-lint rule + 'declaration-property-value-disallowed-list': { + border: [0], + 'border-top': [0], + 'border-right': [0], + 'border-bottom': [0], + 'border-left': [0], + }, + 'function-comma-space-after': null, + 'function-url-quotes': null, + 'length-zero-no-unit': null, + 'max-nesting-depth': null, + 'number-no-trailing-zeros': null, + 'order/order': null, + 'order/properties-alphabetical-order': null, + 'property-no-vendor-prefix': null, + 'rule-empty-line-before': null, + 'scss/at-function-pattern': null, + 'scss/at-mixin-pattern': null, + 'scss/dollar-variable-pattern': null, + 'scss/at-extend-no-missing-placeholder': null, + 'selector-class-pattern': null, + 'selector-max-compound-selectors': null, + 'selector-max-id': null, + 'selector-no-qualifying-type': null, + 'selector-pseudo-element-colon-notation': null, + 'shorthand-property-no-redundant-values': null, + 'value-no-vendor-prefix': null, + }, +}; diff --git a/yarn.lock b/yarn.lock index dde2ffa4484..8b24e22fe10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -109,7 +109,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.15.8, @babel/core@npm:^7.1.0, @babel/core@npm:^7.12.10, @babel/core@npm:^7.7.5": +"@babel/core@npm:7.15.8, @babel/core@npm:>=7.9.0, @babel/core@npm:^7.1.0, @babel/core@npm:^7.12.10, @babel/core@npm:^7.7.5": version: 7.15.8 resolution: "@babel/core@npm:7.15.8" dependencies: @@ -2386,7 +2386,7 @@ __metadata: "@types/node": 10.14.1 "@types/papaparse": 5.2.0 "@types/react": 17.0.30 - "@types/rollup-plugin-visualizer": 2.6.0 + "@types/rollup-plugin-visualizer": 4.2.1 "@types/sinon": ^10.0.4 date-fns: 2.21.3 eventemitter3: 4.0.7 @@ -2414,7 +2414,7 @@ __metadata: "@rollup/plugin-commonjs": 16.0.0 "@rollup/plugin-node-resolve": 10.0.0 "@types/node": 13.7.7 - "@types/rollup-plugin-visualizer": 2.6.0 + "@types/rollup-plugin-visualizer": 4.2.1 "@types/systemjs": ^0.20.6 commander: 5.0.0 execa: 5.1.1 @@ -2441,7 +2441,7 @@ __metadata: "@rollup/plugin-commonjs": 16.0.0 "@rollup/plugin-node-resolve": 10.0.0 "@types/node": 13.7.7 - "@types/rollup-plugin-visualizer": 2.6.0 + "@types/rollup-plugin-visualizer": 4.2.1 blink-diff: 1.0.13 commander: 5.0.0 cypress: 8.4.1 @@ -2495,7 +2495,7 @@ __metadata: "@rollup/plugin-node-resolve": 10.0.0 "@types/history": ^4.7.8 "@types/jest": 26.0.15 - "@types/rollup-plugin-visualizer": 2.6.0 + "@types/rollup-plugin-visualizer": 4.2.1 "@types/systemjs": ^0.20.6 history: 4.10.1 lodash: 4.17.21 @@ -2519,7 +2519,7 @@ __metadata: "@rollup/plugin-commonjs": 16.0.0 "@rollup/plugin-json": 4.1.0 "@rollup/plugin-node-resolve": 10.0.0 - "@types/rollup-plugin-visualizer": 2.6.0 + "@types/rollup-plugin-visualizer": 4.2.1 rollup: 2.33.3 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 @@ -2695,7 +2695,7 @@ __metadata: "@types/react-table": 7.7.2 "@types/react-test-renderer": 17.0.1 "@types/react-transition-group": 4.4.0 - "@types/rollup-plugin-visualizer": 2.6.0 + "@types/rollup-plugin-visualizer": 4.2.1 "@types/slate": 0.47.2 "@types/slate-react": 0.22.5 "@types/tinycolor2": 1.4.1 @@ -6581,6 +6581,31 @@ __metadata: languageName: node linkType: hard +"@stylelint/postcss-css-in-js@npm:^0.37.2": + version: 0.37.2 + resolution: "@stylelint/postcss-css-in-js@npm:0.37.2" + dependencies: + "@babel/core": ">=7.9.0" + peerDependencies: + postcss: ">=7.0.0" + postcss-syntax: ">=0.36.2" + checksum: cc9b5d1bd93b85c5e32754bf28b99031c783bd87a178542e42f84e627f00907c556d3c7839766fe47bb5a8eaa87eae89287e6cc939b9b91e1ab3e7c44acc3014 + languageName: node + linkType: hard + +"@stylelint/postcss-markdown@npm:^0.36.2": + version: 0.36.2 + resolution: "@stylelint/postcss-markdown@npm:0.36.2" + dependencies: + remark: ^13.0.0 + unist-util-find-all-after: ^3.0.2 + peerDependencies: + postcss: ">=7.0.0" + postcss-syntax: ">=0.36.2" + checksum: 5e39bca575356992c27d59fd9ca9ee38867369bdf5bb3d9e31dc074680b77b4b820d48074e5cc0337e047edd8c1e7ef18996bc83199376138f42fbc8ed0cabeb + languageName: node + linkType: hard + "@testing-library/dom@npm:>=7": version: 8.7.2 resolution: "@testing-library/dom@npm:8.7.2" @@ -6645,16 +6670,25 @@ __metadata: languageName: node linkType: hard -"@testing-library/react-hooks@npm:^3.2.1": - version: 3.7.0 - resolution: "@testing-library/react-hooks@npm:3.7.0" +"@testing-library/react-hooks@npm:7.0.2": + version: 7.0.2 + resolution: "@testing-library/react-hooks@npm:7.0.2" dependencies: "@babel/runtime": ^7.12.5 - "@types/testing-library__react-hooks": ^3.4.0 + "@types/react": ">=16.9.0" + "@types/react-dom": ">=16.9.0" + "@types/react-test-renderer": ">=16.9.0" + react-error-boundary: ^3.1.0 peerDependencies: react: ">=16.9.0" + react-dom: ">=16.9.0" react-test-renderer: ">=16.9.0" - checksum: 7ada35cb4114056908990250033527ffd554d055eced797083aca752fb9789b3bc70291077f1d57b8cf64ab5eb7b3827f9de3bcaa38655f2b975c65a33a418d6 + peerDependenciesMeta: + react-dom: + optional: true + react-test-renderer: + optional: true + checksum: 27c6169b5c9832bd02dcea232e6a0a3cd8d4504e13ecb49d57eb5ab6bea5e2f1bff65f3102068b7e57eec3cbd671326dc0b277335014b0edfbdedf11a1fe6db5 languageName: node linkType: hard @@ -7953,7 +7987,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:*": +"@types/react-dom@npm:*, @types/react-dom@npm:>=16.9.0": version: 17.0.9 resolution: "@types/react-dom@npm:17.0.9" dependencies: @@ -7962,12 +7996,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:16.9.9": - version: 16.9.9 - resolution: "@types/react-dom@npm:16.9.9" +"@types/react-dom@npm:17.0.10": + version: 17.0.10 + resolution: "@types/react-dom@npm:17.0.10" dependencies: "@types/react": "*" - checksum: b42a788b6a40a79a738346044c21e6c536f136e9b919fce90847aaaba7298e0b7d4d6845a1449d92a5678c5c221f060d66207a7f4968f7f5991e2511aa8e6860 + checksum: cc7d8d5b77ee2f3b989c107abd8ec0f2460ba1b1ee6e6d637124e1939594b5619fa4166ef0ea7632a69b68358e0b71aa618de446ccc416dcc0017175549da601 languageName: node linkType: hard @@ -8093,7 +8127,7 @@ __metadata: languageName: node linkType: hard -"@types/react-test-renderer@npm:*, @types/react-test-renderer@npm:17.0.1": +"@types/react-test-renderer@npm:*, @types/react-test-renderer@npm:17.0.1, @types/react-test-renderer@npm:>=16.9.0": version: 17.0.1 resolution: "@types/react-test-renderer@npm:17.0.1" dependencies: @@ -8149,7 +8183,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:17.0.30": +"@types/react@npm:17.0.30, @types/react@npm:>=16.9.0": version: 17.0.30 resolution: "@types/react@npm:17.0.30" dependencies: @@ -8229,13 +8263,13 @@ __metadata: languageName: node linkType: hard -"@types/rollup-plugin-visualizer@npm:2.6.0": - version: 2.6.0 - resolution: "@types/rollup-plugin-visualizer@npm:2.6.0" +"@types/rollup-plugin-visualizer@npm:4.2.1": + version: 4.2.1 + resolution: "@types/rollup-plugin-visualizer@npm:4.2.1" dependencies: "@types/node": "*" - rollup: ^0.63.4 - checksum: fe3ca881789eee105313c505d76c61e2c401f561d0e23d4f4694114b6411ee51989946896a9bdba92b3dcf5d0a91482929c2996d9a5a568e5f23d915955d2a4d + rollup: ^2.42.3 + checksum: 4600ba39e863ab6c14739a9de1fe008d204e9e814e9f8afa09e262d34cc5bd67d346eaf31ce274ff867ce700a049125b05a59b8113a46fa82bfdebec0c146ba5 languageName: node linkType: hard @@ -8385,7 +8419,7 @@ __metadata: languageName: node linkType: hard -"@types/testing-library__react-hooks@npm:^3.2.0, @types/testing-library__react-hooks@npm:^3.4.0": +"@types/testing-library__react-hooks@npm:^3.2.0": version: 3.4.1 resolution: "@types/testing-library__react-hooks@npm:3.4.1" dependencies: @@ -9502,15 +9536,6 @@ __metadata: languageName: node linkType: hard -"acorn-jsx@npm:^3.0.0": - version: 3.0.1 - resolution: "acorn-jsx@npm:3.0.1" - dependencies: - acorn: ^3.0.4 - checksum: 43f1302dabfd263674e029537558be832b4bb8baa095a17823bf0ca0509e779deddcb39ab7c398a3f0de406dbf5e6d32dce87da24188ad52397701aa6ebcb6b2 - languageName: node - linkType: hard - "acorn-jsx@npm:^5.3.1": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -9541,16 +9566,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^3.0.4": - version: 3.3.0 - resolution: "acorn@npm:3.3.0" - bin: - acorn: ./bin/acorn - checksum: d24aee1c838a3467c5ab493601ed4b646e4fd0599ca19b16f47f764e7929d0ffcb4d536935283a8d823d64ca3817d27dd5f93830bb86f592249d9feb0fcf78f9 - languageName: node - linkType: hard - -"acorn@npm:^5.0.0, acorn@npm:^5.5.0": +"acorn@npm:^5.0.0": version: 5.7.4 resolution: "acorn@npm:5.7.4" bin: @@ -9680,15 +9696,6 @@ __metadata: languageName: node linkType: hard -"ajv-keywords@npm:^1.0.0": - version: 1.5.1 - resolution: "ajv-keywords@npm:1.5.1" - peerDependencies: - ajv: ">=4.10.0" - checksum: 0e895b321afc359d3a5a946f3402e823efee42186cacd32fa387b9b7f9f53b3909b252929937bcc8eac41a9f400e4ec37dd04ab1e053d19e304c9e62089f0ac7 - languageName: node - linkType: hard - "ajv-keywords@npm:^3.1.0, ajv-keywords@npm:^3.4.1, ajv-keywords@npm:^3.5.2": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -9698,16 +9705,6 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^4.7.0": - version: 4.11.8 - resolution: "ajv@npm:4.11.8" - dependencies: - co: ^4.6.0 - json-stable-stringify: ^1.0.1 - checksum: 1a4fb38ebccc2ff3ab507d5507b133705d056f9db28cb00a59f0753a5f11e809d959b732edcd52c02fed628638ffb9486ee6bd13bf027400b5c9acf9c33e25f2 - languageName: node - linkType: hard - "ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:~6.12.6": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -9806,13 +9803,6 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^1.1.0": - version: 1.4.0 - resolution: "ansi-escapes@npm:1.4.0" - checksum: 287f18ea70cde710dbb83b6b6c4e1d62fcb962b951a601d976df69478a4ebdff6305691e3befb9053d740060544929732b8bade7a9781611dcd2b997e6bda3d6 - languageName: node - linkType: hard - "ansi-escapes@npm:^3.0.0, ansi-escapes@npm:^3.2.0": version: 3.2.0 resolution: "ansi-escapes@npm:3.2.0" @@ -10733,6 +10723,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^2.0.0": + version: 2.0.0 + resolution: "balanced-match@npm:2.0.0" + checksum: 9a5caad6a292c5df164cc6d0c38e0eedf9a1413f42e5fece733640949d74d0052cfa9587c1a1681f772147fb79be495121325a649526957fd75b3a216d1fbc68 + languageName: node + linkType: hard + "baron@npm:3.0.3": version: 3.0.3 resolution: "baron@npm:3.0.3" @@ -11434,15 +11431,6 @@ __metadata: languageName: node linkType: hard -"caller-path@npm:^0.1.0": - version: 0.1.0 - resolution: "caller-path@npm:0.1.0" - dependencies: - callsites: ^0.2.0 - checksum: f4f2216897d2c150e30d06c6a9243115e500184433b42d597f0b8816fda8f6b7f782dba39fc37310dcc67c90e1112729709d3bb9e10983552e76632250b075f3 - languageName: node - linkType: hard - "caller-path@npm:^2.0.0": version: 2.0.0 resolution: "caller-path@npm:2.0.0" @@ -11452,13 +11440,6 @@ __metadata: languageName: node linkType: hard -"callsites@npm:^0.2.0": - version: 0.2.0 - resolution: "callsites@npm:0.2.0" - checksum: 7ed36d5565ec37600fd9642b6e1e00915c078bf2d1c536115c45fe9c07969b50a7d7db61384e533625fa9fee3e6187740784722f0636d52a99419111284cc236 - languageName: node - linkType: hard - "callsites@npm:^2.0.0": version: 2.0.0 resolution: "callsites@npm:2.0.0" @@ -11608,7 +11589,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^1.0.0, chalk@npm:^1.1.1, chalk@npm:^1.1.3, chalk@npm:~1.1.3": +"chalk@npm:^1.0.0, chalk@npm:^1.1.3, chalk@npm:~1.1.3": version: 1.1.3 resolution: "chalk@npm:1.1.3" dependencies: @@ -11631,7 +11612,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:~4.1.0": +"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:~4.1.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -11826,13 +11807,6 @@ __metadata: languageName: node linkType: hard -"circular-json@npm:^0.3.1": - version: 0.3.3 - resolution: "circular-json@npm:0.3.3" - checksum: 61c5e5c244cc752f2d81fa337260327bf9dcb0332eb801039f21b69383050318fd4b30541ce4ebfe6fdd62993a38cb1be3a0d777abfeadf723479a4fc2da70ed - languageName: node - linkType: hard - "circular-json@npm:^0.5.0": version: 0.5.9 resolution: "circular-json@npm:0.5.9" @@ -11903,15 +11877,6 @@ __metadata: languageName: node linkType: hard -"cli-cursor@npm:^1.0.1": - version: 1.0.2 - resolution: "cli-cursor@npm:1.0.2" - dependencies: - restore-cursor: ^1.0.1 - checksum: e3b4400d5e925ed11c7596f82e80e170693f69ac6f0f21da2a400043c37548dd780f985a1a5ef1ffb038e36fc6711d1d4f066b104eed851ae76e34bd883cf2bf - languageName: node - linkType: hard - "cli-cursor@npm:^2.0.0, cli-cursor@npm:^2.1.0": version: 2.1.0 resolution: "cli-cursor@npm:2.1.0" @@ -12040,6 +12005,15 @@ __metadata: languageName: node linkType: hard +"clone-regexp@npm:^2.1.0": + version: 2.2.0 + resolution: "clone-regexp@npm:2.2.0" + dependencies: + is-regexp: ^2.0.0 + checksum: 3624905a98920ad5c196080f4ea4379fa42b12f3b1d1272d958bb79c194508d2aec85160c25846f0016ca861a064316b213a565cf53b81a513047f89cf877803 + languageName: node + linkType: hard + "clone@npm:^1.0.2": version: 1.0.4 resolution: "clone@npm:1.0.4" @@ -12246,7 +12220,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:2, commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.7.1, commander@npm:^2.8.1, commander@npm:^2.9.0": +"commander@npm:2, commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.7.1, commander@npm:^2.9.0": version: 2.20.3 resolution: "commander@npm:2.20.3" checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e @@ -12415,7 +12389,7 @@ __metadata: languageName: node linkType: hard -"concat-stream@npm:^1.4.6, concat-stream@npm:^1.5.0": +"concat-stream@npm:^1.5.0": version: 1.6.2 resolution: "concat-stream@npm:1.6.2" dependencies: @@ -14131,7 +14105,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.1.1, debug@npm:^2.2.0, debug@npm:^2.3.3, debug@npm:^2.6.0": +"debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.3.3, debug@npm:^2.6.0": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -14140,7 +14114,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.3.2, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.2.0, debug@npm:^4.3.1, debug@npm:^4.3.2": +"debug@npm:4, debug@npm:4.3.2, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.2.0, debug@npm:^4.3.1, debug@npm:^4.3.2": version: 4.3.2 resolution: "debug@npm:4.3.2" dependencies: @@ -14571,16 +14545,6 @@ __metadata: languageName: node linkType: hard -"doctrine@npm:^1.2.2": - version: 1.5.0 - resolution: "doctrine@npm:1.5.0" - dependencies: - esutils: ^2.0.2 - isarray: ^1.0.0 - checksum: 7ce8102a05cbb9d942d49db5461d2f3dd1208ebfed929bf1c04770a1ef6ef540b792e63c45eae4c51f8b16075e0af4a73581a06bad31c37ceb0988f2e398509b - languageName: node - linkType: hard - "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -14687,7 +14651,7 @@ __metadata: languageName: node linkType: hard -"domelementtype@npm:1": +"domelementtype@npm:1, domelementtype@npm:^1.3.1": version: 1.3.1 resolution: "domelementtype@npm:1.3.1" checksum: 7893da40218ae2106ec6ffc146b17f203487a52f5228b032ea7aa470e41dfe03e1bd762d0ee0139e792195efda765434b04b43cddcf63207b098f6ae44b36ad6 @@ -14719,6 +14683,15 @@ __metadata: languageName: node linkType: hard +"domhandler@npm:^2.3.0": + version: 2.4.2 + resolution: "domhandler@npm:2.4.2" + dependencies: + domelementtype: 1 + checksum: 49bd70c9c784f845cd047e1dfb3611bd10891c05719acfc93f01fc726a419ed09fbe0b69f9064392d556a63fffc5a02010856cedae9368f4817146d95a97011f + languageName: node + linkType: hard + "domhandler@npm:^4.0.0, domhandler@npm:^4.2.0": version: 4.2.2 resolution: "domhandler@npm:4.2.2" @@ -14728,7 +14701,7 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^1.7.0": +"domutils@npm:^1.5.1, domutils@npm:^1.7.0": version: 1.7.0 resolution: "domutils@npm:1.7.0" dependencies: @@ -15095,6 +15068,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^1.1.1": + version: 1.1.2 + resolution: "entities@npm:1.1.2" + checksum: d537b02799bdd4784ffd714d000597ed168727bddf4885da887c5a491d735739029a00794f1998abbf35f3f6aeda32ef5c15010dca1817d401903a501b6d3e05 + languageName: node + linkType: hard + "entities@npm:^2.0.0": version: 2.2.0 resolution: "entities@npm:2.2.0" @@ -15295,7 +15275,7 @@ __metadata: languageName: node linkType: hard -"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.46, es5-ext@npm:^0.10.50, es5-ext@npm:~0.10.14": +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.50": version: 0.10.53 resolution: "es5-ext@npm:0.10.53" dependencies: @@ -15313,7 +15293,7 @@ __metadata: languageName: node linkType: hard -"es6-iterator@npm:^2.0.3, es6-iterator@npm:~2.0.1, es6-iterator@npm:~2.0.3": +"es6-iterator@npm:~2.0.3": version: 2.0.3 resolution: "es6-iterator@npm:2.0.3" dependencies: @@ -15324,20 +15304,6 @@ __metadata: languageName: node linkType: hard -"es6-map@npm:^0.1.3": - version: 0.1.5 - resolution: "es6-map@npm:0.1.5" - dependencies: - d: 1 - es5-ext: ~0.10.14 - es6-iterator: ~2.0.1 - es6-set: ~0.1.5 - es6-symbol: ~3.1.1 - event-emitter: ~0.3.5 - checksum: 124c4f61be1a6d3378a22950f7548dc4b1d3b6d736a80f9c96d763e4119df962315879045d4b5f8e54d645cbed5e2d742aac2211b43ee16d97f06a357d81b162 - languageName: node - linkType: hard - "es6-promise@npm:4.2.8": version: 4.2.8 resolution: "es6-promise@npm:4.2.8" @@ -15345,19 +15311,6 @@ __metadata: languageName: node linkType: hard -"es6-set@npm:~0.1.5": - version: 0.1.5 - resolution: "es6-set@npm:0.1.5" - dependencies: - d: 1 - es5-ext: ~0.10.14 - es6-iterator: ~2.0.1 - es6-symbol: 3.1.1 - event-emitter: ~0.3.5 - checksum: 8f205eb5eacfee8fbb2c70a8b8f988537d6fd4e16ab6d43511b736f5be5ae4d24b16b04acc0b1afd2f9bdb7e536d90a92875904c16590e28fcc18ca985a09f64 - languageName: node - linkType: hard - "es6-shim@npm:0.35.5": version: 0.35.5 resolution: "es6-shim@npm:0.35.5" @@ -15372,17 +15325,7 @@ __metadata: languageName: node linkType: hard -"es6-symbol@npm:3.1.1": - version: 3.1.1 - resolution: "es6-symbol@npm:3.1.1" - dependencies: - d: 1 - es5-ext: ~0.10.14 - checksum: 0aca3bfe44d90a77f4f76588b41de5267956d0bdbf2b57120da27314f14dab35dd07cb4188cbae879e1aa1a1e1b0d0d2e2006466738bef808eef58b2c50adc99 - languageName: node - linkType: hard - -"es6-symbol@npm:^3.1.1, es6-symbol@npm:~3.1.1, es6-symbol@npm:~3.1.3": +"es6-symbol@npm:^3.1.1, es6-symbol@npm:~3.1.3": version: 3.1.3 resolution: "es6-symbol@npm:3.1.3" dependencies: @@ -15402,18 +15345,6 @@ __metadata: languageName: node linkType: hard -"es6-weak-map@npm:^2.0.1": - version: 2.0.3 - resolution: "es6-weak-map@npm:2.0.3" - dependencies: - d: 1 - es5-ext: ^0.10.46 - es6-iterator: ^2.0.3 - es6-symbol: ^3.1.1 - checksum: 19ca15f46d50948ce78c2da5f21fb5b1ef45addd4fe17b5df952ff1f2a3d6ce4781249bc73b90995257264be2a98b2ec749bb2aba0c14b5776a1154178f9c927 - languageName: node - linkType: hard - "escalade@npm:^3.0.2, escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -15487,18 +15418,6 @@ __metadata: languageName: node linkType: hard -"escope@npm:^3.6.0": - version: 3.6.0 - resolution: "escope@npm:3.6.0" - dependencies: - es6-map: ^0.1.3 - es6-weak-map: ^2.0.1 - esrecurse: ^4.1.0 - estraverse: ^4.1.1 - checksum: f11c006436bb15f7886160032ad222266ea1643e177b0bc57ec48f1e25920ef62d40511ca095c8733bbd6e8704bc431ce02557fc8229292427a079e62c896f66 - languageName: node - linkType: hard - "eslint-config-prettier@npm:7.2.0": version: 7.2.0 resolution: "eslint-config-prettier@npm:7.2.0" @@ -15726,49 +15645,6 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^2.7.0": - version: 2.13.1 - resolution: "eslint@npm:2.13.1" - dependencies: - chalk: ^1.1.3 - concat-stream: ^1.4.6 - debug: ^2.1.1 - doctrine: ^1.2.2 - es6-map: ^0.1.3 - escope: ^3.6.0 - espree: ^3.1.6 - estraverse: ^4.2.0 - esutils: ^2.0.2 - file-entry-cache: ^1.1.1 - glob: ^7.0.3 - globals: ^9.2.0 - ignore: ^3.1.2 - imurmurhash: ^0.1.4 - inquirer: ^0.12.0 - is-my-json-valid: ^2.10.0 - is-resolvable: ^1.0.0 - js-yaml: ^3.5.1 - json-stable-stringify: ^1.0.0 - levn: ^0.3.0 - lodash: ^4.0.0 - mkdirp: ^0.5.0 - optionator: ^0.8.1 - path-is-absolute: ^1.0.0 - path-is-inside: ^1.0.1 - pluralize: ^1.2.1 - progress: ^1.1.8 - require-uncached: ^1.0.2 - shelljs: ^0.6.0 - strip-json-comments: ~1.0.1 - table: ^3.7.8 - text-table: ~0.2.0 - user-home: ^2.0.0 - bin: - eslint: ./bin/eslint.js - checksum: 93ba7fbfe682967008a3c4797719801e50fa9cd7746cedd98b718f7c7742269d7d782ea0634691479a34fea8a9919a1b21e2bd7a2d4f7cb7ff4a3e1b0d22efd4 - languageName: node - linkType: hard - "esm@npm:^3.2.25": version: 3.2.25 resolution: "esm@npm:3.2.25" @@ -15776,16 +15652,6 @@ __metadata: languageName: node linkType: hard -"espree@npm:^3.1.6": - version: 3.5.4 - resolution: "espree@npm:3.5.4" - dependencies: - acorn: ^5.5.0 - acorn-jsx: ^3.0.0 - checksum: cbc8da4cafcc45f802fc561adece3a74d86f7bfbc2434bca34fadcf9dabb3337c25d2d202e4e1a4ff73b0105e887945fb75c375081067fc23ecaae86f5cb6400 - languageName: node - linkType: hard - "espree@npm:^7.3.0, espree@npm:^7.3.1": version: 7.3.1 resolution: "espree@npm:7.3.1" @@ -15906,16 +15772,6 @@ __metadata: languageName: node linkType: hard -"event-emitter@npm:~0.3.5": - version: 0.3.5 - resolution: "event-emitter@npm:0.3.5" - dependencies: - d: 1 - es5-ext: ~0.10.14 - checksum: 27c1399557d9cd7e0aa0b366c37c38a4c17293e3a10258e8b692a847dd5ba9fb90429c3a5a1eeff96f31f6fa03ccbd31d8ad15e00540b22b22f01557be706030 - languageName: node - linkType: hard - "eventemitter2@npm:^6.4.3": version: 6.4.5 resolution: "eventemitter2@npm:6.4.5" @@ -16044,6 +15900,15 @@ __metadata: languageName: node linkType: hard +"execall@npm:^2.0.0": + version: 2.0.0 + resolution: "execall@npm:2.0.0" + dependencies: + clone-regexp: ^2.1.0 + checksum: d98ee3e33f6c9001e80970e927fb9f16c6a121d5e250b2f4d6764d4157974f58cbe88613bbf073db05d5342677012002c5de956f4f0c32d10d092b6ff03a085c + languageName: node + linkType: hard + "executable@npm:^4.1.1": version: 4.1.1 resolution: "executable@npm:4.1.1" @@ -16060,13 +15925,6 @@ __metadata: languageName: node linkType: hard -"exit-hook@npm:^1.0.0": - version: 1.1.1 - resolution: "exit-hook@npm:1.1.1" - checksum: 1b4f16da7c202cd336ca07acb052922639182b4e2f1ad4007ed481bb774ce93469f505dec1371d9cd580ac54146a9fd260f053b0e4a48fa87c49fa3dc4a3f144 - languageName: node - linkType: hard - "exit@npm:^0.1.2": version: 0.1.2 resolution: "exit@npm:0.1.2" @@ -16437,7 +16295,7 @@ __metadata: languageName: node linkType: hard -"figures@npm:^1.3.5, figures@npm:^1.7.0": +"figures@npm:^1.7.0": version: 1.7.0 resolution: "figures@npm:1.7.0" dependencies: @@ -16465,16 +16323,6 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^1.1.1": - version: 1.3.1 - resolution: "file-entry-cache@npm:1.3.1" - dependencies: - flat-cache: ^1.2.1 - object-assign: ^4.0.1 - checksum: d90812a8902e5a586cc9e5f0a4e77ab40d4f69f2464215ee69a6288cd872400580f1490f4f64b1eb84896b3ca322dc0a76d2ead7a851b7bc4348e6833af9534f - languageName: node - linkType: hard - "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -16675,18 +16523,6 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^1.2.1": - version: 1.3.4 - resolution: "flat-cache@npm:1.3.4" - dependencies: - circular-json: ^0.3.1 - graceful-fs: ^4.1.2 - rimraf: ~2.6.2 - write: ^0.2.1 - checksum: 95605618db585e09881579b28d7b7e53215654451103425d1eb3fde2427ede7d71abf791f624c8c24d9dc0f7a4a3a3fa4ce8d146c846a31aa3b089380a89b1f3 - languageName: node - linkType: hard - "flat-cache@npm:^3.0.4": version: 3.0.4 resolution: "flat-cache@npm:3.0.4" @@ -16908,15 +16744,6 @@ __metadata: languageName: node linkType: hard -"front-matter@npm:2.1.2": - version: 2.1.2 - resolution: "front-matter@npm:2.1.2" - dependencies: - js-yaml: ^3.4.6 - checksum: 0ea88921f85b469357c9a3a93f858c2b5007c35f72970d60b4dfa8279190bb142f7323f9a96397690baf56606f466eb3fd0f4c7365c10955abaccb4958a47aa3 - languageName: node - linkType: hard - "fs-constants@npm:^1.0.0": version: 1.0.0 resolution: "fs-constants@npm:1.0.0" @@ -16959,17 +16786,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^3.0.1": - version: 3.0.1 - resolution: "fs-extra@npm:3.0.1" - dependencies: - graceful-fs: ^4.1.2 - jsonfile: ^3.0.0 - universalify: ^0.1.0 - checksum: 8957f9ee33a032b12f786158077dbd2a6b3b843449b36ce37bb3922200bbf12f0412aaebe10e3ce3e46e1f0dd37904e4053b4cfa2a717c80eca3af6dc840ba8b - languageName: node - linkType: hard - "fs-extra@npm:^9.0.0, fs-extra@npm:^9.0.1, fs-extra@npm:^9.1.0": version: 9.1.0 resolution: "fs-extra@npm:9.1.0" @@ -17165,24 +16981,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"generate-function@npm:^2.0.0": - version: 2.3.1 - resolution: "generate-function@npm:2.3.1" - dependencies: - is-property: ^1.0.2 - checksum: 652f083de206ead2bae4caf9c7eeb465e8d98c0b8ed2a29c6afc538cef0785b5c6eea10548f1e13cc586d3afd796c13c830c2cb3dc612ec2457b2aadda5f57c9 - languageName: node - linkType: hard - -"generate-object-property@npm:^1.1.0": - version: 1.2.0 - resolution: "generate-object-property@npm:1.2.0" - dependencies: - is-property: ^1.0.0 - checksum: 5141ca5fd545f0aabd24fd13f9f3ecf9cfea2255db00d46e282d65141d691d560c70b6361c3c0c4982f86f600361925bfd4773e0350c66d0210e6129ae553a09 - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.1, gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -17282,6 +17080,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"get-stdin@npm:^8.0.0": + version: 8.0.0 + resolution: "get-stdin@npm:8.0.0" + checksum: 40128b6cd25781ddbd233344f1a1e4006d4284906191ed0a7d55ec2c1a3e44d650f280b2c9eeab79c03ac3037da80257476c0e4e5af38ddfb902d6ff06282d77 + languageName: node + linkType: hard + "get-stream@npm:^3.0.0": version: 3.0.0 resolution: "get-stream@npm:3.0.0" @@ -17525,7 +17330,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.0.3, glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": +"glob@npm:^7.0.3, glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.0 resolution: "glob@npm:7.2.0" dependencies: @@ -17571,7 +17376,7 @@ fsevents@~2.1.2: 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: @@ -17626,13 +17431,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"globals@npm:^9.2.0": - version: 9.18.0 - resolution: "globals@npm:9.18.0" - checksum: e9c066aecfdc5ea6f727344a4246ecc243aaf66ede3bffee10ddc0c73351794c25e727dd046090dcecd821199a63b9de6af299a6e3ba292c8b22f0a80ea32073 - languageName: node - linkType: hard - "globalthis@npm:^1.0.0": version: 1.0.2 resolution: "globalthis@npm:1.0.2" @@ -17745,6 +17543,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"globjoin@npm:^0.1.4": + version: 0.1.4 + resolution: "globjoin@npm:0.1.4" + checksum: 0a47d88d566122d9e42da946453ee38b398e0021515ac6a95d13f980ba8c1e42954e05ee26cfcbffce1ac1ee094d0524b16ce1dd874ca52408d6db5c6d39985b + languageName: node + linkType: hard + "globule@npm:^1.0.0": version: 1.3.3 resolution: "globule@npm:1.3.3" @@ -17756,14 +17561,14 @@ fsevents@~2.1.2: languageName: node linkType: hard -"gonzales-pe-sl@npm:^4.2.3": - version: 4.2.3 - resolution: "gonzales-pe-sl@npm:4.2.3" +"gonzales-pe@npm:^4.3.0": + version: 4.3.0 + resolution: "gonzales-pe@npm:4.3.0" dependencies: - minimist: 1.1.x + minimist: ^1.2.5 bin: - gonzales: ./bin/gonzales.js - checksum: 38098a36a254128fa1e59a1e19b36ae4c3e84edfe4db3788a423e95f54ceb7cbc6f11928ec2d94c171c09b231e25b91ddcd6c3fd25763329dd83aced602dc262 + gonzales: bin/gonzales.js + checksum: 49d60fc49ad35639e5d55923c1516d3ec2e4de5e6e5913ec3458a479b66623e54a060d568295349b0bb9f96ee970c473ff984d4b82a5cfeaf736c55f0d6dc3b7 languageName: node linkType: hard @@ -17845,7 +17650,7 @@ fsevents@~2.1.2: "@sentry/utils": 6.13.2 "@testing-library/jest-dom": 5.11.5 "@testing-library/react": 12.1.2 - "@testing-library/react-hooks": ^3.2.1 + "@testing-library/react-hooks": 7.0.2 "@testing-library/user-event": 13.3.0 "@types/angular": 1.6.56 "@types/angular-route": 1.7.0 @@ -17876,7 +17681,7 @@ fsevents@~2.1.2: "@types/rc-time-picker": ^3 "@types/react": 17.0.30 "@types/react-beautiful-dnd": 13.1.1 - "@types/react-dom": 16.9.9 + "@types/react-dom": 17.0.10 "@types/react-grid-layout": 1.1.1 "@types/react-highlight-words": ^0.16.2 "@types/react-loadable": 5.5.2 @@ -18047,7 +17852,6 @@ fsevents@~2.1.2: rxjs: 7.3.0 rxjs-spy: 8.0.0 sass: 1.32.13 - sass-lint: 1.12.1 sass-loader: 12.1.0 search-query-parser: 1.5.4 selecto: 1.13.0 @@ -18056,6 +17860,9 @@ fsevents@~2.1.2: slate: 0.47.8 slate-plain-serializer: 0.7.10 style-loader: 3.3.0 + stylelint: 13.13.1 + stylelint-config-prettier: 9.0.3 + stylelint-config-sass-guidelines: 8.0.0 terser-webpack-plugin: 5.2.4 testing-library-selector: ^0.1.3 tether: 1.4.7 @@ -18698,6 +18505,20 @@ fsevents@~2.1.2: languageName: node linkType: hard +"htmlparser2@npm:^3.10.0": + version: 3.10.1 + resolution: "htmlparser2@npm:3.10.1" + dependencies: + domelementtype: ^1.3.1 + domhandler: ^2.3.0 + domutils: ^1.5.1 + entities: ^1.1.1 + inherits: ^2.0.1 + readable-stream: ^3.1.1 + checksum: 6875f7dd875aa10be17d9b130e3738cd8ed4010b1f2edaf4442c82dfafe9d9336b155870dcc39f38843cbf7fef5e4fcfdf0c4c1fd4db3a1b91a1e0ee8f6c3475 + languageName: node + linkType: hard + "htmlparser2@npm:^6.1.0": version: 6.1.0 resolution: "htmlparser2@npm:6.1.0" @@ -18991,7 +18812,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"ignore@npm:^3.1.2, ignore@npm:^3.3.5": +"ignore@npm:^3.3.5": version: 3.3.10 resolution: "ignore@npm:3.3.10" checksum: 23e8cc776e367b56615ab21b78decf973a35dfca5522b39d9b47643d8168473b0d1f18dd1321a1bab466a12ea11a2411903f3b21644f4d5461ee0711ec8678bd @@ -19005,7 +18826,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"ignore@npm:^5.1.1, ignore@npm:^5.1.4": +"ignore@npm:^5.1.1, ignore@npm:^5.1.4, ignore@npm:^5.1.8": version: 5.1.8 resolution: "ignore@npm:5.1.8" checksum: 967abadb61e2cb0e5c5e8c4e1686ab926f91bc1a4680d994b91947d3c65d04c3ae126dcdf67f08e0feeb8ff8407d453e641aeeddcc47a3a3cca359f283cf6121 @@ -19087,7 +18908,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"import-lazy@npm:~4.0.0": +"import-lazy@npm:^4.0.0, import-lazy@npm:~4.0.0": version: 4.0.0 resolution: "import-lazy@npm:4.0.0" checksum: 22f5e51702134aef78890156738454f620e5fe7044b204ebc057c614888a1dd6fdf2ede0fdcca44d5c173fd64f65c985f19a51775b06967ef58cc3d26898df07 @@ -19224,27 +19045,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"inquirer@npm:^0.12.0": - version: 0.12.0 - resolution: "inquirer@npm:0.12.0" - dependencies: - ansi-escapes: ^1.1.0 - ansi-regex: ^2.0.0 - chalk: ^1.0.0 - cli-cursor: ^1.0.1 - cli-width: ^2.0.0 - figures: ^1.3.5 - lodash: ^4.3.0 - readline2: ^1.0.1 - run-async: ^0.1.0 - rx-lite: ^3.1.2 - string-width: ^1.0.1 - strip-ansi: ^3.0.0 - through: ^2.3.6 - checksum: 4f3b0af69b6bb3757b4e521fa6a1558a065cb90cfa7707262958fddc2e24686b49d1fbc857c3cab71e763ba722918646954a54526dc267d1340a402f26449ed3 - languageName: node - linkType: hard - "inquirer@npm:^6.3.1": version: 6.5.2 resolution: "inquirer@npm:6.5.2" @@ -19843,26 +19643,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"is-my-ip-valid@npm:^1.0.0": - version: 1.0.0 - resolution: "is-my-ip-valid@npm:1.0.0" - checksum: d16168b4ad8ae3809131928af557dcec93a9fc772263560d1b681730194b77f52333e20a6279727fb70516c7025dea2ee8fedf35b93152c2d76d196ec0413fa6 - languageName: node - linkType: hard - -"is-my-json-valid@npm:^2.10.0": - version: 2.20.5 - resolution: "is-my-json-valid@npm:2.20.5" - dependencies: - generate-function: ^2.0.0 - generate-object-property: ^1.1.0 - is-my-ip-valid: ^1.0.0 - jsonpointer: ^4.0.0 - xtend: ^4.0.0 - checksum: a12b338c593eb518ac430bcb7844a49d015f635f96cc66077a2404a24f4edd795c6804b884dd1e4eb61817ce559286979f0fa48ffd57e5bb7cb88287dcc03ba7 - languageName: node - linkType: hard - "is-negative-zero@npm:^2.0.0, is-negative-zero@npm:^2.0.1": version: 2.0.1 resolution: "is-negative-zero@npm:2.0.1" @@ -20020,13 +19800,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"is-property@npm:^1.0.0, is-property@npm:^1.0.2": - version: 1.0.2 - resolution: "is-property@npm:1.0.2" - checksum: 33b661a3690bcc88f7e47bb0a21b9e3187e76a317541ea7ec5e8096d954f441b77a46d8930c785f7fbf4ef8dfd624c25495221e026e50f74c9048fe501773be5 - languageName: node - linkType: hard - "is-redirect@npm:^1.0.0": version: 1.0.0 resolution: "is-redirect@npm:1.0.0" @@ -20060,6 +19833,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"is-regexp@npm:^2.0.0": + version: 2.1.0 + resolution: "is-regexp@npm:2.1.0" + checksum: 502f8e09faddc2e360350d3fa88dfb3af47b3c8e0bea1d0fe9903a1265cb199547cc11c99e9ee27cb010f678f6b48e52e92273860b68f6339e463e034f21859c + languageName: node + linkType: hard + "is-resolvable@npm:^1.0.0, is-resolvable@npm:^1.1.0": version: 1.1.0 resolution: "is-resolvable@npm:1.1.0" @@ -21131,7 +20911,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"js-yaml@npm:^3.13.1, js-yaml@npm:^3.4.6, js-yaml@npm:^3.5.1, js-yaml@npm:^3.5.4": +"js-yaml@npm:^3.13.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" dependencies: @@ -21338,15 +21118,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"json-stable-stringify@npm:^1.0.0, json-stable-stringify@npm:^1.0.1": - version: 1.0.1 - resolution: "json-stable-stringify@npm:1.0.1" - dependencies: - jsonify: ~0.0.0 - checksum: 65d6cbf0fca72a4136999f65f4401cf39a129f7aeff0fdd987ac3d3423a2113659294045fb8377e6e20d865cac32b1b8d70f3d87346c9786adcee60661d96ca5 - languageName: node - linkType: hard - "json-stringify-pretty-compact@npm:^2.0.0": version: 2.0.0 resolution: "json-stringify-pretty-compact@npm:2.0.0" @@ -21404,18 +21175,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"jsonfile@npm:^3.0.0": - version: 3.0.1 - resolution: "jsonfile@npm:3.0.1" - dependencies: - graceful-fs: ^4.1.6 - dependenciesMeta: - graceful-fs: - optional: true - checksum: f2935da339462fe6489c3b8961b637e4eeebd42bcbbe1c8d88f4e937fe19d2d9bc222167281ada2e2f6ddc0324edb43b18107a9b12c743b350326d83ba4db5ef - languageName: node - linkType: hard - "jsonfile@npm:^4.0.0": version: 4.0.0 resolution: "jsonfile@npm:4.0.0" @@ -21441,13 +21200,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"jsonify@npm:~0.0.0": - version: 0.0.0 - resolution: "jsonify@npm:0.0.0" - checksum: d8d4ed476c116e6987a460dcb82f22284686caae9f498ac87b0502c1765ac1522f4f450a4cad4cc368d202fd3b27a3860735140a82867fc6d558f5f199c38bce - languageName: node - linkType: hard - "jsonparse@npm:^1.2.0, jsonparse@npm:^1.3.1": version: 1.3.1 resolution: "jsonparse@npm:1.3.1" @@ -21455,13 +21207,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"jsonpointer@npm:^4.0.0": - version: 4.1.0 - resolution: "jsonpointer@npm:4.1.0" - checksum: ffc3e8937380989934676b339718d3213ecf5f6b7ce637b1ce5669a22f45dc61a86463e28abbe8c743d62f87ae790253c50cce0f586cb8e7623a21a7f811a444 - languageName: node - linkType: hard - "jsprim@npm:^1.2.2": version: 1.4.1 resolution: "jsprim@npm:1.4.1" @@ -21595,10 +21340,10 @@ fsevents@~2.1.2: languageName: node linkType: hard -"known-css-properties@npm:^0.3.0": - version: 0.3.0 - resolution: "known-css-properties@npm:0.3.0" - checksum: c16fb0e20021d79eac25ea0ba05fdb44d3207e9340235f3391163ef67ca08468582ddf3763c3b05f289fdeee46a5cb9146f54506686613dd084ae5ae5090b66e +"known-css-properties@npm:^0.21.0": + version: 0.21.0 + resolution: "known-css-properties@npm:0.21.0" + checksum: 28a47943cdeb04bf1690d013e732743b855bb21ae4290afeb34fb0b251c2f75b901bb9f2c92a919fa6cdbe8186827528b47569f660143b4932b42423bf90a628 languageName: node linkType: hard @@ -21740,16 +21485,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"levn@npm:^0.3.0, levn@npm:~0.3.0": - version: 0.3.0 - resolution: "levn@npm:0.3.0" - dependencies: - prelude-ls: ~1.1.2 - type-check: ~0.3.2 - checksum: 0d084a524231a8246bb10fec48cdbb35282099f6954838604f3c7fc66f2e16fa66fd9cc2f3f20a541a113c4dafdf181e822c887c8a319c9195444e6c64ac395e - languageName: node - linkType: hard - "levn@npm:^0.4.1": version: 0.4.1 resolution: "levn@npm:0.4.1" @@ -21760,6 +21495,16 @@ fsevents@~2.1.2: languageName: node linkType: hard +"levn@npm:~0.3.0": + version: 0.3.0 + resolution: "levn@npm:0.3.0" + dependencies: + prelude-ls: ~1.1.2 + type-check: ~0.3.2 + checksum: 0d084a524231a8246bb10fec48cdbb35282099f6954838604f3c7fc66f2e16fa66fd9cc2f3f20a541a113c4dafdf181e822c887c8a319c9195444e6c64ac395e + languageName: node + linkType: hard + "lezer-promql@npm:0.20.0": version: 0.20.0 resolution: "lezer-promql@npm:0.20.0" @@ -22035,13 +21780,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"lodash.capitalize@npm:^4.1.0": - version: 4.2.1 - resolution: "lodash.capitalize@npm:4.2.1" - checksum: d9195f31d48c105206f1099946d8bbc8ab71435bc1c8708296992a31a992bb901baf120fdcadd773098ac96e62a79e6b023ee7d26a2deb0d6c6aada930e6ad0a - languageName: node - linkType: hard - "lodash.clonedeep@npm:^4.5.0": version: 4.5.0 resolution: "lodash.clonedeep@npm:4.5.0" @@ -22119,13 +21857,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"lodash.kebabcase@npm:^4.0.0": - version: 4.1.1 - resolution: "lodash.kebabcase@npm:4.1.1" - checksum: 5a6c59161914e1bae23438a298c7433e83d935e0f59853fa862e691164696bc07f6dfa4c313d499fbf41ba8d53314e9850416502376705a357d24ee6ca33af78 - languageName: node - linkType: hard - "lodash.memoize@npm:4.x, lodash.memoize@npm:^4.1.2": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" @@ -22194,7 +21925,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4, lodash@npm:^4.0.0, lodash@npm:^4.1.1, lodash@npm:^4.17.10, lodash@npm:^4.17.12, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.3, lodash@npm:^4.17.4, lodash@npm:^4.17.5, lodash@npm:^4.3.0, lodash@npm:^4.7.0, lodash@npm:~4.17.10, lodash@npm:~4.17.15, lodash@npm:~4.17.21": +"lodash@npm:4.17.21, lodash@npm:^4, lodash@npm:^4.0.0, lodash@npm:^4.1.1, lodash@npm:^4.17.10, lodash@npm:^4.17.12, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.3, lodash@npm:^4.17.4, lodash@npm:^4.17.5, lodash@npm:^4.7.0, lodash@npm:~4.17.10, lodash@npm:~4.17.15, lodash@npm:~4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -22228,7 +21959,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"log-symbols@npm:^4.0.0": +"log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" dependencies: @@ -22310,6 +22041,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"longest-streak@npm:^2.0.0": + version: 2.0.4 + resolution: "longest-streak@npm:2.0.4" + checksum: 28b8234a14963002c5c71035dee13a0a11e9e9d18ffa320fdc8796ed7437399204495702ed69cd2a7087b0af041a2a8b562829b7c1e2042e73a3374d1ecf6580 + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -22595,6 +22333,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"mathml-tag-names@npm:^2.1.3": + version: 2.1.3 + resolution: "mathml-tag-names@npm:2.1.3" + checksum: 1201a25a137d6b9e328facd67912058b8b45b19a6c4cc62641c9476195da28a275ca6e0eca070af5378b905c2b11abc1114676ba703411db0b9ce007de921ad0 + languageName: node + linkType: hard + "md5-file@npm:^4.0.0": version: 4.0.0 resolution: "md5-file@npm:4.0.0" @@ -22633,6 +22378,19 @@ fsevents@~2.1.2: languageName: node linkType: hard +"mdast-util-from-markdown@npm:^0.8.0": + version: 0.8.5 + resolution: "mdast-util-from-markdown@npm:0.8.5" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-to-string: ^2.0.0 + micromark: ~2.11.0 + parse-entities: ^2.0.0 + unist-util-stringify-position: ^2.0.0 + checksum: 5a9d0d753a42db763761e874c22365d0c7c9934a5a18b5ff76a0643610108a208a041ffdb2f3d3dd1863d3d915225a4020a0aade282af0facfd0df110601eee6 + languageName: node + linkType: hard + "mdast-util-to-hast@npm:10.0.1": version: 10.0.1 resolution: "mdast-util-to-hast@npm:10.0.1" @@ -22649,6 +22407,20 @@ fsevents@~2.1.2: languageName: node linkType: hard +"mdast-util-to-markdown@npm:^0.6.0": + version: 0.6.5 + resolution: "mdast-util-to-markdown@npm:0.6.5" + dependencies: + "@types/unist": ^2.0.0 + longest-streak: ^2.0.0 + mdast-util-to-string: ^2.0.0 + parse-entities: ^2.0.0 + repeat-string: ^1.0.0 + zwitch: ^1.0.0 + checksum: 7ebc47533bff6e8669f85ae124dc521ea570e9df41c0d9e4f0f43c19ef4a8c9928d741f3e4afa62fcca1927479b714582ff5fd684ef240d84ee5b75ab9d863cf + languageName: node + linkType: hard + "mdast-util-to-string@npm:^1.0.0": version: 1.1.0 resolution: "mdast-util-to-string@npm:1.1.0" @@ -22656,6 +22428,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"mdast-util-to-string@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-to-string@npm:2.0.0" + checksum: 0b2113ada10e002fbccb014170506dabe2f2ddacaacbe4bc1045c33f986652c5a162732a2c057c5335cdb58419e2ad23e368e5be226855d4d4e280b81c4e9ec2 + languageName: node + linkType: hard + "mdn-data@npm:2.0.14": version: 2.0.14 resolution: "mdn-data@npm:2.0.14" @@ -22783,6 +22562,26 @@ fsevents@~2.1.2: languageName: node linkType: hard +"meow@npm:^9.0.0": + version: 9.0.0 + resolution: "meow@npm:9.0.0" + dependencies: + "@types/minimist": ^1.2.0 + camelcase-keys: ^6.2.2 + decamelize: ^1.2.0 + decamelize-keys: ^1.1.0 + hard-rejection: ^2.1.0 + minimist-options: 4.1.0 + normalize-package-data: ^3.0.0 + read-pkg-up: ^7.0.1 + redent: ^3.0.0 + trim-newlines: ^3.0.0 + type-fest: ^0.18.0 + yargs-parser: ^20.2.3 + checksum: 99799c47247f4daeee178e3124f6ef6f84bde2ba3f37652865d5d8f8b8adcf9eedfc551dd043e2455cd8206545fd848e269c0c5ab6b594680a0ad4d3617c9639 + languageName: node + linkType: hard + "merge-class-names@npm:^1.1.1": version: 1.4.2 resolution: "merge-class-names@npm:1.4.2" @@ -22811,13 +22610,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"merge@npm:^1.2.0": - version: 1.2.1 - resolution: "merge@npm:1.2.1" - checksum: 2298c4fdcf64561f320b92338681f7ffcafafb579a6e294066ae3e7bd10ae25df363903d2f028072733b9f79a1f75d2b999aef98ad5d73de13641da39cda0913 - languageName: node - linkType: hard - "methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -22832,6 +22624,16 @@ fsevents@~2.1.2: languageName: node linkType: hard +"micromark@npm:~2.11.0": + version: 2.11.4 + resolution: "micromark@npm:2.11.4" + dependencies: + debug: ^4.0.0 + parse-entities: ^2.0.0 + checksum: f8a5477d394908a5d770227aea71657a76423d420227c67ea0699e659a5f62eb39d504c1f7d69ec525a6af5aaeb6a7bffcdba95614968c03d41d3851edecb0d6 + languageName: node + linkType: hard + "micromatch@npm:^3.1.10, micromatch@npm:^3.1.4": version: 3.1.10 resolution: "micromatch@npm:3.1.10" @@ -23043,13 +22845,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"minimist@npm:1.1.x": - version: 1.1.3 - resolution: "minimist@npm:1.1.3" - checksum: 78546d2b50c8060377f18e36520d6b101d4f1375b0ed256f3158bf9a39103046d9ff8b8a2843ec17b6c2764c3d15aec5d8a0dc509ac6746f7e40624d23c003b7 - languageName: node - linkType: hard - "minimist@npm:^1.1.1, minimist@npm:^1.2.0, minimist@npm:^1.2.5": version: 1.2.5 resolution: "minimist@npm:1.2.5" @@ -23222,7 +23017,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"mkdirp@npm:^0.5.0, mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.3, mkdirp@npm:^0.5.5, mkdirp@npm:~0.5.1": +"mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.3, mkdirp@npm:^0.5.5, mkdirp@npm:~0.5.1": version: 0.5.5 resolution: "mkdirp@npm:0.5.5" dependencies: @@ -23409,13 +23204,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"mute-stream@npm:0.0.5": - version: 0.0.5 - resolution: "mute-stream@npm:0.0.5" - checksum: 679c91ed82619e91382da17ace04dfd535d3b22d42c4d661161f980b252551c053175a238d76c16ea56cf38ae90c00ea60aebacfaf365bd45318ae581a12f042 - languageName: node - linkType: hard - "mute-stream@npm:0.0.7": version: 0.0.7 resolution: "mute-stream@npm:0.0.7" @@ -23884,6 +23672,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"normalize-selector@npm:^0.2.0": + version: 0.2.0 + resolution: "normalize-selector@npm:0.2.0" + checksum: 6cc88334df26cf1f809692892f4069e1112958574403d0a6753fe5b1e41707170e242e1602e21fa62ea92618827882c4d18a773bc99075a77553bd527eec9930 + languageName: node + linkType: hard + "normalize-url@npm:1.9.1": version: 1.9.1 resolution: "normalize-url@npm:1.9.1" @@ -24286,13 +24081,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"onetime@npm:^1.0.0": - version: 1.1.0 - resolution: "onetime@npm:1.1.0" - checksum: 4e9ab082cad172bd69c5f86630f55132c78e89e62b6e7abc5b4df922c3a5a397eeb88ad4810c8493a40a6ea5e54c146810ea8553db609903db3643985b301f67 - languageName: node - linkType: hard - "onetime@npm:^2.0.0": version: 2.0.1 resolution: "onetime@npm:2.0.1" @@ -25235,13 +25023,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"pluralize@npm:^1.2.1": - version: 1.2.1 - resolution: "pluralize@npm:1.2.1" - checksum: 81d77b2f4c6762f8cecf87b4de8f6a588f373783e689365f1e574ac7c336e634a3fce205c3d9686168cd6c3b667080cf5cbf2601e04692eae89dfc43b3ce2141 - languageName: node - linkType: hard - "pluralize@npm:^8.0.0": version: 8.0.0 resolution: "pluralize@npm:8.0.0" @@ -25645,6 +25426,18 @@ fsevents@~2.1.2: languageName: node linkType: hard +"postcss-html@npm:^0.36.0": + version: 0.36.0 + resolution: "postcss-html@npm:0.36.0" + dependencies: + htmlparser2: ^3.10.0 + peerDependencies: + postcss: ">=5.0.0" + postcss-syntax: ">=0.36.0" + checksum: 5f340df1d9e1595a6d0051cca408efa86efa77a51efe570ab4db6c463b05936f9582b143be8eedc3ba7fd3ed313f6a6838e11e31abcefc3543486b45ba3893e1 + languageName: node + linkType: hard + "postcss-image-set-function@npm:^3.0.1": version: 3.0.1 resolution: "postcss-image-set-function@npm:3.0.1" @@ -25675,6 +25468,15 @@ fsevents@~2.1.2: languageName: node linkType: hard +"postcss-less@npm:^3.1.4": + version: 3.1.4 + resolution: "postcss-less@npm:3.1.4" + dependencies: + postcss: ^7.0.14 + checksum: f18d002e114c62bbdc71c0cfa5723d725492301b5079311a531618390dfffbe12f544c3820be5bd9b1447100508187827944b78ff86e7b31a0737347fc8b9882 + languageName: node + linkType: hard + "postcss-load-config@npm:^2.0.0": version: 2.1.2 resolution: "postcss-load-config@npm:2.1.2" @@ -25745,6 +25547,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"postcss-media-query-parser@npm:^0.2.3": + version: 0.2.3 + resolution: "postcss-media-query-parser@npm:0.2.3" + checksum: 8000d4d95b912994928ff86137f5ab0ed4c4ee1498af2336e93d708ae8827a690cd7acbaed55d14684cf44d82c8d44b031c1c69ae6bcd2f9620ea67573888090 + languageName: node + linkType: hard + "postcss-merge-longhand@npm:^4.0.11": version: 4.0.11 resolution: "postcss-merge-longhand@npm:4.0.11" @@ -26373,6 +26182,41 @@ fsevents@~2.1.2: languageName: node linkType: hard +"postcss-resolve-nested-selector@npm:^0.1.1": + version: 0.1.1 + resolution: "postcss-resolve-nested-selector@npm:0.1.1" + checksum: b08fb76ab092a09ee01328bad620a01dcb445ac5eb02dd0ed9ed75217c2f779ecb3bf99a361c46e695689309c08c09f1a1ad7354c8d58c2c2c40d364657fcb08 + languageName: node + linkType: hard + +"postcss-safe-parser@npm:^4.0.2": + version: 4.0.2 + resolution: "postcss-safe-parser@npm:4.0.2" + dependencies: + postcss: ^7.0.26 + checksum: b812832c06f9fc17b74b714f9c07de80fa770a1535a103b06b679f33b8e09caf60dff1e1eca489613f4ce2bb6439cd949b7d026c843aa9b45bb50f0168b75023 + languageName: node + linkType: hard + +"postcss-sass@npm:^0.4.4": + version: 0.4.4 + resolution: "postcss-sass@npm:0.4.4" + dependencies: + gonzales-pe: ^4.3.0 + postcss: ^7.0.21 + checksum: d361114e5a6a6cc65db9ab71d2af2fe82df8876ce1135b6569498cbf4f3e303312edf430de925bd0d5b110f482ed55a44143da07621726cfdd07e71917390b58 + languageName: node + linkType: hard + +"postcss-scss@npm:^2.1.1": + version: 2.1.1 + resolution: "postcss-scss@npm:2.1.1" + dependencies: + postcss: ^7.0.6 + checksum: 61535f04652daed70c8ffa13589de81f4d9f607d87ccf1e2b494b0edfabc388853058229c8070f559503f4963e6dedc3690d4f587f4a034b7c23aa6fc03f251c + languageName: node + linkType: hard + "postcss-selector-matches@npm:^4.0.0": version: 4.0.0 resolution: "postcss-selector-matches@npm:4.0.0" @@ -26425,6 +26269,16 @@ fsevents@~2.1.2: languageName: node linkType: hard +"postcss-sorting@npm:^5.0.1": + version: 5.0.1 + resolution: "postcss-sorting@npm:5.0.1" + dependencies: + lodash: ^4.17.14 + postcss: ^7.0.17 + checksum: 0a410afab47e1ec10f1bd8768d7118cad93d00990a298816bb6c30f72f66ed05d29f152c8a04b943749fef134413171d6c91ef9915a1b95281573c1c7d13f76c + languageName: node + linkType: hard + "postcss-svgo@npm:^4.0.3": version: 4.0.3 resolution: "postcss-svgo@npm:4.0.3" @@ -26448,6 +26302,15 @@ fsevents@~2.1.2: languageName: node linkType: hard +"postcss-syntax@npm:^0.36.2": + version: 0.36.2 + resolution: "postcss-syntax@npm:0.36.2" + peerDependencies: + postcss: ">=5.0.0" + checksum: 812baee602910903b8b77391583721613951d87dbc8baff140879069ff98423392675c4ddfdf073418f4a699ee5d4dd020914bad07504c62f9f333211bf979b8 + languageName: node + linkType: hard + "postcss-unique-selectors@npm:^4.0.1": version: 4.0.1 resolution: "postcss-unique-selectors@npm:4.0.1" @@ -26508,7 +26371,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"postcss@npm:^7.0.0, postcss@npm:^7.0.1, postcss@npm:^7.0.14, postcss@npm:^7.0.17, postcss@npm:^7.0.2, postcss@npm:^7.0.23, postcss@npm:^7.0.26, postcss@npm:^7.0.27, postcss@npm:^7.0.32, postcss@npm:^7.0.36, postcss@npm:^7.0.5, postcss@npm:^7.0.6": +"postcss@npm:^7.0.0, postcss@npm:^7.0.1, postcss@npm:^7.0.14, postcss@npm:^7.0.17, postcss@npm:^7.0.2, postcss@npm:^7.0.21, postcss@npm:^7.0.23, postcss@npm:^7.0.26, postcss@npm:^7.0.27, postcss@npm:^7.0.31, postcss@npm:^7.0.32, postcss@npm:^7.0.35, postcss@npm:^7.0.36, postcss@npm:^7.0.5, postcss@npm:^7.0.6": version: 7.0.39 resolution: "postcss@npm:7.0.39" dependencies: @@ -26820,13 +26683,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"progress@npm:^1.1.8": - version: 1.1.8 - resolution: "progress@npm:1.1.8" - checksum: 789c824156e03a7353b190fc63da46bc42b210250cebaa2dca447a6a740f5469f34ed30f768cdef088ca720a8c3c42dd743958e8bc6a35b2d2a1a83171ad2c56 - languageName: node - linkType: hard - "progress@npm:^2.0.0, progress@npm:^2.0.1": version: 2.0.3 resolution: "progress@npm:2.0.3" @@ -27822,6 +27678,17 @@ fsevents@~2.1.2: languageName: node linkType: hard +"react-error-boundary@npm:^3.1.0": + version: 3.1.3 + resolution: "react-error-boundary@npm:3.1.3" + dependencies: + "@babel/runtime": ^7.12.5 + peerDependencies: + react: ">=16.13.1" + checksum: 0a05af404aa054c54d7bc65a1814810093bf136c3ad4b3576a51d8509ee8fc302adfb66405da501fc01e839db557dd0d994b487e651897e36293907bb61458cf + languageName: node + linkType: hard + "react-error-overlay@npm:^6.0.9": version: 6.0.9 resolution: "react-error-overlay@npm:6.0.9" @@ -28639,17 +28506,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"readline2@npm:^1.0.1": - version: 1.0.1 - resolution: "readline2@npm:1.0.1" - dependencies: - code-point-at: ^1.0.0 - is-fullwidth-code-point: ^1.0.0 - mute-stream: 0.0.5 - checksum: 7ac8ffa917af89f042bb24f695b1333158d83e26f398108f6d4ce7ca3ab6bccb6fa32623d9254ea1dc5420db7e6ce0b0fc527108645ababf6e280d8db3fe8a89 - languageName: node - linkType: hard - "realpath-native@npm:^1.1.0": version: 1.1.0 resolution: "realpath-native@npm:1.1.0" @@ -28970,6 +28826,15 @@ fsevents@~2.1.2: languageName: node linkType: hard +"remark-parse@npm:^9.0.0": + version: 9.0.0 + resolution: "remark-parse@npm:9.0.0" + dependencies: + mdast-util-from-markdown: ^0.8.0 + checksum: 50104880549639b7dd7ae6f1e23c214915fe9c054f02f3328abdaee3f6de6d7282bf4357c3c5b106958fe75e644a3c248c2197755df34f9955e8e028fc74868f + languageName: node + linkType: hard + "remark-slug@npm:^6.0.0": version: 6.1.0 resolution: "remark-slug@npm:6.1.0" @@ -28990,6 +28855,26 @@ fsevents@~2.1.2: languageName: node linkType: hard +"remark-stringify@npm:^9.0.0": + version: 9.0.1 + resolution: "remark-stringify@npm:9.0.1" + dependencies: + mdast-util-to-markdown: ^0.6.0 + checksum: 93f46076f4d96ab1946d13e7dd43e83088480ac6b1dfe05a65e2c2f0e33d1f52a50175199b464a81803fc0f5b3bf182037665f89720b30515eba37bec4d63d56 + languageName: node + linkType: hard + +"remark@npm:^13.0.0": + version: 13.0.0 + resolution: "remark@npm:13.0.0" + dependencies: + remark-parse: ^9.0.0 + remark-stringify: ^9.0.0 + unified: ^9.1.0 + checksum: e3432bfa1b0029680302e99a6356c08789b3e908457a71eca37ada6a58497e302f08bd5f62fbad840082a8348c181b7f6f981aaf3cd3112207583ddf793a2429 + languageName: node + linkType: hard + "remove-trailing-separator@npm:^1.0.1": version: 1.1.0 resolution: "remove-trailing-separator@npm:1.1.0" @@ -29017,7 +28902,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"repeat-string@npm:^1.5.4, repeat-string@npm:^1.6.1": +"repeat-string@npm:^1.0.0, repeat-string@npm:^1.5.4, repeat-string@npm:^1.6.1": version: 1.6.1 resolution: "repeat-string@npm:1.6.1" checksum: 1b809fc6db97decdc68f5b12c4d1a671c8e3f65ec4a40c238bc5200e44e85bcc52a54f78268ab9c29fcf5fe4f1343e805420056d1f30fa9a9ee4c2d93e3cc6c0 @@ -29133,16 +29018,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"require-uncached@npm:^1.0.2": - version: 1.0.3 - resolution: "require-uncached@npm:1.0.3" - dependencies: - caller-path: ^0.1.0 - resolve-from: ^1.0.0 - checksum: ace5261d38072130d1fefcfe9662b0d038fe1e38988a801be3e90fbfcab9f6786eeadcf53ac36d6d81b676b29649c7dc5719be0ee571f63058f842838d704bee - languageName: node - linkType: hard - "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -29182,13 +29057,6 @@ fsevents@~2.1.2: languageName: node linkType: hard -"resolve-from@npm:^1.0.0": - version: 1.0.1 - resolution: "resolve-from@npm:1.0.1" - checksum: 10134654dd6e758d4a4ad60acf69a90731673058a1a96068afc5f2ee84ac373df4d0237e0f052b5c81cc076273213ed50d228fc09723e0840c5c61ea37eb8854 - languageName: node - linkType: hard - "resolve-from@npm:^3.0.0": version: 3.0.0 resolution: "resolve-from@npm:3.0.0" @@ -29291,16 +29159,6 @@ resolve@~1.19.0: languageName: node linkType: hard -"restore-cursor@npm:^1.0.1": - version: 1.0.1 - resolution: "restore-cursor@npm:1.0.1" - dependencies: - exit-hook: ^1.0.0 - onetime: ^1.0.0 - checksum: e40bd1a540d69970341fc734dfada908815a44f91903211f34d32c47da33f6e7824bbc97f6e76aff387137d6b2a1ada3d3d2dc1b654b8accdc8ed5721c46cbfa - languageName: node - linkType: hard - "restore-cursor@npm:^2.0.0": version: 2.0.0 resolution: "restore-cursor@npm:2.0.0" @@ -29410,17 +29268,6 @@ resolve@~1.19.0: languageName: node linkType: hard -"rimraf@npm:~2.6.2": - version: 2.6.3 - resolution: "rimraf@npm:2.6.3" - dependencies: - glob: ^7.1.3 - bin: - rimraf: ./bin.js - checksum: 3ea587b981a19016297edb96d1ffe48af7e6af69660e3b371dbfc73722a73a0b0e9be5c88089fbeeb866c389c1098e07f64929c7414290504b855f54f901ab10 - languageName: node - linkType: hard - "ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": version: 2.0.2 resolution: "ripemd160@npm:2.0.2" @@ -29520,15 +29367,17 @@ resolve@~1.19.0: languageName: node linkType: hard -"rollup@npm:^0.63.4": - version: 0.63.5 - resolution: "rollup@npm:0.63.5" +"rollup@npm:^2.42.3": + version: 2.58.0 + resolution: "rollup@npm:2.58.0" dependencies: - "@types/estree": 0.0.39 - "@types/node": "*" + fsevents: ~2.3.2 + dependenciesMeta: + fsevents: + optional: true bin: - rollup: ./bin/rollup - checksum: a8af189d03e402b1ea70788c4c8bffc3af7db612e8c1effbea63f411b1d683378b9e8d10c9b2fae81aaf6f12c8a0aad6802ab7e1a7dee006229398b03e92e5e5 + rollup: dist/bin/rollup + checksum: a5e5642f2b1e239e3698eeee93b19d905e55e3012c63978cebc71e76c0a4eb2de5cff06a2a80d68bf87fe6869c57647cccc335daba280a5449a9214abdeca247 languageName: node linkType: hard @@ -29567,15 +29416,6 @@ resolve@~1.19.0: languageName: node linkType: hard -"run-async@npm:^0.1.0": - version: 0.1.0 - resolution: "run-async@npm:0.1.0" - dependencies: - once: ^1.3.0 - checksum: 66fd3ada4036a77a70fbf5063d66bf88df77fa9cbf20516115a6a09431ba66621f353e6fefecd10f9cb6a3345b5fe007a438dbf3f6020fbfd5732634cd4d3e15 - languageName: node - linkType: hard - "run-async@npm:^2.2.0, run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -29608,13 +29448,6 @@ resolve@~1.19.0: languageName: node linkType: hard -"rx-lite@npm:^3.1.2": - version: 3.1.2 - resolution: "rx-lite@npm:3.1.2" - checksum: e3cf6d42fa662aecbc90e0e0a547dcbd5188de8944d6e1bc353e8596ce2fd90ef5a6def0b3c6f6e1dfe49c32cec018e745ac5446d16ddf08a07fa8ed9cbc6272 - languageName: node - linkType: hard - "rxjs-report-usage@npm:^1.0.4": version: 1.0.5 resolution: "rxjs-report-usage@npm:1.0.5" @@ -29731,30 +29564,6 @@ resolve@~1.19.0: languageName: node linkType: hard -"sass-lint@npm:1.12.1": - version: 1.12.1 - resolution: "sass-lint@npm:1.12.1" - dependencies: - commander: ^2.8.1 - eslint: ^2.7.0 - front-matter: 2.1.2 - fs-extra: ^3.0.1 - glob: ^7.0.0 - globule: ^1.0.0 - gonzales-pe-sl: ^4.2.3 - js-yaml: ^3.5.4 - known-css-properties: ^0.3.0 - lodash.capitalize: ^4.1.0 - lodash.kebabcase: ^4.0.0 - merge: ^1.2.0 - path-is-absolute: ^1.0.0 - util: ^0.10.3 - bin: - sass-lint: ./bin/sass-lint.js - checksum: c605412ed0f9c73676aa2d63ee0cecc59f123e5603d1be880d813a9db23884fe58b2f5e00bae2416770eff0c956c8ba590783e78ae2caa95c407f3482ead3c77 - languageName: node - linkType: hard - "sass-loader@npm:12.1.0": version: 12.1.0 resolution: "sass-loader@npm:12.1.0" @@ -30244,15 +30053,6 @@ resolve@~1.19.0: languageName: node linkType: hard -"shelljs@npm:^0.6.0": - version: 0.6.1 - resolution: "shelljs@npm:0.6.1" - bin: - shjs: ./bin/shjs - checksum: 344142e8cc6f6a4529d6c31c700e0681f5b08c96a6e1aa7f09aed9b2e54cedc4126f49897ccc35a7ee0d4e228db65d8222d0e9572b5c002847efde0aa62ae59e - languageName: node - linkType: hard - "shellwords@npm:^0.1.1": version: 0.1.1 resolution: "shellwords@npm:0.1.1" @@ -30785,6 +30585,15 @@ resolve@~1.19.0: languageName: node linkType: hard +"specificity@npm:^0.4.1": + version: 0.4.1 + resolution: "specificity@npm:0.4.1" + bin: + specificity: ./bin/specificity + checksum: e558f1098f85aa54a8e90277309ac0d1913c84812c0bd349aa449076aa700964f71ab69f04f5fda9b7898bef9b7da3faa1cad9caedfd3f1a1ebfebedc18604ab + languageName: node + linkType: hard + "split-on-first@npm:^1.0.0": version: 1.1.0 resolution: "split-on-first@npm:1.1.0" @@ -31141,7 +30950,7 @@ resolve@~1.19.0: languageName: node linkType: hard -"string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -31366,15 +31175,6 @@ resolve@~1.19.0: languageName: node linkType: hard -"strip-json-comments@npm:~1.0.1": - version: 1.0.4 - resolution: "strip-json-comments@npm:1.0.4" - bin: - strip-json-comments: cli.js - checksum: 8a6487da5db2f5cf75630bc7c0185e13ece71b180b9dd1f786c1737c88a685080ca680b776443fce862867f3bd0b41145304a1c17f6f6ce7e495459523d31392 - languageName: node - linkType: hard - "strip-json-comments@npm:~2.0.1": version: 2.0.1 resolution: "strip-json-comments@npm:2.0.1" @@ -31440,6 +31240,13 @@ resolve@~1.19.0: languageName: node linkType: hard +"style-search@npm:^0.1.0": + version: 0.1.0 + resolution: "style-search@npm:0.1.0" + checksum: 3cfefe335033aad6d47da0725cb48f5db91a73935954c77eab77d9e415e6668cdb406da4a4f7ef9f1aca77853cf5ba7952c45e869caa5bd6439691d88098d468 + languageName: node + linkType: hard + "style-to-object@npm:0.3.0, style-to-object@npm:^0.3.0": version: 0.3.0 resolution: "style-to-object@npm:0.3.0" @@ -31472,6 +31279,116 @@ resolve@~1.19.0: languageName: node linkType: hard +"stylelint-config-prettier@npm:9.0.3": + version: 9.0.3 + resolution: "stylelint-config-prettier@npm:9.0.3" + peerDependencies: + stylelint: ">=11.0.0" + bin: + stylelint-config-prettier: bin/check.js + stylelint-config-prettier-check: bin/check.js + checksum: 9ff3f719daf3865878615ba52c31de6ef0a0d25d41cb58c41afe2f1c459a838997ff912cc4a5b4d401f92e2193667ff4d140b6d303cf8192e894b5cb454c41b9 + languageName: node + linkType: hard + +"stylelint-config-sass-guidelines@npm:8.0.0": + version: 8.0.0 + resolution: "stylelint-config-sass-guidelines@npm:8.0.0" + dependencies: + stylelint-order: ^4.0.0 + stylelint-scss: ^3.18.0 + peerDependencies: + stylelint: ^13.7.0 + checksum: 623bc33ba8cba037485ec13a6d88fbffbda75579507c6685e81a04b326fe70177b2252d45edd19cc27f070a96cd6b30532ef5ddf1505ef7a00dec606617ff4fa + languageName: node + linkType: hard + +"stylelint-order@npm:^4.0.0": + version: 4.1.0 + resolution: "stylelint-order@npm:4.1.0" + dependencies: + lodash: ^4.17.15 + postcss: ^7.0.31 + postcss-sorting: ^5.0.1 + peerDependencies: + stylelint: ^10.0.1 || ^11.0.0 || ^12.0.0 || ^13.0.0 + checksum: 95162ed46e2e90d923827a4bd4652d160cb225ead5520d13d83132337a72546c79d7d5d67833b87d054945dd74361a181a509273311fe7f56ac0f034959172c8 + languageName: node + linkType: hard + +"stylelint-scss@npm:^3.18.0": + version: 3.21.0 + resolution: "stylelint-scss@npm:3.21.0" + dependencies: + lodash: ^4.17.15 + postcss-media-query-parser: ^0.2.3 + postcss-resolve-nested-selector: ^0.1.1 + postcss-selector-parser: ^6.0.2 + postcss-value-parser: ^4.1.0 + peerDependencies: + stylelint: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 + checksum: 5c16263da4a913bd05871682a4fa90769abcb6a9e3f3d1c2386c7364b2308733af53a71c3ba17dd8646bb0d1a193a19cb3e6d7818bef60057c86f3287df1dc9c + languageName: node + linkType: hard + +"stylelint@npm:13.13.1": + version: 13.13.1 + resolution: "stylelint@npm:13.13.1" + dependencies: + "@stylelint/postcss-css-in-js": ^0.37.2 + "@stylelint/postcss-markdown": ^0.36.2 + autoprefixer: ^9.8.6 + balanced-match: ^2.0.0 + chalk: ^4.1.1 + cosmiconfig: ^7.0.0 + debug: ^4.3.1 + execall: ^2.0.0 + fast-glob: ^3.2.5 + fastest-levenshtein: ^1.0.12 + file-entry-cache: ^6.0.1 + get-stdin: ^8.0.0 + global-modules: ^2.0.0 + globby: ^11.0.3 + globjoin: ^0.1.4 + html-tags: ^3.1.0 + ignore: ^5.1.8 + import-lazy: ^4.0.0 + imurmurhash: ^0.1.4 + known-css-properties: ^0.21.0 + lodash: ^4.17.21 + log-symbols: ^4.1.0 + mathml-tag-names: ^2.1.3 + meow: ^9.0.0 + micromatch: ^4.0.4 + normalize-selector: ^0.2.0 + postcss: ^7.0.35 + postcss-html: ^0.36.0 + postcss-less: ^3.1.4 + postcss-media-query-parser: ^0.2.3 + postcss-resolve-nested-selector: ^0.1.1 + postcss-safe-parser: ^4.0.2 + postcss-sass: ^0.4.4 + postcss-scss: ^2.1.1 + postcss-selector-parser: ^6.0.5 + postcss-syntax: ^0.36.2 + postcss-value-parser: ^4.1.0 + resolve-from: ^5.0.0 + slash: ^3.0.0 + specificity: ^0.4.1 + string-width: ^4.2.2 + strip-ansi: ^6.0.0 + style-search: ^0.1.0 + sugarss: ^2.0.0 + svg-tags: ^1.0.0 + table: ^6.6.0 + v8-compile-cache: ^2.3.0 + write-file-atomic: ^3.0.3 + bin: + stylelint: bin/stylelint.js + checksum: 9dafa8d90f139e0518753546855df149a8770cead6fc31e40fc0b1904f7698a734767b441c0ba44dd694c846491708c6127dd0e5bb6917507ffe230dd40f0b8a + languageName: node + linkType: hard + "stylis@npm:^4.0.3, stylis@npm:^4.0.6": version: 4.0.10 resolution: "stylis@npm:4.0.10" @@ -31479,6 +31396,15 @@ resolve@~1.19.0: languageName: node linkType: hard +"sugarss@npm:^2.0.0": + version: 2.0.0 + resolution: "sugarss@npm:2.0.0" + dependencies: + postcss: ^7.0.2 + checksum: 777abf31671b67aafc5bb6dbca0853070ff9c129b7a52e90cfbe1a24ff069765e53b03767f85407386edf01c26fe2c2861aae2841f9a391751df891694137839 + languageName: node + linkType: hard + "supports-color@npm:^2.0.0": version: 2.0.0 resolution: "supports-color@npm:2.0.0" @@ -31541,6 +31467,13 @@ resolve@~1.19.0: languageName: node linkType: hard +"svg-tags@npm:^1.0.0": + version: 1.0.0 + resolution: "svg-tags@npm:1.0.0" + checksum: 407e5ef87cfa2fb81c61d738081c2decd022ce13b922d035b214b49810630bf5d1409255a4beb3a940b77b32f6957806deff16f1bf0ce1ab11c7a184115a0b7f + languageName: node + linkType: hard + "svgo@npm:^1.0.0": version: 1.3.2 resolution: "svgo@npm:1.3.2" @@ -31621,21 +31554,7 @@ resolve@~1.19.0: languageName: node linkType: hard -"table@npm:^3.7.8": - version: 3.8.3 - resolution: "table@npm:3.8.3" - dependencies: - ajv: ^4.7.0 - ajv-keywords: ^1.0.0 - chalk: ^1.1.1 - lodash: ^4.0.0 - slice-ansi: 0.0.4 - string-width: ^2.0.0 - checksum: 3cd27fc6aa8e2c4997b0bdcae07c901fda714db25e2f8b1a1c3c69d85952293b82130a3e3331256def65266179c5c6c6dc43d715ad15293c085cf4224e259541 - languageName: node - linkType: hard - -"table@npm:^6.0.4": +"table@npm:^6.0.4, table@npm:^6.6.0": version: 6.7.2 resolution: "table@npm:6.7.2" dependencies: @@ -31940,7 +31859,7 @@ resolve@~1.19.0: languageName: node linkType: hard -"text-table@npm:0.2.0, text-table@npm:^0.2.0, 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 @@ -32849,6 +32768,20 @@ typescript@~4.4.2: languageName: node linkType: hard +"unified@npm:^9.1.0": + version: 9.2.2 + resolution: "unified@npm:9.2.2" + dependencies: + bail: ^1.0.0 + extend: ^3.0.0 + is-buffer: ^2.0.0 + is-plain-obj: ^2.0.0 + trough: ^1.0.0 + vfile: ^4.0.0 + checksum: 7c24461be7de4145939739ce50d18227c5fbdf9b3bc5a29dabb1ce26dd3e8bd4a1c385865f6f825f3b49230953ee8b591f23beab3bb3643e3e9dc37aa8a089d5 + languageName: node + linkType: hard + "union-value@npm:^1.0.0": version: 1.0.1 resolution: "union-value@npm:1.0.1" @@ -32918,6 +32851,15 @@ typescript@~4.4.2: languageName: node linkType: hard +"unist-util-find-all-after@npm:^3.0.2": + version: 3.0.2 + resolution: "unist-util-find-all-after@npm:3.0.2" + dependencies: + unist-util-is: ^4.0.0 + checksum: 74b1fe81e3a980cc281b8d46a4cbc41940ece45608cae41d8021f245a73e7ed885222b80c1a2391137e04d2cda4de009416356aa7f4462d5e875c61579e33981 + languageName: node + linkType: hard + "unist-util-generated@npm:^1.0.0": version: 1.1.6 resolution: "unist-util-generated@npm:1.1.6" @@ -33241,15 +33183,6 @@ typescript@~4.4.2: languageName: node linkType: hard -"user-home@npm:^2.0.0": - version: 2.0.0 - resolution: "user-home@npm:2.0.0" - dependencies: - os-homedir: ^1.0.0 - checksum: a3329faa959fcd9e3e01a03347ca974f7f6b8896e6a634f29c61d8d5b61557d853c6fc5a6dff1a28e2da85b400d0e4490368a28de452ba8c41a2bf3a92cb110a - languageName: node - linkType: hard - "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -33310,15 +33243,6 @@ typescript@~4.4.2: languageName: node linkType: hard -"util@npm:^0.10.3": - version: 0.10.4 - resolution: "util@npm:0.10.4" - dependencies: - inherits: 2.0.3 - checksum: 913f9a90d05a60e91f91af01b8bd37e06bca4cc02d7b49e01089f9d5b78be2fffd61fb1a41b517de7238c5fc7337fa939c62d1fb4eb82e014894c7bee6637aaf - languageName: node - linkType: hard - "util@npm:^0.11.0": version: 0.11.1 resolution: "util@npm:0.11.1" @@ -33376,7 +33300,7 @@ typescript@~4.4.2: languageName: node linkType: hard -"v8-compile-cache@npm:^2.0.3, v8-compile-cache@npm:^2.2.0": +"v8-compile-cache@npm:^2.0.3, v8-compile-cache@npm:^2.2.0, v8-compile-cache@npm:^2.3.0": version: 2.3.0 resolution: "v8-compile-cache@npm:2.3.0" checksum: adb0a271eaa2297f2f4c536acbfee872d0dd26ec2d76f66921aa7fc437319132773483344207bdbeee169225f4739016d8d2dbf0553913a52bb34da6d0334f8e @@ -34381,15 +34305,6 @@ typescript@~4.4.2: languageName: node linkType: hard -"write@npm:^0.2.1": - version: 0.2.1 - resolution: "write@npm:0.2.1" - dependencies: - mkdirp: ^0.5.1 - checksum: 91bf45a4cf5c2a23fb56ca6cd3b1583295dafe7633f5fdf247f45ca212364cb2bcfe0c3040775b9c1efea613a49104da73d4ae5c28accb9d822aeb176fc7731e - languageName: node - linkType: hard - "ws@npm:^7.0.0, ws@npm:^7.2.3, ws@npm:^7.3.1, ws@npm:^7.4.6": version: 7.5.5 resolution: "ws@npm:7.5.5"