Merge remote-tracking branch 'origin/main' into resource-store

pull/89891/head
Ryan McKinley 1 year ago
commit 44a134f72b
  1. 66
      .drone.yml
  2. 24
      .github/CODEOWNERS
  3. 7
      .github/pr-checks.json
  4. 35
      .github/workflows/auto-milestone.yml
  5. 22
      .github/workflows/i18n-crowdin-download.yml
  6. 3
      conf/defaults.ini
  7. 3
      conf/sample.ini
  8. 16
      devenv/docker/blocks/prometheus/alertmanager.yml
  9. 6
      devenv/docker/blocks/prometheus/docker-compose.yaml
  10. 64
      docs/sources/administration/announcement-banner/_index.md
  11. 2
      docs/sources/administration/api-keys/index.md
  12. 2
      docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md
  13. 71
      docs/sources/dashboards/build-dashboards/manage-library-panels/index.md
  14. 79
      docs/sources/dashboards/use-dashboards/index.md
  15. 5
      docs/sources/dashboards/variables/add-template-variables/index.md
  16. 20
      docs/sources/dashboards/variables/inspect-variable/index.md
  17. 6
      docs/sources/setup-grafana/configure-grafana/_index.md
  18. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  19. 4
      docs/sources/tutorials/alerting-get-started-pt2/index.md
  20. 8
      docs/sources/tutorials/alerting-get-started/index.md
  21. 6
      e2e/dashboards-suite/dashboard-public-create.spec.ts
  22. 2
      e2e/dashboards-suite/dashboard-public-templating.spec.ts
  23. 39
      e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts
  24. 5
      e2e/run-suite
  25. 0
      e2e/scenes/dashboards-suite/Repeating_a_panel_horizontally.spec.ts
  26. 0
      e2e/scenes/dashboards-suite/Repeating_a_panel_vertically.spec.ts
  27. 0
      e2e/scenes/dashboards-suite/Repeating_an_empty_row.spec.ts
  28. 0
      e2e/scenes/dashboards-suite/dashboard-browse-nested.spec.ts
  29. 4
      e2e/scenes/dashboards-suite/dashboard-browse.spec.ts
  30. 0
      e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts
  31. 10
      e2e/scenes/dashboards-suite/dashboard-public-create.spec.ts
  32. 2
      e2e/scenes/dashboards-suite/dashboard-public-templating.spec.ts
  33. 0
      e2e/scenes/dashboards-suite/dashboard-templating.spec.ts
  34. 0
      e2e/scenes/dashboards-suite/dashboard-time-zone.spec.ts
  35. 0
      e2e/scenes/dashboards-suite/dashboard-timepicker.spec.ts
  36. 0
      e2e/scenes/dashboards-suite/embedded-dashboard.spec.ts
  37. 0
      e2e/scenes/dashboards-suite/general-dashboards.spec.ts
  38. 0
      e2e/scenes/dashboards-suite/import-dashboard.spec.ts
  39. 0
      e2e/scenes/dashboards-suite/load-options-from-url.spec.ts
  40. 0
      e2e/scenes/dashboards-suite/new-constant-variable.spec.ts
  41. 0
      e2e/scenes/dashboards-suite/new-custom-variable.spec.ts
  42. 0
      e2e/scenes/dashboards-suite/new-datasource-variable.spec.ts
  43. 0
      e2e/scenes/dashboards-suite/new-interval-variable.spec.ts
  44. 0
      e2e/scenes/dashboards-suite/new-query-variable.spec.ts
  45. 0
      e2e/scenes/dashboards-suite/new-text-box-variable.spec.ts
  46. 0
      e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts
  47. 2
      e2e/scenes/dashboards-suite/snapshot-create.spec.ts
  48. 0
      e2e/scenes/dashboards-suite/templating-dashboard-links-and-variables.spec.ts
  49. 0
      e2e/scenes/dashboards-suite/textbox-variables.spec.ts
  50. 0
      e2e/scenes/dashboards-suite/utils/makeDashboard.ts
  51. 4
      e2e/scenes/panels-suite/frontend-sandbox-panel.spec.ts
  52. 4
      e2e/scenes/various-suite/exemplars.spec.ts
  53. 3
      e2e/scenes/various-suite/filter-annotations.spec.ts
  54. 3
      e2e/scenes/various-suite/keybinds.spec.ts
  55. 3
      e2e/scenes/various-suite/loki-table-explore-to-dash.spec.ts
  56. 4
      e2e/scenes/various-suite/prometheus-variable-editor.spec.ts
  57. 2
      go.work.sum
  58. 2
      package.json
  59. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  60. 2
      packages/grafana-data/src/utils/LocalStorageValueProvider.tsx
  61. 2
      packages/grafana-data/src/utils/index.ts
  62. 0
      packages/grafana-data/src/utils/store.ts
  63. 2
      packages/grafana-e2e-selectors/src/selectors/components.ts
  64. 11
      packages/grafana-e2e-selectors/src/selectors/pages.ts
  65. 2
      packages/grafana-o11y-ds-frontend/src/index.ts
  66. 6
      packages/grafana-ui/.storybook/grafana.dark.scss
  67. 6
      packages/grafana-ui/.storybook/grafana.light.scss
  68. 4
      packages/grafana-ui/.storybook/preview.ts
  69. 2
      packages/grafana-ui/src/components/Tabs/Tab.tsx
  70. 1
      packages/grafana-ui/src/components/index.ts
  71. 2
      packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx
  72. 2743
      packages/grafana-ui/src/themes/GlobalStyles/fontAwesome.ts
  73. 11
      packages/grafana-ui/src/themes/GlobalStyles/fonts.ts
  74. 7
      pkg/api/pluginproxy/ds_proxy.go
  75. 4
      pkg/apiserver/rest/dualwriter_mode1.go
  76. 13
      pkg/apiserver/rest/dualwriter_mode2.go
  77. 88
      pkg/apiserver/rest/dualwriter_mode2_test.go
  78. 28
      pkg/middleware/loggermw/logger.go
  79. 37
      pkg/middleware/loggermw/logger_test.go
  80. 5
      pkg/registry/apis/query/query.go
  81. 9
      pkg/services/featuremgmt/registry.go
  82. 1
      pkg/services/featuremgmt/toggles_gen.csv
  83. 4
      pkg/services/featuremgmt/toggles_gen.go
  84. 5
      pkg/services/featuremgmt/toggles_gen.json
  85. 2
      pkg/services/navtree/navtreeimpl/applinks.go
  86. 6
      pkg/services/user/userimpl/user.go
  87. 1
      pkg/services/user/userimpl/user_test.go
  88. 14
      pkg/setting/setting.go
  89. 54
      pkg/util/uri_sanitize.go
  90. 74
      pkg/util/uri_sanitize_test.go
  91. 2
      public/app/core/utils/navBarItem-translations.ts
  92. 2
      public/app/features/alerting/unified/RuleList.test.tsx
  93. 43
      public/app/features/alerting/unified/api/alertRuleApi.ts
  94. 55
      public/app/features/alerting/unified/components/MenuItemPauseRule.tsx
  95. 4
      public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx
  96. 25
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
  97. 12
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
  98. 35
      public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
  99. 17
      public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx
  100. 4
      public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -593,6 +593,14 @@ steps:
HOST: grafana-server HOST: grafana-server
image: cypress/included:13.10.0 image: cypress/included:13.10.0
name: end-to-end-tests-dashboards-suite name: end-to-end-tests-dashboards-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/dashboards-suite
- commands: - commands:
- ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite
depends_on: depends_on:
@ -601,6 +609,14 @@ steps:
HOST: grafana-server HOST: grafana-server
image: cypress/included:13.10.0 image: cypress/included:13.10.0
name: end-to-end-tests-smoke-tests-suite name: end-to-end-tests-smoke-tests-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/smoke-tests-suite
- commands: - commands:
- ./bin/build e2e-tests --port 3001 --suite panels-suite - ./bin/build e2e-tests --port 3001 --suite panels-suite
depends_on: depends_on:
@ -609,6 +625,14 @@ steps:
HOST: grafana-server HOST: grafana-server
image: cypress/included:13.10.0 image: cypress/included:13.10.0
name: end-to-end-tests-panels-suite name: end-to-end-tests-panels-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/panels-suite
- commands: - commands:
- ./bin/build e2e-tests --port 3001 --suite various-suite - ./bin/build e2e-tests --port 3001 --suite various-suite
depends_on: depends_on:
@ -617,6 +641,14 @@ steps:
HOST: grafana-server HOST: grafana-server
image: cypress/included:13.10.0 image: cypress/included:13.10.0
name: end-to-end-tests-various-suite name: end-to-end-tests-various-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/various-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/various-suite
- commands: - commands:
- cd / - cd /
- ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH} - ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH}
@ -1908,6 +1940,14 @@ steps:
HOST: grafana-server HOST: grafana-server
image: cypress/included:13.10.0 image: cypress/included:13.10.0
name: end-to-end-tests-dashboards-suite name: end-to-end-tests-dashboards-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/dashboards-suite
- commands: - commands:
- ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite
depends_on: depends_on:
@ -1916,6 +1956,14 @@ steps:
HOST: grafana-server HOST: grafana-server
image: cypress/included:13.10.0 image: cypress/included:13.10.0
name: end-to-end-tests-smoke-tests-suite name: end-to-end-tests-smoke-tests-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/smoke-tests-suite
- commands: - commands:
- ./bin/build e2e-tests --port 3001 --suite panels-suite - ./bin/build e2e-tests --port 3001 --suite panels-suite
depends_on: depends_on:
@ -1924,6 +1972,14 @@ steps:
HOST: grafana-server HOST: grafana-server
image: cypress/included:13.10.0 image: cypress/included:13.10.0
name: end-to-end-tests-panels-suite name: end-to-end-tests-panels-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/panels-suite
- commands: - commands:
- ./bin/build e2e-tests --port 3001 --suite various-suite - ./bin/build e2e-tests --port 3001 --suite various-suite
depends_on: depends_on:
@ -1932,6 +1988,14 @@ steps:
HOST: grafana-server HOST: grafana-server
image: cypress/included:13.10.0 image: cypress/included:13.10.0
name: end-to-end-tests-various-suite name: end-to-end-tests-various-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/various-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/various-suite
- commands: - commands:
- cd / - cd /
- ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH} - ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH}
@ -4996,6 +5060,6 @@ kind: secret
name: gcr_credentials name: gcr_credentials
--- ---
kind: signature kind: signature
hmac: 06f574902baa67d8885abb48e48987f675d7637e30d4b783b3bb84e51b46cdaf hmac: a916fba452c568a0e1d392702db3423fa52652d8d60f65f7b7aa43a7c1e952f4
... ...

@ -152,7 +152,7 @@
/pkg/tests/apis/ @grafana/grafana-app-platform-squad /pkg/tests/apis/ @grafana/grafana-app-platform-squad
/pkg/tests/api/correlations/ @grafana/explore-squad /pkg/tests/api/correlations/ @grafana/explore-squad
/pkg/tsdb/grafanads/ @grafana/grafana-backend-group /pkg/tsdb/grafanads/ @grafana/grafana-backend-group
/pkg/tsdb/opentsdb/ @grafana/grafana-backend-group /pkg/tsdb/opentsdb/ @grafana/partner-datasources
/pkg/util/ @grafana/grafana-backend-group /pkg/util/ @grafana/grafana-backend-group
/pkg/web/ @grafana/grafana-backend-group /pkg/web/ @grafana/grafana-backend-group
@ -182,11 +182,11 @@
/devenv/docker/blocks/collectd/ @grafana/observability-metrics /devenv/docker/blocks/collectd/ @grafana/observability-metrics
/devenv/docker/blocks/etcd @grafana/grafana-app-platform-squad /devenv/docker/blocks/etcd @grafana/grafana-app-platform-squad
/devenv/docker/blocks/grafana/ @grafana/grafana-as-code /devenv/docker/blocks/grafana/ @grafana/grafana-as-code
/devenv/docker/blocks/graphite/ @grafana/observability-metrics /devenv/docker/blocks/graphite/ @grafana/partner-datasources
/devenv/docker/blocks/graphite09/ @grafana/observability-metrics /devenv/docker/blocks/graphite09/ @grafana/partner-datasources
/devenv/docker/blocks/graphite1/ @grafana/observability-metrics /devenv/docker/blocks/graphite1/ @grafana/partner-datasources
/devenv/docker/blocks/influxdb/ @grafana/observability-metrics /devenv/docker/blocks/influxdb/ @grafana/partner-datasources
/devenv/docker/blocks/influxdb1/ @grafana/observability-metrics /devenv/docker/blocks/influxdb1/ @grafana/partner-datasources
/devenv/docker/blocks/jaeger/ @grafana/observability-traces-and-profiling /devenv/docker/blocks/jaeger/ @grafana/observability-traces-and-profiling
/devenv/docker/blocks/maildev/ @grafana/alerting-frontend /devenv/docker/blocks/maildev/ @grafana/alerting-frontend
/devenv/docker/blocks/mariadb/ @grafana/oss-big-tent /devenv/docker/blocks/mariadb/ @grafana/oss-big-tent
@ -200,7 +200,7 @@
/devenv/docker/blocks/mysql_exporter/ @grafana/oss-big-tent /devenv/docker/blocks/mysql_exporter/ @grafana/oss-big-tent
/devenv/docker/blocks/mysql_opendata/ @grafana/oss-big-tent /devenv/docker/blocks/mysql_opendata/ @grafana/oss-big-tent
/devenv/docker/blocks/mysql_tests/ @grafana/oss-big-tent /devenv/docker/blocks/mysql_tests/ @grafana/oss-big-tent
/devenv/docker/blocks/opentsdb/ @grafana/observability-metrics /devenv/docker/blocks/opentsdb/ @grafana/partner-datasources
/devenv/docker/blocks/postgres/ @grafana/oss-big-tent /devenv/docker/blocks/postgres/ @grafana/oss-big-tent
/devenv/docker/blocks/postgres_tests/ @grafana/oss-big-tent /devenv/docker/blocks/postgres_tests/ @grafana/oss-big-tent
/devenv/docker/blocks/prometheus/ @grafana/observability-metrics /devenv/docker/blocks/prometheus/ @grafana/observability-metrics
@ -253,9 +253,7 @@
# Observability backend code # Observability backend code
/pkg/tsdb/prometheus/ @grafana/observability-metrics /pkg/tsdb/prometheus/ @grafana/observability-metrics
/pkg/tsdb/influxdb/ @grafana/observability-metrics
/pkg/tsdb/elasticsearch/ @grafana/observability-logs /pkg/tsdb/elasticsearch/ @grafana/observability-logs
/pkg/tsdb/graphite/ @grafana/observability-metrics
/pkg/tsdb/loki/ @grafana/observability-logs /pkg/tsdb/loki/ @grafana/observability-logs
/pkg/tsdb/tempo/ @grafana/observability-traces-and-profiling /pkg/tsdb/tempo/ @grafana/observability-traces-and-profiling
/pkg/tsdb/grafana-pyroscope-datasource/ @grafana/observability-traces-and-profiling /pkg/tsdb/grafana-pyroscope-datasource/ @grafana/observability-traces-and-profiling
@ -267,6 +265,8 @@
# Partner Datasources backend code # Partner Datasources backend code
/pkg/tsdb/mssql/ @grafana/partner-datasources /pkg/tsdb/mssql/ @grafana/partner-datasources
/pkg/tsdb/influxdb/ @grafana/partner-datasources
/pkg/tsdb/graphite/ @grafana/partner-datasources
# Database migrations # Database migrations
/pkg/services/sqlstore/migrations/ @grafana/grafana-search-and-storage /pkg/services/sqlstore/migrations/ @grafana/grafana-search-and-storage
@ -573,14 +573,14 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/plugins/datasource/grafana/ @grafana/grafana-frontend-platform /public/app/plugins/datasource/grafana/ @grafana/grafana-frontend-platform
/public/app/plugins/datasource/grafana-testdata-datasource/ @grafana/plugins-platform-frontend /public/app/plugins/datasource/grafana-testdata-datasource/ @grafana/plugins-platform-frontend
/public/app/plugins/datasource/azuremonitor/ @grafana/partner-datasources /public/app/plugins/datasource/azuremonitor/ @grafana/partner-datasources
/public/app/plugins/datasource/graphite/ @grafana/observability-metrics /public/app/plugins/datasource/graphite/ @grafana/partner-datasources
/public/app/plugins/datasource/influxdb/ @grafana/observability-metrics /public/app/plugins/datasource/influxdb/ @grafana/partner-datasources
/public/app/plugins/datasource/jaeger/ @grafana/observability-traces-and-profiling /public/app/plugins/datasource/jaeger/ @grafana/observability-traces-and-profiling
/public/app/plugins/datasource/loki/ @grafana/observability-logs /public/app/plugins/datasource/loki/ @grafana/observability-logs
/public/app/plugins/datasource/mixed/ @grafana/dashboards-squad /public/app/plugins/datasource/mixed/ @grafana/dashboards-squad
/public/app/plugins/datasource/mssql/ @grafana/partner-datasources /public/app/plugins/datasource/mssql/ @grafana/partner-datasources
/public/app/plugins/datasource/mysql/ @grafana/oss-big-tent /public/app/plugins/datasource/mysql/ @grafana/oss-big-tent
/public/app/plugins/datasource/opentsdb/ @grafana/observability-metrics /public/app/plugins/datasource/opentsdb/ @grafana/partner-datasources
/public/app/plugins/datasource/grafana-postgresql-datasource/ @grafana/oss-big-tent /public/app/plugins/datasource/grafana-postgresql-datasource/ @grafana/oss-big-tent
/public/app/plugins/datasource/prometheus/ @grafana/observability-metrics /public/app/plugins/datasource/prometheus/ @grafana/observability-metrics
/public/app/plugins/datasource/cloud-monitoring/ @grafana/partner-datasources /public/app/plugins/datasource/cloud-monitoring/ @grafana/partner-datasources

@ -1,11 +1,4 @@
[ [
{
"type": "check-milestone",
"title": "Milestone Check",
"targetUrl": "https://github.com/grafana/grafana/blob/main/contribute/merge-pull-request.md#assign-a-milestone",
"success": "Milestone set",
"failure": "Milestone not set"
},
{ {
"type": "check-changelog", "type": "check-changelog",
"title": "Changelog Check", "title": "Changelog Check",

@ -1,39 +1,26 @@
name: Auto-milestone name: Auto-milestone
on: on:
pull_request: pull_request_target:
types: types:
- opened - opened
- reopened - reopened
- closed - closed
- ready_for_review
jobs: permissions:
config: pull-requests: write
runs-on: "ubuntu-latest"
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
- name: "Check for secrets"
id: check
shell: bash
run: |
if [ -n "${{ (secrets.GRAFANA_DELIVERY_BOT_APP_ID != '' && secrets.GRAFANA_DELIVERY_BOT_APP_PEM != '') || '' }}" ]; then
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
fi
# Note: this action runs with write permissions on GITHUB_TOKEN even from forks
# so it must not run untrusted code (such as checking out the pull request)
jobs:
main: main:
needs: config
if: needs.config.outputs.has-secrets
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps: steps:
- name: "Generate token" # Note: Github will not trigger other actions from this because it uses
id: generate_token # the GITHUB_TOKEN token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
- name: Run auto-milestone - name: Run auto-milestone
uses: grafana/grafana-github-actions-go/auto-milestone@main uses: grafana/grafana-github-actions-go/auto-milestone@main
with: with:
pr: ${{ github.event.pull_request.number }} pr: ${{ github.event.pull_request.number }}
token: ${{ steps.generate_token.outputs.token }} token: ${{ secrets.GITHUB_TOKEN }}

@ -18,6 +18,14 @@ jobs:
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}
- name: Generate token
if: steps.crowdin-download.outputs.pull_request_url
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_ID }}
private_key: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_PEM }}
- name: Download sources - name: Download sources
id: crowdin-download id: crowdin-download
uses: crowdin/github-action@v1 uses: crowdin/github-action@v1
@ -53,18 +61,10 @@ jobs:
github_user_name: "github-actions[bot]" github_user_name: "github-actions[bot]"
github_user_email: "41898282+github-actions[bot]@users.noreply.github.com" github_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Generate token
if: steps.crowdin-download.outputs.pull_request_url
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_ID }}
private_key: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_PEM }}
- name: Get pull request ID - name: Get pull request ID
if: steps.crowdin-download.outputs.pull_request_url if: steps.crowdin-download.outputs.pull_request_url
shell: bash shell: bash
@ -74,7 +74,7 @@ jobs:
pr_id=$(gh pr view ${{ steps.crowdin-download.outputs.pull_request_url }} --json id -q .id) pr_id=$(gh pr view ${{ steps.crowdin-download.outputs.pull_request_url }} --json id -q .id)
echo "PULL_REQUEST_ID=$pr_id" >> "$GITHUB_ENV" echo "PULL_REQUEST_ID=$pr_id" >> "$GITHUB_ENV"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
- name: Get project board ID - name: Get project board ID
uses: octokit/graphql-action@v2.x uses: octokit/graphql-action@v2.x
@ -119,4 +119,4 @@ jobs:
if: steps.crowdin-download.outputs.pull_request_url if: steps.crowdin-download.outputs.pull_request_url
with: with:
pr: ${{ steps.crowdin-download.outputs.pull_request_number }} pr: ${{ steps.crowdin-download.outputs.pull_request_number }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ steps.generate_token.outputs.token }}

@ -519,6 +519,9 @@ user_invite_max_lifetime_duration = 24h
# The duration in time a verification email, used to update the email address of a user, remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 1h (1 hour). # The duration in time a verification email, used to update the email address of a user, remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 1h (1 hour).
verification_email_max_lifetime_duration = 1h verification_email_max_lifetime_duration = 1h
# Frequency of updating a user's last seen time. The minimum supported duration is 5m (5 minutes). The maximum supported duration is 1h (1 hour)
last_seen_update_interval = 15m
# Enter a comma-separated list of usernames to hide them in the Grafana UI. These users are shown to Grafana admins and to themselves. # Enter a comma-separated list of usernames to hide them in the Grafana UI. These users are shown to Grafana admins and to themselves.
hidden_users = hidden_users =

@ -520,6 +520,9 @@
# The duration in time a verification email, used to update the email address of a user, remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 1h (1 hour). # The duration in time a verification email, used to update the email address of a user, remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 1h (1 hour).
;verification_email_max_lifetime_duration = 1h ;verification_email_max_lifetime_duration = 1h
# Frequency of updating a user's last seen time. The minimum supported duration is 5m (5 minutes). The maximum supported duration is 1h (1 hour).
;last_seen_update_interval = 15m
# Enter a comma-separated list of users login to hide them in the Grafana UI. These users are shown to Grafana admins and themselves. # Enter a comma-separated list of users login to hide them in the Grafana UI. These users are shown to Grafana admins and themselves.
; hidden_users = ; hidden_users =

@ -0,0 +1,16 @@
route:
group_by: ['alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 1h
receiver: 'web.hook'
receivers:
- name: 'web.hook'
webhook_configs:
- url: 'http://127.0.0.1:5001/'
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'dev', 'instance']

@ -25,7 +25,11 @@
FD_DATASOURCE: prom FD_DATASOURCE: prom
alertmanager: alertmanager:
image: quay.io/prometheus/alertmanager image: prom/alertmanager
volumes:
- ${PWD}/docker/blocks/prometheus/alertmanager.yml:/etc/alertmanager/alertmanager.yml
command: >
--config.file=/etc/alertmanager/alertmanager.yml
ports: ports:
- "9093:9093" - "9093:9093"

@ -1,11 +1,5 @@
--- ---
draft: true draft: true
aliases:
- ../administration/reports/
- ../enterprise/export-pdf/
- ../enterprise/reporting/
- ../panels/create-reports/
- reporting/
keywords: keywords:
- grafana - grafana
- announcement - announcement
@ -15,34 +9,30 @@ labels:
- enterprise - enterprise
menuTitle: Announcement banner menuTitle: Announcement banner
title: Create and configure announcement banner title: Create and configure announcement banner
description: Creat a banner to show important updates and information at the top of on every page description: How to create an announcement banner to show important updates and information at the top of every Grafana page.
refs:
rbac:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/
--- ---
# Create and configure announcement banner # Create an announcement banner
Announcement banner allows you to show important updates and information at the top of every page in Grafana. You can use the announcement banner to communicate important information to your users, such as maintenance windows, new features, or other important updates. An announcement banner shows at the top of every page in Grafana. You can use the announcement banner to communicate information to your users, such as maintenance windows, new features, or other important updates.
## Create or update an announcement banner ## Create or update an announcement banner
Only organization administrators can create announcement banner by default. You can customize who can create announcement banner with [Role-based access control](ref:rbac). By default, only organization administrators can create announcement banners. You can customize who can create announcement banners with [Role-based access control](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/).
To create or update an announcement banner, follow these steps: To create or update an announcement banner, follow these steps:
1. Click **Administration > General > Announcement banner** in the side navigation menu. 1. Click **Administration > General > Announcement banner** in the side navigation menu.
The Announcement banner page allows you to view, create and update the settings for a notification banner. Only one banner can be created at a time. The **Announcement banner** page allows you to view, create, and update the settings for an announcement banner.
Only one banner can be active at a time.
2. Toggle the **Enable** switch on to enable the announcement banner. It can be toggled off at any time to disable the banner. 1. Toggle the **Enable** switch on to enable the announcement banner.
You can disable the banner at any time with this toggle.
1. Enter the **Message** for the announcement banner.
The message field supports Markdown.
3. Enter the **Message** for the announcement banner. To add a header, use the following syntax:
The message field supports Markdown. To add a header, use the following syntax:
```markdown ```markdown
### Header ### Header
@ -54,26 +44,18 @@ To create or update an announcement banner, follow these steps:
[link text](https://www.example.com) [link text](https://www.example.com)
``` ```
The preview of the configured banner will appear on top of the form, under the **Preview** section. The preview of the configured banner appears on top of the form, under the **Preview** section.
4. Select the banner's start date and time in the **Starts** field.
By default, the banner starts being displayed immediately. You can set a future date and time for the banner to start displaying.
5. Select the banner's end date and time in the **Ends** field.
By default, the banner is displayed indefinitely. You can set a future date and time for the banner to stop displaying.
6. Select the banner's visibility.
1. Select the banner's start date and time in the **Starts at** field.
By default, the banner starts being displayed immediately.
You can set a future date and time for the banner to start displaying.
1. Select the banner's end date and time in the **Ends at** field.
By default, the banner is displayed indefinitely.
You can set a date and time for the banner to stop displaying.
1. Select the banner's visibility.
**Everyone** - The banner is visible to all users, including on login page. **Everyone** - The banner is visible to all users, including on login page.
**Authenticated users** - The banner is visible to only authenticated users. **Authenticated users** - The banner is visible to only authenticated users.
1. Select the type of banner in the **Variant** field.
7. Select the type of banner in the **Variant** field. This determines the color of the banner's background.
1. Click **Save** to save the banner settings.
This will determine the color of the banner's background. The banner displays at the top of every page in Grafana between the start and end dates.
8. Click **Save** to save the banner settings.
The banner will now be displayed at the top of every page in Grafana.

@ -49,7 +49,7 @@ To follow these instructions, you need at least one of the following:
### Steps ### Steps
To create an API, complete the following steps: To create an API key, complete the following steps:
1. Sign in to Grafana. 1. Sign in to Grafana.
1. Click **Administration** in the left-side menu, **Users and access**, and select **API Keys**. 1. Click **Administration** in the left-side menu, **Users and access**, and select **API Keys**.

@ -91,7 +91,7 @@ roles:
# <bool> force deletion revoking all grants of the role. # <bool> force deletion revoking all grants of the role.
force: true force: true
- uid: 'basic_editor' - uid: 'basic_editor'
# <bool> always apply the specified changes to the role, regardless of the role version in the data base # <bool> always apply the specified changes to the role, regardless of the role version in the database
overrideRole: true overrideRole: true
global: true global: true
# <list> list of roles to copy permissions from. # <list> list of roles to copy permissions from.

@ -39,52 +39,75 @@ You can control permissions for library panels using [role-based access control
When you create a library panel, the panel on the source dashboard is converted to a library panel as well. You need to save the original dashboard once a panel is converted. When you create a library panel, the panel on the source dashboard is converted to a library panel as well. You need to save the original dashboard once a panel is converted.
1. Open a panel in edit mode. 1. Click **Edit** in the top-right corner of the dashboard.
1. In the panel display options, click the down arrow option to bring changes to the visualization. 1. On the panel you want to update, hover over any part of the panel to display the menu icon on the top-right corner.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-create-lib-panel-from-edit-9-5.png" class="docs-image--no-shadow" max-width= "800px" alt="Library panels tab of the panel editor pane" >}} 1. Click the menu icon and select **More > Create library panel**.
1. Click **Library panels**, and then click **+ Create library panel** to open the create dialog.
1. In **Library panel name**, enter the name. 1. In **Library panel name**, enter the name.
1. In **Save in folder**, select the folder to save the library panel. 1. In **Save in folder**, select the folder to save the library panel.
1. Click **Create library panel** to save your changes. 1. Click **Create library panel**.
1. Save the dashboard. 1. Click **Save dashboard** and **Exit edit**.
Once created, you can modify the library panel using any dashboard on which it appears. After you save the changes, all instances of the library panel reflect these modifications. Once created, you can modify the library panel using any dashboard on which it appears. After you save the changes, all instances of the library panel reflect these modifications.
You can also create a library panel directly from the edit menu of any panel.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-create-from-more-9-5.png" class="docs-image--no-shadow" max-width= "900px" alt="Create library panel option in the panel menu" >}}
## Add a library panel to a dashboard ## Add a library panel to a dashboard
Add a Grafana library panel to a dashboard when you want to provide visualizations to other dashboard users. Add a Grafana library panel to a dashboard when you want to provide visualizations to other dashboard users.
1. Click **Dashboards** in the left-side menu. 1. Click **Dashboards** in the main menu.
1. Click **New** and select **New Dashboard** in the dropdown. 1. Click **New** and select **New Dashboard** in the dropdown.
1. On the empty dashboard, click **+ Import library panel**. 1. On the empty dashboard, click **+ Add library panel**.
You will see a list of your library panels. You'll see a list of your library panels.
1. Filter the list or search to find the panel you want to add. 1. Filter the list or search to find the panel you want to add.
1. Click a panel to add it to the dashboard. 1. Click a panel to add it to the dashboard.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
## Unlink a library panel ## Unlink a library panel
Unlink a library panel when you want to make a change to the panel and not affect other instances of the library panel. Unlink a library panel when you want to make a change to the panel and not affect other instances of the library panel.
1. Click **Dashboards** in the left-side menu. 1. Click **Dashboards** in the main menu.
1. Click **Library panels**. 1. Click **Library panels**.
1. Select a library panel that is being used in different dashboards. 1. Select a library panel that is being used in dashboards.
1. Select the panel you want to unlink. 1. Click the panel you want to unlink.
1. Hover over any part of the panel to display the actions menu on the top right corner. 1. In the dialog box, select the dashboard from which you want to unlink the panel.
1. Click the menu and select **Edit**. 1. Click **View panel in \<dashboard name\>**.
1. Click **Unlink** on the top right corner of the page. 1. Click **Edit** in the top-right corner of the dashboard.
1. Hover over any part of the panel you want to unlink to display the menu icon on the top-right corner.
1. Click the menu icon and select **More > Unlink library panel**.
1. Click **Yes, unlink**. 1. Click **Yes, unlink**.
1. Click **Save dashboard** and **Exit edit**.
Alternatively, if you know where the library panel is being used, you can go directly to that dashboard and start at step 7.
## Replace a library panel
To replace a library panel with a different one, follow these steps:
1. Click **Dashboards** in the main menu.
1. Click **Library panels**.
1. Select a library panel that is being used in different dashboards.
1. Click the panel you want to unlink.
1. In the dialog box, select the dashboard from which you want to unlink the panel.
1. Click **View panel in \<dashboard name\>**.
1. Click **Edit** in the top-right corner of the dashboard.
1. Hover over any part of the panel you want to unlink to display the menu icon on the top-right corner.
1. Click the menu icon and select **More > Replace library panel**.
1. Select the replacement library panel.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save** and **Exit edit**.
Alternatively, if you know where the library panel that you want to replace is being used, you can go directly to that dashboard and start at step 7.
## View a list of library panels ## View a list of library panels
You can view a list of available library panels and search for a library panel. You can view a list of available library panels and see where those panels are being used.
1. Click **Dashboards** in the left-side menu. 1. Click **Dashboards** in the main menu.
1. Click **Library panels**. 1. Click **Library panels**.
You can see a list of previously defined library panels. You can see a list of previously defined library panels.
@ -94,10 +117,14 @@ You can view a list of available library panels and search for a library panel.
You can also filter the panels by folder or type. You can also filter the panels by folder or type.
1. Click the panel to see if it's being used in any dashboards.
1. (Optional) If the library panel is in use, select one of the dashboards using it.
1. (Optional) Click **View panel in \<dashboard name\>** to see the panel in context.
## Delete a library panel ## Delete a library panel
Delete a library panel when you no longer need it. Delete a library panel when you no longer need it.
1. Click **Dashboards** in the left-side menu. 1. Click **Dashboards** in the main menu.
1. Click **Library panels**. 1. Click **Library panels**.
1. Click the delete icon next to the library panel name. 1. Click the delete icon next to the library panel name.

@ -26,7 +26,7 @@ refs:
- pattern: /docs/grafana/ - pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/assess-dashboard-usage/ destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/assess-dashboard-usage/
- pattern: /docs/grafana-cloud/ - pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/assess-dashboard-usage/ destination: /docs/grafana-cloud/visualizations/dashboards/assess-dashboard-usage/
generative-ai-features: generative-ai-features:
- pattern: /docs/grafana/ - pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/manage-dashboards/#set-up-generative-ai-features-for-dashboards destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/manage-dashboards/#set-up-generative-ai-features-for-dashboards
@ -36,12 +36,37 @@ refs:
- pattern: /docs/grafana/ - pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/modify-dashboard-settings/ destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/modify-dashboard-settings/
- pattern: /docs/grafana-cloud/ - pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/modify-dashboard-settings/ destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/modify-dashboard-settings/
repeating-rows: repeating-rows:
- pattern: /docs/grafana/ - pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows
- pattern: /docs/grafana-cloud/ - pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows
variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
dashboard-folders:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/manage-dashboards/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/manage-dashboards/
sharing:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/share-dashboards-panels/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/share-dashboards-panels/
dashboard-links:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/manage-dashboard-links/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/manage-dashboard-links/
panel-overview:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/panel-overview/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-overview/
--- ---
# Use dashboards # Use dashboards
@ -56,32 +81,30 @@ The dashboard user interface provides a number of features that you can use to c
The following image and descriptions highlight all dashboard features. The following image and descriptions highlight all dashboard features.
{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-annotated-9-5-0.png" width="700px" alt="An annotated image of a dashboard" >}} ![An annotated image of a dashboard](/media/docs/grafana/dashboards/screenshot-dashboard-annotated-11.2.png)
- (1) **Grafana home**: Click **Home** in the breadcrumb to be redirected to the home page configured in the Grafana instance. 1. **Grafana home** - Click **Home** in the breadcrumb to go to the home page configured in the Grafana instance.
- (2) **Dashboard title**: When you click the dashboard title, you can search for dashboards contained in the current folder. You can create your own dashboard titles or have Grafana create them for you using [generative AI features](ref:generative-ai-features). 1. **Dashboard folder** - When you click the dashboard folder name, you can search for other dashboards contained in the folder and perform other [folder management tasks](ref:dashboard-folders).
- (3) **Share dashboard or panel**: Use this option to share the current dashboard or panel using a link or snapshot. You can also export the dashboard definition from the share modal. 1. **Dashboard title** - You can create your own dashboard titles or have Grafana create them for you using [generative AI features](ref:generative-ai-features).
- (4) **Add**: Use this option to add a panel, dashboard row, or library panel to the current dashboard. 1. **Mark as favorite** - Mark the dashboard as one of your favorites so it's included in your list of **Starred** dashboards in the main menu.
- (5) **Save dashboard**: Click to save changes to your dashboard. 1. **Dashboard insights** - Click to view analytics about your dashboard including information about users, activity, query counts. Learn more about [dashboard analytics](ref:dashboard-analytics).
- (6) **Dashboard insights**: Click to view analytics about your dashboard including information about users, activity, query counts. Learn more about [dashboard analytics](ref:dashboard-analytics). 1. **Share dashboard** - Access several [dashboard sharing](ref:sharing) options.
- (7) **Dashboard settings**: Use this option to change dashboard name, folder, and tags and manage variables and annotation queries. Learn more about [dashboard settings](ref:dashboard-settings). 1. **Edit** - Click to leave view-only mode and enter edit mode, where you can make changes directly to the dashboard and access dashboard settings, as well as several panel editing functions.
- (8) **Time picker dropdown**: Click to select relative time range options and set custom absolute time ranges. 1. **Kiosk mode** - Click to display the dashboard on a large screen such as a TV or a kiosk. Kiosk mode hides elements such as navigation menus. Learn more about kiosk mode in our [How to Create Kiosks to Display Dashboards on a TV blog post](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/). Press `Enter` to leave kiosk mode.
- You can change the **Timezone** and **fiscal year** settings from the time range controls by clicking the **Change time settings** button. 1. **Variables** - Use [variables](ref:variables) to create more interactive and dynamic dashboards.
1. **Dashboard links** - Link to other dashboards, panels, and external websites. Learn more about [dashboard links](ref:dashboard-links).
1. **Current dashboard time range and time picker** - Click to select [relative time range](#relative-time-range) options and set custom [absolute time ranges](#absolute-time-range).
- You can change the **Timezone** and **Fiscal year** settings from the time range controls by clicking the **Change time settings** button.
- Time settings are saved on a per-dashboard basis. - Time settings are saved on a per-dashboard basis.
- (9) **Zoom out time range**: Click to zoom out the time range. Learn more about how to use [common time range controls](#common-time-range-controls). 1. **Time range zoom out** - Click to zoom out the time range. Learn more about how to use [common time range controls](#common-time-range-controls).
- (10) **Refresh dashboard**: Click to immediately trigger queries and refresh dashboard data. 1. **Refresh dashboard** - Click to immediately trigger queries and refresh dashboard data.
- (11) **Refresh dashboard time interval**: Click to select a dashboard auto refresh time interval. 1. **Auto refresh control** - Click to select a dashboard auto refresh time interval.
- (12) **View mode**: Click to display the dashboard on a large screen such as a TV or a kiosk. View mode hides irrelevant information such as navigation menus. Learn more about view mode in our [How to Create Kiosks to Display Dashboards on a TV blog post](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/). 1. **Dashboard row** - A dashboard row is a logical divider within a dashboard that groups panels together.
- (13) **Dashboard panel**: The primary building block of a dashboard is the panel. To add a new panel, dashboard row, or library panel, click **Add panel**.
- Library panels can be shared among many dashboards.
- To move a panel, drag the panel header to another location.
- To resize a panel, click and drag the lower right corner of the panel.
- Use [generative AI features](ref:generative-ai-features) to create panel titles and descriptions.
- (14) **Graph legend**: Change series colors, y-axis and series visibility directly from the legend.
- (15) **Dashboard row**: A dashboard row is a logical divider within a dashboard that groups panels together.
- Rows can be collapsed or expanded allowing you to hide parts of the dashboard. - Rows can be collapsed or expanded allowing you to hide parts of the dashboard.
- Panels inside a collapsed row do not issue queries. - Panels inside a collapsed row do not issue queries.
- Use [repeating rows](ref:repeating-rows) to dynamically create rows based on a template variable. - Use [repeating rows](ref:repeating-rows) to dynamically create rows based on a template variable.
1. **Dashboard panel** - The [panel](ref:panel-overview) is the primary building block of a dashboard.
1. **Panel legend** - Change series colors as well as y-axis and series visibility directly from the legend.
## Keyboard shortcuts ## Keyboard shortcuts
@ -148,7 +171,7 @@ Grafana Alerting does not support the following syntaxes at this time:
The dashboard and panel time controls have a common UI. The dashboard and panel time controls have a common UI.
<img class="no-shadow" src="/static/img/docs/time-range-controls/common-time-controls-7-0.png" max-width="700px"> ![Common time controls](/media/docs/grafana/dashboards/screenshot-common-time-controls-11.2.png)
The following sections define common time range controls. The following sections define common time range controls.
@ -158,11 +181,11 @@ The current time range, also called the _time picker_, shows the time range curr
Hover your cursor over the field to see the exact time stamps in the range and their source (such as the local browser). Hover your cursor over the field to see the exact time stamps in the range and their source (such as the local browser).
<img class="no-shadow" src="/static/img/docs/time-range-controls/time-picker-7-0.png" max-width="300px"> ![Time picker](/media/docs/grafana/dashboards/screenshot-time-picker-11.2.png)
Click the current time range to change it. You can change the current time using a _relative time range_, such as the last 15 minutes, or an _absolute time range_, such as `2020-05-14 00:00:00 to 2020-05-15 23:59:59`. Click the current time range to change it. You can change the current time using a _relative time range_, such as the last 15 minutes, or an _absolute time range_, such as `2020-05-14 00:00:00 to 2020-05-15 23:59:59`.
<img class="no-shadow" src="/media/docs/grafana/dashboards/screenshot-change-current-time-range-10.3.png" max-width="900px"> ![Current time range](/media/docs/grafana/dashboards/screenshot-current-time-range-11.2.png)
#### Relative time range #### Relative time range

@ -135,9 +135,12 @@ Query expressions are different for each data source. For more information, refe
- **On Dashboard Load:** Queries the data source every time the dashboard loads. This slows down dashboard loading, because the variable query needs to be completed before dashboard can be initialized. - **On Dashboard Load:** Queries the data source every time the dashboard loads. This slows down dashboard loading, because the variable query needs to be completed before dashboard can be initialized.
- **On Time Range Change:** Queries the data source every time the dashboard loads and when the dashboard time range changes. Use this option if your variable options query contains a time range filter or is dependent on the dashboard time range. - **On Time Range Change:** Queries the data source every time the dashboard loads and when the dashboard time range changes. Use this option if your variable options query contains a time range filter or is dependent on the dashboard time range.
1. In the **Query** field, enter a query. 1. In the **Query** field, enter a query.
- The query field varies according to your data source. Some data sources have custom query editors. - The query field varies according to your data source. Some data sources have custom query editors.
- Make sure that the query returns values named `__text` and `__value` as appropriate in your query syntax. For example, in SQL, you can use a query such as `SELECT hostname AS __text, id AS __value FROM MyTable`. Queries for other languages will vary depending on syntax. - Each data source defines how the variable values are extracted. The typical implementation uses every string value returned from the data source response as a variable value. Make sure to double-check the documentation for the data source.
- Some data sources let you provide custom "display names" for the values. For instance, the PostgreSQL, MySQL, and Microsoft SQL Server plugins handle this by looking for fields named `__text` and `__value` in the result. Other data sources may look for `text` and `value` or use a different approach. Always remember to double-check the documentation for the data source.
- If you need more room in a single input field query editor, then hover your cursor over the lines in the lower right corner of the field and drag downward to expand. - If you need more room in a single input field query editor, then hover your cursor over the lines in the lower right corner of the field and drag downward to expand.
1. (Optional) In the **Regex** field, type a regex expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with regex](#filter-variables-with-regex). 1. (Optional) In the **Regex** field, type a regex expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with regex](#filter-variables-with-regex).
1. In the **Sort** list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query will be used. 1. In the **Sort** list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query will be used.
1. (Optional) Enter [Selection Options](#configure-variable-selection-options). 1. (Optional) Enter [Selection Options](#configure-variable-selection-options).

@ -28,19 +28,21 @@ weight: 200
# Manage and inspect variables # Manage and inspect variables
The variables page lets you [add](ref:add) variables and [manage](#manage-variables) existing variables. It also allows you to [inspect](#inspect-variables) variables and identify whether a variable is being referenced (or used) in other variables or dashboard. In the **Variables** tab, you can [add](ref:add) variables and [manage](#manage-variables) existing variables. You can also [inspect](#inspect-variables) variables to identify any dependencies between them. <!--whether a variable is being referenced (or used) in other variables or dashboard.-->
## Manage variables ## Manage variables
You can take the following actions on the variables page: You can take the following actions in the **Variables** tab:
**Move:** You can move a variable up or down the list using drag and drop. - **Move** - Move a variable up or down the list using drag and drop.
- **Clone** - Clone a variable by clicking the clone icon in the set of icons on the right. This creates a copy of the variable with the name of the original variable prefixed with `copy_of_`.
- **Delete** - Delete a variable by clicking the trash icon in the set of icons on the right.
**Clone:** To clone a variable, click the clone icon from the set of icons on the right. This creates a copy of the variable with the name of the original variable prefixed with `copy_of_`. ## Inspect variables
**Delete:** To delete a variable, click the trash icon from the set of icons on the right. In addition to [managing variables](#manage-variables), the **Variables** tab lets you easily identify whether variables have any dependencies. To check, click **Show dependencies** at the bottom of the list, which opens the dependencies diagram:
## Inspect variables <!-- Update and comment this back in when the reference functionality is working again
The variables page lets you easily identify whether a variable is being referenced (or used) in other variables or dashboard. In addition, you can also [add](ref:add) and [manage variables](#manage-variables) on this page. The variables page lets you easily identify whether a variable is being referenced (or used) in other variables or dashboard. In addition, you can also [add](ref:add) and [manage variables](#manage-variables) on this page.
@ -54,6 +56,10 @@ Any variable that is referenced or used has a green check mark next to it, while
![Variables list](/static/img/docs/variables-templates/variable-not-referenced-7-4.png) ![Variables list](/static/img/docs/variables-templates/variable-not-referenced-7-4.png)
In addition, all referenced variables have a dependency icon next to the green check mark. You can click on the icon to view the dependency map. The dependency map can be moved. You can zoom in out with mouse wheel or track pad equivalent. In addition, all referenced variables have a dependency icon next to the green check mark. You can click on the icon to view the dependency map. The dependency map can be moved. You can zoom in out with mouse wheel or track pad equivalent.-->
![Variables list](/static/img/docs/variables-templates/dependancy-map-7-4.png) ![Variables list](/static/img/docs/variables-templates/dependancy-map-7-4.png)
{{% admonition type="note" %}}
This feature is available in Grafana 7.4 and later versions.
{{% /admonition %}}

@ -892,6 +892,12 @@ The duration in time a verification email, used to update the email address of a
This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week).
Default is 1h (1 hour). Default is 1h (1 hour).
### last_seen_update_interval
The frequency of updating a user's last seen time.
This setting should be expressed as a duration. Examples: 1h (hour), 15m (minutes)
Default is `15m` (15 minutes). The minimum supported duration is `5m` (5 minutes). The maximum supported duration is `1h` (1 hour).
### hidden_users ### hidden_users
This is a comma-separated list of usernames. Users specified here are hidden in the Grafana UI. They are still visible to Grafana administrators and to themselves. This is a comma-separated list of usernames. Users specified here are hidden in the Grafana UI. They are still visible to Grafana administrators and to themselves.

@ -27,7 +27,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `publicDashboards` | [Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version. | Yes | | `publicDashboards` | [Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version. | Yes |
| `featureHighlights` | Highlight Grafana Enterprise features | | | `featureHighlights` | Highlight Grafana Enterprise features | |
| `correlations` | Correlations page | Yes | | `correlations` | Correlations page | Yes |
| `exploreContentOutline` | Content outline sidebar | Yes |
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | | `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `nestedFolders` | Enable folder nesting | Yes | | `nestedFolders` | Enable folder nesting | Yes |
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | | `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |

@ -17,8 +17,6 @@ weight: 50
# Get started with Grafana Alerting - Part 2 # Get started with Grafana Alerting - Part 2
## Introduction
This is part 2 of the [Get Started with Grafana Alerting tutorial](http://grafana.com/tutorials/alerting-get-started/). This is part 2 of the [Get Started with Grafana Alerting tutorial](http://grafana.com/tutorials/alerting-get-started/).
In this guide, we dig into more complex yet equally fundamental elements of Grafana Alerting: **alert instances** and **notification policies**. In this guide, we dig into more complex yet equally fundamental elements of Grafana Alerting: **alert instances** and **notification policies**.
@ -65,7 +63,7 @@ Create a notification policy if you want to handle metrics returned by alert rul
1. In the field **Label** enter `device`, and in the field **Value** enter `desktop`. 1. In the field **Label** enter `device`, and in the field **Value** enter `desktop`.
1. From the **Contact point** drop-down, choose **Webhook**. 1. From the **Contact point** drop-down, choose **Webhook**.
{{< admonition type="note" >}} {{< admonition type="note" >}}
If you don’t have any contact points, add a [Contact point](http://localhost:3002/docs/grafana/latest/tutorials/alerting-get-started/#create-a-contact-point). If you don’t have any contact points, add a [Contact point](https://grafana.com/tutorials/alerting-get-started/#create-a-contact-point).
{{</ admonition >}} {{</ admonition >}}
1. Click **Save Policy**. 1. Click **Save Policy**.

@ -25,6 +25,10 @@ In this tutorial you will:
- Set up an alert rule. - Set up an alert rule.
- Receive firing and resolved alert notifications in a public webhook. - Receive firing and resolved alert notifications in a public webhook.
{{< admonition type="tip" >}}
Check out [Part 2](http://grafana.com/tutorials/alerting-get-started-pt2/) if you want to learn more about alerts and notification routing.
{{< /admonition >}}
## Before you begin ## Before you begin
### Grafana Cloud users ### Grafana Cloud users
@ -184,6 +188,10 @@ To edit the Alert rule:
By incrementing the threshold, the condition is no longer met, and after the evaluation interval has concluded (1 minute approx.), you should receive an alert notification with status **“Resolved”**. By incrementing the threshold, the condition is no longer met, and after the evaluation interval has concluded (1 minute approx.), you should receive an alert notification with status **“Resolved”**.
## Learn more
Your learning journey continues in [Part 2](http://grafana.com/tutorials/alerting-get-started-pt2/) where you will learn about alert instances and notification routing.
## Summary ## Summary
In this tutorial, you have learned how to set up a contact point, create an alert, and send alert notifications to a public Webhook. By following these steps, you’ve gained a foundational understanding of how to leverage Grafana Alerting capabilities to monitor and respond to events of interest in your data. In this tutorial, you have learned how to set up a contact point, create an alert, and send alert notifications to a public Webhook. By following these steps, you’ve gained a foundational understanding of how to leverage Grafana Alerting capabilities to monitor and respond to events of interest in your data.

@ -17,7 +17,7 @@ describe('Public dashboards', () => {
e2e.pages.Dashboard.DashNav.shareButton().click(); e2e.pages.Dashboard.DashNav.shareButton().click();
// Select public dashboards tab // Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click(); e2e.components.Tab.title('Public dashboard').click();
// Create button should be disabled // Create button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled'); e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled');
@ -78,7 +78,7 @@ describe('Public dashboards', () => {
// Select public dashboards tab // Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click(); e2e.components.Tab.title('Public dashboard').click();
cy.wait('@query-public-dashboard'); cy.wait('@query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist'); e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
@ -118,7 +118,7 @@ describe('Public dashboards', () => {
// Select public dashboards tab // Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click(); e2e.components.Tab.title('Public dashboard').click();
cy.wait('@query-public-dashboard'); cy.wait('@query-public-dashboard');
// save url before disabling public dashboard // save url before disabling public dashboard

@ -13,7 +13,7 @@ describe('Create a public dashboard with template variables shows a template var
e2e.pages.Dashboard.DashNav.shareButton().click(); e2e.pages.Dashboard.DashNav.shareButton().click();
// Select public dashboards tab // Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click(); e2e.components.Tab.title('Public dashboard').click();
// Warning Alert dashboard cannot be made public because it has template variables // Warning Alert dashboard cannot be made public because it has template variables
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible'); e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');

@ -0,0 +1,39 @@
import * as e2e from '@grafana/e2e-selectors';
import { expect, test } from '@grafana/plugin-e2e';
test('should evaluate to false if entire request returns 500', async ({ page, alertRuleEditPage, selectors }) => {
await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule');
// remove the default query
const queryA = alertRuleEditPage.getAlertRuleQueryRow('A');
await alertRuleEditPage
.getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Remove query'), {
root: queryA.locator,
})
.click();
await expect(alertRuleEditPage.evaluate()).not.toBeOK();
});
test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({
page,
alertRuleEditPage,
}) => {
await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule');
//add working query
const queryA = alertRuleEditPage.getAlertRuleQueryRow('A');
await queryA.datasource.set('gdev-prometheus');
await queryA.locator.getByLabel('Code').click();
await page.waitForFunction(() => window.monaco);
await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click();
await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))');
//add broken query
const newQuery = await alertRuleEditPage.clickAddQueryRow();
await newQuery.datasource.set('gdev-prometheus');
await newQuery.locator.getByLabel('Code').click();
await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click();
await page.keyboard.insertText('topk(5,');
await expect(alertRuleEditPage.evaluate()).not.toBeOK();
});

@ -106,6 +106,11 @@ case "$1" in
;; ;;
esac esac
;; ;;
"scenes/"*)
cypressConfig[specPattern]=./e2e/"${args[0]}"/$testFilesForSingleSuite
cypressConfig[video]=${args[1]}
env[SCENES]=true
;;
"enterprise-smtp") "enterprise-smtp")
env[SMTP_PLUGIN_ENABLED]=true env[SMTP_PLUGIN_ENABLED]=true
cypressConfig[specPattern]=./e2e/extensions/enterprise/smtp-suite/$testFilesForSingleSuite cypressConfig[specPattern]=./e2e/extensions/enterprise/smtp-suite/$testFilesForSingleSuite

@ -1,7 +1,7 @@
import testDashboard from '../dashboards/TestDashboard.json'; import testDashboard from '../dashboards/TestDashboard.json';
import { e2e } from '../utils'; import { e2e } from '../utils';
// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-browse.spec.ts
describe('Dashboard browse', () => { describe.skip('Dashboard browse', () => {
beforeEach(() => { beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
}); });

@ -1,6 +1,6 @@
import { e2e } from '../utils'; import { e2e } from '../utils';
// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-public-create.spec.ts
describe('Public dashboards', () => { describe.skip('Public dashboards', () => {
beforeEach(() => { beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
}); });
@ -17,7 +17,7 @@ describe('Public dashboards', () => {
e2e.components.NavToolbar.shareDashboard().click(); e2e.components.NavToolbar.shareDashboard().click();
// Select public dashboards tab // Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click(); e2e.components.Tab.title('Public dashboard').click();
// Create button should be disabled // Create button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled'); e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled');
@ -78,7 +78,7 @@ describe('Public dashboards', () => {
// Select public dashboards tab // Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click(); e2e.components.Tab.title('Public dashboard').click();
cy.wait('@query-public-dashboard'); cy.wait('@query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist'); e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
@ -118,7 +118,7 @@ describe('Public dashboards', () => {
// Select public dashboards tab // Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click(); e2e.components.Tab.title('Public dashboard').click();
cy.wait('@query-public-dashboard'); cy.wait('@query-public-dashboard');
// save url before disabling public dashboard // save url before disabling public dashboard

@ -13,7 +13,7 @@ describe('Create a public dashboard with template variables shows a template var
e2e.components.NavToolbar.shareDashboard().click(); e2e.components.NavToolbar.shareDashboard().click();
// Select public dashboards tab // Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click(); e2e.components.Tab.title('Public Dashboard').click();
// Warning Alert dashboard cannot be made public because it has template variables // Warning Alert dashboard cannot be made public because it has template variables
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible'); e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');

@ -25,7 +25,7 @@ describe('Snapshots', () => {
e2e.components.NavToolbar.shareDashboard().click(); e2e.components.NavToolbar.shareDashboard().click();
// Select the snapshot tab // Select the snapshot tab
e2e.pages.ShareDashboardModal.SnapshotScene.Tab().click(); e2e.components.Tab.title('Snapshot').click();
// Publish snapshot // Publish snapshot
cy.intercept('POST', '/api/snapshots').as('save'); cy.intercept('POST', '/api/snapshots').as('save');

@ -2,8 +2,8 @@ import panelSandboxDashboard from '../../dashboards/PanelSandboxDashboard.json';
import { e2e } from '../../utils'; import { e2e } from '../../utils';
const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950';
// Skipping due to race conditions with same old arch test e2e/panels-suite/frontend-sandbox-panel.spec.ts
describe('Panel sandbox', () => { describe.skip('Panel sandbox', () => {
beforeEach(() => { beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true); return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true);

@ -17,8 +17,8 @@ const addDataSource = () => {
}, },
}); });
}; };
// Skipping due to race conditions with same old arch test e2e/various-suite/exemplars.spec.ts
describe('Exemplars', () => { describe.skip('Exemplars', () => {
beforeEach(() => { beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));

@ -1,7 +1,8 @@
import { e2e } from '../utils'; import { e2e } from '../utils';
const DASHBOARD_ID = 'ed155665'; const DASHBOARD_ID = 'ed155665';
describe('Annotations filtering', () => { // Skipping due to race conditions with same old arch test e2e/various-suite/filter-annotations.spec.ts
describe.skip('Annotations filtering', () => {
beforeEach(() => { beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
}); });

@ -1,7 +1,8 @@
import { e2e } from '../utils'; import { e2e } from '../utils';
import { fromBaseUrl } from '../utils/support/url'; import { fromBaseUrl } from '../utils/support/url';
describe('Keyboard shortcuts', () => { // Skipping due to race conditions with same old arch test e2e/various-suite/keybinds.spec.ts
describe.skip('Keyboard shortcuts', () => {
beforeEach(() => { beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));

@ -110,7 +110,8 @@ const lokiQueryResult = {
}, },
}; };
describe('Loki Query Editor', () => { // Skipping due to race conditions with same old arch test e2e/various-suite/loki-table-explore-to-dash.spec.ts
describe.skip('Loki Query Editor', () => {
beforeEach(() => { beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
}); });

@ -77,8 +77,8 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) {
// do nothing // do nothing
} }
} }
// Skipping due to race conditions with same old arch test e2e/various-suite/prometheus-variable-editor.spec.ts
describe('Prometheus variable query editor', () => { describe.skip('Prometheus variable query editor', () => {
beforeEach(() => { beforeEach(() => {
createPromDS(DATASOURCE_ID, DATASOURCE_NAME); createPromDS(DATASOURCE_ID, DATASOURCE_NAME);
}); });

@ -479,6 +479,7 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH
github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4=
github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4=
github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM=
@ -885,6 +886,7 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=

@ -76,7 +76,7 @@
"@emotion/eslint-plugin": "11.11.0", "@emotion/eslint-plugin": "11.11.0",
"@grafana/eslint-config": "7.0.0", "@grafana/eslint-config": "7.0.0",
"@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules",
"@grafana/plugin-e2e": "1.3.2", "@grafana/plugin-e2e": "1.4.0",
"@grafana/tsconfig": "^1.3.0-rc1", "@grafana/tsconfig": "^1.3.0-rc1",
"@manypkg/get-packages": "^2.2.0", "@manypkg/get-packages": "^2.2.0",
"@playwright/test": "1.44.1", "@playwright/test": "1.44.1",

@ -29,7 +29,6 @@ export interface FeatureToggles {
featureHighlights?: boolean; featureHighlights?: boolean;
storage?: boolean; storage?: boolean;
correlations?: boolean; correlations?: boolean;
exploreContentOutline?: boolean;
datasourceQueryMultiStatus?: boolean; datasourceQueryMultiStatus?: boolean;
autoMigrateOldPanels?: boolean; autoMigrateOldPanels?: boolean;
autoMigrateGraphPanel?: boolean; autoMigrateGraphPanel?: boolean;

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { store } from '../store'; import { store } from './store';
export interface Props<T> { export interface Props<T> {
storageKey: string; storageKey: string;

@ -13,6 +13,8 @@ export * from './binaryOperators';
export * from './unaryOperators'; export * from './unaryOperators';
export * from './nodeGraph'; export * from './nodeGraph';
export * from './selectUtils'; export * from './selectUtils';
export * from './store';
export * from './LocalStorageValueProvider';
export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUIBuilders'; export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUIBuilders';
export { arrayUtils }; export { arrayUtils };
export { getFlotPairs, getFlotPairsConstant } from './flotPairs'; export { getFlotPairs, getFlotPairsConstant } from './flotPairs';

@ -266,7 +266,7 @@ export const Components = {
}, },
}, },
Tab: { Tab: {
title: (title: string) => `Tab ${title}`, title: (title: string) => `data-testid Tab ${title}`,
active: () => '[class*="-activeTabStyle"]', active: () => '[class*="-activeTabStyle"]',
}, },
RefreshPicker: { RefreshPicker: {

@ -66,6 +66,7 @@ export const Pages = {
container: 'data-testid new share button menu', container: 'data-testid new share button menu',
shareInternally: 'data-testid new share button share internally', shareInternally: 'data-testid new share button share internally',
shareExternally: 'data-testid new share button share externally', shareExternally: 'data-testid new share button share externally',
shareSnapshot: 'data-testid new share button share snapshot',
}, },
}, },
playlistControls: { playlistControls: {
@ -103,7 +104,7 @@ export const Pages = {
* @deprecated use components.TimeZonePicker.containerV2 from Grafana 8.3 instead * @deprecated use components.TimeZonePicker.containerV2 from Grafana 8.3 instead
*/ */
timezone: 'Time zone picker select container', timezone: 'Time zone picker select container',
title: 'Tab General', title: 'General',
}, },
Annotations: { Annotations: {
List: { List: {
@ -245,7 +246,6 @@ export const Pages = {
}, },
ShareDashboardModal: { ShareDashboardModal: {
PublicDashboard: { PublicDashboard: {
Tab: 'Tab Public dashboard',
WillBePublicCheckbox: 'data-testid public dashboard will be public checkbox', WillBePublicCheckbox: 'data-testid public dashboard will be public checkbox',
LimitedDSCheckbox: 'data-testid public dashboard limited datasources checkbox', LimitedDSCheckbox: 'data-testid public dashboard limited datasources checkbox',
CostIncreaseCheckbox: 'data-testid public dashboard cost may increase checkbox', CostIncreaseCheckbox: 'data-testid public dashboard cost may increase checkbox',
@ -270,12 +270,8 @@ export const Pages = {
ReshareLink: 'data-testid public dashboard reshare link button', ReshareLink: 'data-testid public dashboard reshare link button',
}, },
}, },
PublicDashboardScene: {
Tab: 'Tab Public Dashboard',
},
SnapshotScene: { SnapshotScene: {
url: (key: string) => `/dashboard/snapshot/${key}`, url: (key: string) => `/dashboard/snapshot/${key}`,
Tab: 'Tab Snapshot',
PublishSnapshot: 'data-testid publish snapshot button', PublishSnapshot: 'data-testid publish snapshot button',
CopyUrlButton: 'data-testid snapshot copy url button', CopyUrlButton: 'data-testid snapshot copy url button',
CopyUrlInput: 'data-testid snapshot copy url input', CopyUrlInput: 'data-testid snapshot copy url input',
@ -287,6 +283,9 @@ export const Pages = {
copyUrlButton: 'data-testid share externally copy url button', copyUrlButton: 'data-testid share externally copy url button',
shareTypeSelect: 'data-testid share externally share type select', shareTypeSelect: 'data-testid share externally share type select',
}, },
ShareSnapshot: {
container: 'data-testid share snapshot drawer container',
},
}, },
PublicDashboard: { PublicDashboard: {
page: 'public-dashboard-page', page: 'public-dashboard-page',

@ -13,6 +13,4 @@ export * from './TraceToLogs/TraceToLogsSettings';
export * from './TraceToMetrics/TraceToMetricsSettings'; export * from './TraceToMetrics/TraceToMetricsSettings';
export * from './TraceToProfiles/TraceToProfilesSettings'; export * from './TraceToProfiles/TraceToProfilesSettings';
export * from './utils'; export * from './utils';
export * from './store';
export * from './LocalStorageValueProvider/LocalStorageValueProvider';
export * from './combineResponses'; export * from './combineResponses';

@ -1,6 +0,0 @@
// reset font file paths so storybook loads them based on
// staticDirs defined in packages/grafana-ui/.storybook/main.ts
$font-file-path: './public/fonts';
$fa-font-path: $font-file-path;
@import '../../../public/sass/grafana.dark.scss';

@ -1,6 +0,0 @@
// reset font file paths so storybook loads them based on
// staticDirs defined in packages/grafana-ui/.storybook/main.ts
$font-file-path: './public/fonts';
$fa-font-path: $font-file-path;
@import '../../../public/sass/grafana.light.scss';

@ -17,9 +17,9 @@ import { withTimeZone } from '../src/utils/storybook/withTimeZone';
import { ThemedDocsContainer } from '../src/utils/storybook/ThemedDocsContainer'; import { ThemedDocsContainer } from '../src/utils/storybook/ThemedDocsContainer';
// @ts-ignore // @ts-ignore
import lightTheme from './grafana.light.scss'; import lightTheme from '../../../public/sass/grafana.light.scss';
// @ts-ignore // @ts-ignore
import darkTheme from './grafana.dark.scss'; import darkTheme from '../../../public/sass/grafana.dark.scss';
import { GrafanaDark, GrafanaLight } from './storybookTheme'; import { GrafanaDark, GrafanaLight } from './storybookTheme';
const handleThemeChange = (theme: any) => { const handleThemeChange = (theme: any) => {

@ -43,9 +43,9 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
const commonProps = { const commonProps = {
className: linkClass, className: linkClass,
'data-testid': selectors.components.Tab.title(label),
...otherProps, ...otherProps,
onClick: onChangeTab, onClick: onChangeTab,
'aria-label': otherProps['aria-label'] || selectors.components.Tab.title(label),
role: 'tab', role: 'tab',
'aria-selected': active, 'aria-selected': active,
}; };

@ -231,6 +231,7 @@ export { FieldArray } from './Forms/FieldArray';
// Select // Select
export { default as resetSelectStyles } from './Select/resetSelectStyles'; export { default as resetSelectStyles } from './Select/resetSelectStyles';
export * from './Select/Select'; export * from './Select/Select';
export { SelectMenuOptions } from './Select/SelectMenu';
export { getSelectStyles } from './Select/getSelectStyles'; export { getSelectStyles } from './Select/getSelectStyles';
export * from './Select/types'; export * from './Select/types';

@ -9,6 +9,7 @@ import { getCardStyles } from './card';
import { getCodeStyles } from './code'; import { getCodeStyles } from './code';
import { getElementStyles } from './elements'; import { getElementStyles } from './elements';
import { getExtraStyles } from './extra'; import { getExtraStyles } from './extra';
import { getFontAwesomeStyles } from './fontAwesome';
import { getFontStyles } from './fonts'; import { getFontStyles } from './fonts';
import { getFormElementStyles } from './forms'; import { getFormElementStyles } from './forms';
import { getJsonFormatterStyles } from './jsonFormatter'; import { getJsonFormatterStyles } from './jsonFormatter';
@ -31,6 +32,7 @@ export function GlobalStyles() {
getCodeStyles(theme), getCodeStyles(theme),
getElementStyles(theme), getElementStyles(theme),
getExtraStyles(theme), getExtraStyles(theme),
getFontAwesomeStyles(theme),
getFontStyles(theme), getFontStyles(theme),
getFormElementStyles(theme), getFormElementStyles(theme),
getJsonFormatterStyles(theme), getJsonFormatterStyles(theme),

File diff suppressed because it is too large Load Diff

@ -3,6 +3,9 @@ import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
export function getFontStyles(theme: GrafanaTheme2) { export function getFontStyles(theme: GrafanaTheme2) {
const grafanaPublicPath = typeof window !== 'undefined' && window.__grafana_public_path__;
const fontRoot = grafanaPublicPath ? `${grafanaPublicPath}fonts/` : 'public/fonts/';
return css([ return css([
{ {
/* latin */ /* latin */
@ -11,7 +14,7 @@ export function getFontStyles(theme: GrafanaTheme2) {
fontStyle: 'normal', fontStyle: 'normal',
fontWeight: 400, fontWeight: 400,
fontDisplay: 'swap', fontDisplay: 'swap',
src: "url('./public/fonts/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')", src: `url('${fontRoot}roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')`,
unicodeRange: unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
}, },
@ -23,7 +26,7 @@ export function getFontStyles(theme: GrafanaTheme2) {
fontStyle: 'normal', fontStyle: 'normal',
fontWeight: 500, fontWeight: 500,
fontDisplay: 'swap', fontDisplay: 'swap',
src: "url('./public/fonts/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')", src: `url('${fontRoot}roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')`,
unicodeRange: unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
}, },
@ -42,7 +45,7 @@ export function getFontStyles(theme: GrafanaTheme2) {
fontStyle: 'normal', fontStyle: 'normal',
fontWeight: 400, fontWeight: 400,
fontDisplay: 'swap', fontDisplay: 'swap',
src: "url('./public/fonts/inter/Inter-Regular.woff2') format('woff2')", src: `url('${fontRoot}inter/Inter-Regular.woff2') format('woff2')`,
}, },
}, },
{ {
@ -51,7 +54,7 @@ export function getFontStyles(theme: GrafanaTheme2) {
fontStyle: 'normal', fontStyle: 'normal',
fontWeight: 500, fontWeight: 500,
fontDisplay: 'swap', fontDisplay: 'swap',
src: "url('./public/fonts/inter/Inter-Medium.woff2') format('woff2')", src: `url('${fontRoot}inter/Inter-Medium.woff2') format('woff2')`,
}, },
}, },
]); ]);

@ -373,13 +373,18 @@ func (proxy *DataSourceProxy) logRequest() {
panelPluginId := proxy.ctx.Req.Header.Get("X-Panel-Plugin-Id") panelPluginId := proxy.ctx.Req.Header.Get("X-Panel-Plugin-Id")
uri, err := util.SanitizeURI(proxy.ctx.Req.RequestURI)
if err == nil {
proxy.ctx.Logger.Error("Could not sanitize RequestURI", "error", err)
}
ctxLogger := logger.FromContext(proxy.ctx.Req.Context()) ctxLogger := logger.FromContext(proxy.ctx.Req.Context())
ctxLogger.Info("Proxying incoming request", ctxLogger.Info("Proxying incoming request",
"userid", proxy.ctx.UserID, "userid", proxy.ctx.UserID,
"orgid", proxy.ctx.OrgID, "orgid", proxy.ctx.OrgID,
"username", proxy.ctx.Login, "username", proxy.ctx.Login,
"datasource", proxy.ds.Type, "datasource", proxy.ds.Type,
"uri", proxy.ctx.Req.RequestURI, "uri", uri,
"method", proxy.ctx.Req.Method, "method", proxy.ctx.Req.Method,
"panelPluginId", panelPluginId, "panelPluginId", panelPluginId,
"body", body) "body", body)

@ -54,7 +54,7 @@ func (d *DualWriterMode1) Create(ctx context.Context, original runtime.Object, c
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage create timeout")) ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage create timeout"))
defer cancel() defer cancel()
if err := enrichLegacyObject(original, createdCopy, true); err != nil { if err := enrichLegacyObject(original, createdCopy); err != nil {
cancel() cancel()
} }
@ -201,7 +201,7 @@ func (d *DualWriterMode1) Update(ctx context.Context, name string, objInfo rest.
// if the object is found, create a new updateWrapper with the object found // if the object is found, create a new updateWrapper with the object found
if foundObj != nil { if foundObj != nil {
if err := enrichLegacyObject(foundObj, resCopy, false); err != nil { if err := enrichLegacyObject(foundObj, resCopy); err != nil {
log.Error(err, "could not enrich object") log.Error(err, "could not enrich object")
cancel() cancel()
} }

@ -50,7 +50,7 @@ func (d *DualWriterMode2) Create(ctx context.Context, original runtime.Object, c
} }
d.recordLegacyDuration(false, mode2Str, options.Kind, method, startLegacy) d.recordLegacyDuration(false, mode2Str, options.Kind, method, startLegacy)
if err := enrichLegacyObject(original, created, true); err != nil { if err := enrichLegacyObject(original, created); err != nil {
return created, err return created, err
} }
@ -261,7 +261,7 @@ func (d *DualWriterMode2) Update(ctx context.Context, name string, objInfo rest.
// if the object is found, create a new updateWrapper with the object found // if the object is found, create a new updateWrapper with the object found
if foundObj != nil { if foundObj != nil {
err = enrichLegacyObject(foundObj, obj, false) err = enrichLegacyObject(foundObj, obj)
if err != nil { if err != nil {
return obj, false, err return obj, false, err
} }
@ -328,7 +328,7 @@ func parseList(legacyList []runtime.Object) (metainternalversion.ListOptions, ma
return options, indexMap, nil return options, indexMap, nil
} }
func enrichLegacyObject(originalObj, returnedObj runtime.Object, created bool) error { func enrichLegacyObject(originalObj, returnedObj runtime.Object) error {
accessorReturned, err := meta.Accessor(returnedObj) accessorReturned, err := meta.Accessor(returnedObj)
if err != nil { if err != nil {
return err return err
@ -350,13 +350,6 @@ func enrichLegacyObject(originalObj, returnedObj runtime.Object, created bool) e
} }
accessorReturned.SetAnnotations(ac) accessorReturned.SetAnnotations(ac)
// if the object is created, we need to reset the resource version and UID
// create method expects an empty resource version
if created {
accessorReturned.SetResourceVersion("")
accessorReturned.SetUID("")
return nil
}
// otherwise, we propagate the original RV and UID // otherwise, we propagate the original RV and UID
accessorReturned.SetResourceVersion(accessorOriginal.GetResourceVersion()) accessorReturned.SetResourceVersion(accessorOriginal.GetResourceVersion())
accessorReturned.SetUID(accessorOriginal.GetUID()) accessorReturned.SetUID(accessorOriginal.GetUID())

@ -79,7 +79,7 @@ func TestMode2_Create(t *testing.T) {
assert.Equal(t, exampleObj, obj) assert.Equal(t, exampleObj, obj)
accessor, err := meta.Accessor(obj) accessor, err := meta.Accessor(obj)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, accessor.GetResourceVersion(), "") assert.Equal(t, accessor.GetResourceVersion(), "1")
}) })
} }
} }
@ -493,83 +493,7 @@ func TestEnrichReturnedObject(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{ {
name: "create: original object does not have labels and annotations", name: "original object does not have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5")},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
inputReturned: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "2", UID: types.UID("6"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
isCreated: true,
expectedObject: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", UID: types.UID("")},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
},
{
name: "create: returned object does not have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
inputReturned: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "2", UID: types.UID("6")},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
isCreated: true,
expectedObject: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", UID: types.UID(""), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
},
{
name: "create: both objects have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
inputReturned: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "2", UID: types.UID("6"), Labels: map[string]string{"label2": "2"}, Annotations: map[string]string{"annotation2": "2"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
isCreated: true,
expectedObject: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", UID: types.UID(""), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
},
{
name: "create: both objects have labels and annotations with duplicated keys",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
inputReturned: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "2", UID: types.UID("6"), Labels: map[string]string{"label1": "11"}, Annotations: map[string]string{"annotation1": "11"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
isCreated: true,
expectedObject: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", UID: types.UID(""), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
},
{
name: "update: original object does not have labels and annotations",
inputOriginal: &example.Pod{ inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"}, TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5")}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5")},
@ -587,7 +511,7 @@ func TestEnrichReturnedObject(t *testing.T) {
}, },
}, },
{ {
name: "update: returned object does not have labels and annotations", name: "returned object does not have labels and annotations",
inputOriginal: &example.Pod{ inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"}, TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
@ -605,7 +529,7 @@ func TestEnrichReturnedObject(t *testing.T) {
}, },
}, },
{ {
name: "update: both objects have labels and annotations", name: "both objects have labels and annotations",
inputOriginal: &example.Pod{ inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"}, TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
@ -623,7 +547,7 @@ func TestEnrichReturnedObject(t *testing.T) {
}, },
}, },
{ {
name: "update: both objects have labels and annotations with duplicated keys", name: "both objects have labels and annotations with duplicated keys",
inputOriginal: &example.Pod{ inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"}, TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
@ -658,7 +582,7 @@ func TestEnrichReturnedObject(t *testing.T) {
for _, tt := range testCase { for _, tt := range testCase {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := enrichLegacyObject(tt.inputOriginal, tt.inputReturned, tt.isCreated) err := enrichLegacyObject(tt.inputOriginal, tt.inputReturned)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
return return

@ -19,11 +19,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"time" "time"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/errutil"
@ -113,7 +113,7 @@ func (l *loggerImpl) prepareLogParams(c *contextmodel.ReqContext, duration time.
"size", rw.Size(), "size", rw.Size(),
} }
referer, err := SanitizeURL(r.Referer()) referer, err := util.SanitizeURI(r.Referer())
// We add an empty referer when there's a parsing error, hence this is before the err check. // We add an empty referer when there's a parsing error, hence this is before the err check.
logParams = append(logParams, "referer", referer) logParams = append(logParams, "referer", referer)
if err != nil { if err != nil {
@ -153,27 +153,3 @@ func errorLogParams(err error) []any {
"error", gfErr.LogMessage, "error", gfErr.LogMessage,
} }
} }
var sensitiveQueryStrings = [...]string{
"auth_token",
}
func SanitizeURL(s string) (string, error) {
if s == "" {
return s, nil
}
u, err := url.ParseRequestURI(s)
if err != nil {
return "", fmt.Errorf("failed to sanitize URL")
}
// strip out sensitive query strings
values := u.Query()
for _, query := range sensitiveQueryStrings {
values.Del(query)
}
u.RawQuery = values.Encode()
return u.String(), nil
}

@ -16,43 +16,6 @@ import (
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
func Test_sanitizeURL(t *testing.T) {
tests := []struct {
name string
input string
want string
expectError bool
}{
{
name: "Receiving empty string should return it",
input: "",
want: "",
},
{
name: "Receiving valid URL string should return it parsed",
input: "https://grafana.com/",
want: "https://grafana.com/",
},
{
name: "Receiving invalid URL string should return empty string",
input: "this is not a valid URL",
want: "",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url, err := SanitizeURL(tt.input)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equalf(t, tt.want, url, "SanitizeURL(%v)", tt.input)
})
}
}
func Test_prepareLog(t *testing.T) { func Test_prepareLog(t *testing.T) {
type opts struct { type opts struct {
Features []any Features []any

@ -17,6 +17,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
@ -77,7 +78,7 @@ func (r *queryREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, "" // true means you can use the trailing path as a variable return nil, false, "" // true means you can use the trailing path as a variable
} }
func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Object, incomingResponder rest.Responder) (http.Handler, error) { func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.Object, incomingResponder rest.Responder) (http.Handler, error) {
// See: /pkg/apiserver/builder/helper.go#L34 // See: /pkg/apiserver/builder/helper.go#L34
// The name is set with a rewriter hack // The name is set with a rewriter hack
if name != "name" { if name != "name" {
@ -88,6 +89,7 @@ func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Objec
return http.HandlerFunc(func(w http.ResponseWriter, httpreq *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, httpreq *http.Request) {
ctx, span := b.tracer.Start(httpreq.Context(), "QueryService.Query") ctx, span := b.tracer.Start(httpreq.Context(), "QueryService.Query")
defer span.End() defer span.End()
ctx = request.WithNamespace(ctx, request.NamespaceValue(connectCtx))
responder := newResponderWrapper(incomingResponder, responder := newResponderWrapper(incomingResponder,
func(statusCode int, obj runtime.Object) { func(statusCode int, obj runtime.Object) {
@ -116,7 +118,6 @@ func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Objec
responder.Error(err) responder.Error(err)
return return
} }
// Parses the request and splits it into multiple sub queries (if necessary) // Parses the request and splits it into multiple sub queries (if necessary)
req, err := b.parser.parseRequest(ctx, raw) req, err := b.parser.parseRequest(ctx, raw)
if err != nil { if err != nil {

@ -99,15 +99,6 @@ var (
Expression: "true", // enabled by default Expression: "true", // enabled by default
AllowSelfServe: true, AllowSelfServe: true,
}, },
{
Name: "exploreContentOutline",
Description: "Content outline sidebar",
Stage: FeatureStageGeneralAvailability,
Owner: grafanaExploreSquad,
Expression: "true", // enabled by default
FrontendOnly: true,
AllowSelfServe: true,
},
{ {
Name: "datasourceQueryMultiStatus", Name: "datasourceQueryMultiStatus",
Description: "Introduce HTTP 207 Multi Status for api/ds/query", Description: "Introduce HTTP 207 Multi Status for api/ds/query",

@ -10,7 +10,6 @@ lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,f
featureHighlights,GA,@grafana/grafana-as-code,false,false,false featureHighlights,GA,@grafana/grafana-as-code,false,false,false
storage,experimental,@grafana/grafana-app-platform-squad,false,false,false storage,experimental,@grafana/grafana-app-platform-squad,false,false,false
correlations,GA,@grafana/explore-squad,false,false,false correlations,GA,@grafana/explore-squad,false,false,false
exploreContentOutline,GA,@grafana/explore-squad,false,false,true
datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false
autoMigrateOldPanels,preview,@grafana/dataviz-squad,false,false,true autoMigrateOldPanels,preview,@grafana/dataviz-squad,false,false,true
autoMigrateGraphPanel,preview,@grafana/dataviz-squad,false,false,true autoMigrateGraphPanel,preview,@grafana/dataviz-squad,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
10 featureHighlights GA @grafana/grafana-as-code false false false
11 storage experimental @grafana/grafana-app-platform-squad false false false
12 correlations GA @grafana/explore-squad false false false
exploreContentOutline GA @grafana/explore-squad false false true
13 datasourceQueryMultiStatus experimental @grafana/plugins-platform-backend false false false
14 autoMigrateOldPanels preview @grafana/dataviz-squad false false true
15 autoMigrateGraphPanel preview @grafana/dataviz-squad false false true

@ -51,10 +51,6 @@ const (
// Correlations page // Correlations page
FlagCorrelations = "correlations" FlagCorrelations = "correlations"
// FlagExploreContentOutline
// Content outline sidebar
FlagExploreContentOutline = "exploreContentOutline"
// FlagDatasourceQueryMultiStatus // FlagDatasourceQueryMultiStatus
// Introduce HTTP 207 Multi Status for api/ds/query // Introduce HTTP 207 Multi Status for api/ds/query
FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus" FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus"

@ -841,8 +841,9 @@
{ {
"metadata": { "metadata": {
"name": "exploreContentOutline", "name": "exploreContentOutline",
"resourceVersion": "1718727528075", "resourceVersion": "1717578796182",
"creationTimestamp": "2023-10-13T16:57:13Z" "creationTimestamp": "2023-10-13T16:57:13Z",
"deletionTimestamp": "2024-06-17T09:45:00Z"
}, },
"spec": { "spec": {
"description": "Content outline sidebar", "description": "Content outline sidebar",

@ -292,7 +292,7 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"}, "grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"},
"grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"}, "grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"},
"grafana-oncall-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "OnCall"}, "grafana-oncall-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "OnCall"},
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incidents"}, "grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incident"},
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"}, "grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"},
"grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 4}, "grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 4},
"grafana-cloud-link-app": {SectionID: navtree.NavIDCfgPlugins, SortWeight: 3}, "grafana-cloud-link-app": {SectionID: navtree.NavIDCfgPlugins, SortWeight: 3},

@ -301,15 +301,15 @@ func (s *Service) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLast
return err return err
} }
if !shouldUpdateLastSeen(u.LastSeenAt) { if !s.shouldUpdateLastSeen(u.LastSeenAt) {
return user.ErrLastSeenUpToDate return user.ErrLastSeenUpToDate
} }
return s.store.UpdateLastSeenAt(ctx, cmd) return s.store.UpdateLastSeenAt(ctx, cmd)
} }
func shouldUpdateLastSeen(t time.Time) bool { func (s *Service) shouldUpdateLastSeen(t time.Time) bool {
return time.Since(t) > time.Minute*15 return time.Since(t) > s.cfg.UserLastSeenUpdateInterval
} }
func (s *Service) GetSignedInUser(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) { func (s *Service) GetSignedInUser(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {

@ -221,6 +221,7 @@ func TestUpdateLastSeenAt(t *testing.T) {
tracer: tracing.InitializeTracerForTest(), tracer: tracing.InitializeTracerForTest(),
} }
userService.cfg = setting.NewCfg() userService.cfg = setting.NewCfg()
userService.cfg.UserLastSeenUpdateInterval = 5 * time.Minute
t.Run("update last seen at", func(t *testing.T) { t.Run("update last seen at", func(t *testing.T) {
userStore.ExpectedSignedInUser = &user.SignedInUser{UserID: 1, OrgID: 1, Email: "email", Login: "login", Name: "name", LastSeenAt: time.Now().Add(-20 * time.Minute)} userStore.ExpectedSignedInUser = &user.SignedInUser{UserID: 1, OrgID: 1, Email: "email", Login: "login", Name: "name", LastSeenAt: time.Now().Add(-20 * time.Minute)}

@ -306,6 +306,7 @@ type Cfg struct {
UserInviteMaxLifetime time.Duration UserInviteMaxLifetime time.Duration
HiddenUsers map[string]struct{} HiddenUsers map[string]struct{}
CaseInsensitiveLogin bool // Login and Email will be considered case insensitive CaseInsensitiveLogin bool // Login and Email will be considered case insensitive
UserLastSeenUpdateInterval time.Duration
VerificationEmailMaxLifetime time.Duration VerificationEmailMaxLifetime time.Duration
// Service Accounts // Service Accounts
@ -1695,6 +1696,19 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
return errors.New("the minimum supported value for the `user_invite_max_lifetime_duration` configuration is 15m (15 minutes)") return errors.New("the minimum supported value for the `user_invite_max_lifetime_duration` configuration is 15m (15 minutes)")
} }
cfg.UserLastSeenUpdateInterval, err = gtime.ParseDuration(valueAsString(users, "last_seen_update_interval", "15m"))
if err != nil {
return err
}
if cfg.UserLastSeenUpdateInterval < time.Minute*5 {
cfg.Logger.Warn("the minimum supported value for the `last_seen_update_interval` configuration is 5m (5 minutes)")
cfg.UserLastSeenUpdateInterval = time.Minute * 5
} else if cfg.UserLastSeenUpdateInterval > time.Hour*1 {
cfg.Logger.Warn("the maximum supported value for the `last_seen_update_interval` configuration is 1h (1 hour)")
cfg.UserLastSeenUpdateInterval = time.Hour * 1
}
cfg.HiddenUsers = make(map[string]struct{}) cfg.HiddenUsers = make(map[string]struct{})
hiddenUsers := users.Key("hidden_users").MustString("") hiddenUsers := users.Key("hidden_users").MustString("")
for _, user := range strings.Split(hiddenUsers, ",") { for _, user := range strings.Split(hiddenUsers, ",") {

@ -0,0 +1,54 @@
package util
import (
"fmt"
"net/url"
"strings"
)
const masking = "hidden"
var sensitiveQueryChecks = map[string]func(key string, urlValues url.Values) bool{
"auth_token": func(key string, urlValues url.Values) bool {
return true
},
"x-amz-signature": func(key string, urlValues url.Values) bool {
return true
},
"x-goog-signature": func(key string, urlValues url.Values) bool {
return true
},
"sig": func(key string, urlValues url.Values) bool {
for k := range urlValues {
if strings.ToLower(k) == "sv" {
return true
}
}
return false
},
}
func SanitizeURI(s string) (string, error) {
if s == "" {
return s, nil
}
u, err := url.ParseRequestURI(s)
if err != nil {
return "", fmt.Errorf("failed to sanitize URL")
}
// strip out sensitive query strings
urlValues := u.Query()
for key := range urlValues {
lk := strings.ToLower(key)
if checker, ok := sensitiveQueryChecks[lk]; ok {
if checker(key, urlValues) {
urlValues.Set(key, masking)
}
}
}
u.RawQuery = urlValues.Encode()
return u.String(), nil
}

@ -0,0 +1,74 @@
package util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_sanitizeURI(t *testing.T) {
tests := []struct {
name string
input string
want string
expectError bool
}{
{
name: "Receiving empty string should return it",
input: "",
want: "",
},
{
name: "Receiving URL with auth_token should remove it",
input: "https://grafana.com/?auth_token=secret-token&q=1234",
want: "https://grafana.com/?auth_token=hidden&q=1234",
},
{
name: "Receiving presigned URL from AWS should remove signature",
input: "https://s3.amazonaws.com/finance-department-bucket/2022/tax-certificate.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3SGQVQG7FGA6KKA6%2F20221104%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221104T140227Z&X-Amz-Expires=3600&X-Amz-Signature=b22&X-Amz-SignedHeaders=host",
want: "https://s3.amazonaws.com/finance-department-bucket/2022/tax-certificate.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3SGQVQG7FGA6KKA6%2F20221104%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221104T140227Z&X-Amz-Expires=3600&X-Amz-Signature=hidden&X-Amz-SignedHeaders=host",
},
{
name: "Receiving presigned URL from GCP should remove signature",
input: "https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com%2F20181026%2Fus-central1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T181309Z&X-Goog-Expires=900&X-Goog-Signature=247a&X-Goog-SignedHeaders=host",
want: "https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com%2F20181026%2Fus-central1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T181309Z&X-Goog-Expires=900&X-Goog-Signature=hidden&X-Goog-SignedHeaders=host",
},
{
name: "Receiving presigned URL with lower case query params from GCP should remove signature",
input: "https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com%2F20181026%2Fus-central1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T181309Z&X-Goog-Expires=900&x-goog-signature=247a&X-Goog-SignedHeaders=host",
want: "https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com%2F20181026%2Fus-central1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T181309Z&X-Goog-Expires=900&X-Goog-SignedHeaders=host&x-goog-signature=hidden",
},
{
name: "Receiving presigned URL from Azure should remove signature",
input: "https://myaccount.queue.core.windows.net/myqueue/messages?se=2015-07-02T08%3A49Z&si=YWJjZGVmZw%3D%3D&sig=jDrr6cna7JPwIaxWfdH0tT5v9dc%3d&sp=p&st=2015-07-01T08%3A49Z&sv=2015-02-21&visibilitytimeout=120",
want: "https://myaccount.queue.core.windows.net/myqueue/messages?se=2015-07-02T08%3A49Z&si=YWJjZGVmZw%3D%3D&sig=hidden&sp=p&st=2015-07-01T08%3A49Z&sv=2015-02-21&visibilitytimeout=120",
},
{
name: "Receiving presigned URL from Azure with upper case query values should remove signature",
input: "https://myaccount.queue.core.windows.net/myqueue/messages?se=2015-07-02T08%3A49Z&si=YWJjZGVmZw%3D%3D&SIG=jDrr6cna7JPwIaxWfdH0tT5v9dc%3d&sp=p&st=2015-07-01T08%3A49Z&SV=2015-02-21&visibilitytimeout=120",
want: "https://myaccount.queue.core.windows.net/myqueue/messages?SIG=hidden&SV=2015-02-21&se=2015-07-02T08%3A49Z&si=YWJjZGVmZw%3D%3D&sp=p&st=2015-07-01T08%3A49Z&visibilitytimeout=120",
},
{
name: "Receiving valid URL string should return it parsed",
input: "https://grafana.com/?sig=testing-a-generic-parameter",
want: "https://grafana.com/?sig=testing-a-generic-parameter",
},
{
name: "Receiving invalid URL string should return empty string",
input: "this is not a valid URL",
want: "",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url, err := SanitizeURI(tt.input)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equalf(t, tt.want, url, "SanitizeURI(%v)", tt.input)
})
}
}

@ -137,7 +137,7 @@ export function getNavTitle(navId: string | undefined) {
case 'testing-and-synthetics': case 'testing-and-synthetics':
return t('nav.testing-and-synthetics.title', 'Testing & synthetics'); return t('nav.testing-and-synthetics.title', 'Testing & synthetics');
case 'plugin-page-grafana-incident-app': case 'plugin-page-grafana-incident-app':
return t('nav.incidents.title', 'Incidents'); return t('nav.incidents.title', 'Incident');
case 'plugin-page-grafana-ml-app': case 'plugin-page-grafana-ml-app':
return t('nav.machine-learning.title', 'Machine learning'); return t('nav.machine-learning.title', 'Machine learning');
case 'plugin-page-grafana-slo-app': case 'plugin-page-grafana-slo-app':

@ -689,7 +689,7 @@ describe('RuleList', () => {
expect(alertsInReorder).toHaveLength(2); expect(alertsInReorder).toHaveLength(2);
}); });
describe('pausing rules', () => { describe.skip('pausing rules', () => {
beforeEach(() => { beforeEach(() => {
grantUserPermissions([ grantUserPermissions([
AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleRead,

@ -8,11 +8,9 @@ import {
Annotations, Annotations,
GrafanaAlertStateDecision, GrafanaAlertStateDecision,
Labels, Labels,
PostableRuleGrafanaRuleDTO, PostableRulerRuleGroupDTO,
PromRulesResponse, PromRulesResponse,
RulerAlertingRuleDTO,
RulerGrafanaRuleDTO, RulerGrafanaRuleDTO,
RulerRecordingRuleDTO,
RulerRuleGroupDTO, RulerRuleGroupDTO,
RulerRulesConfigDTO, RulerRulesConfigDTO,
} from 'app/types/unified-alerting-dto'; } from 'app/types/unified-alerting-dto';
@ -77,14 +75,7 @@ interface ExportRulesParams {
ruleUid?: string; ruleUid?: string;
} }
export interface ModifyExportPayload { export interface AlertGroupUpdated {
rules: Array<RulerAlertingRuleDTO | RulerRecordingRuleDTO | PostableRuleGrafanaRuleDTO>;
name: string;
interval?: string | undefined;
source_tenants?: string[] | undefined;
}
export interface AlertRuleUpdated {
message: string; message: string;
/** /**
* UIDs of rules updated from this request * UIDs of rules updated from this request
@ -220,7 +211,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({
}), }),
// TODO This should be probably a separate ruler API file // TODO This should be probably a separate ruler API file
rulerRuleGroup: build.query< getRuleGroupForNamespace: build.query<
RulerRuleGroupDTO, RulerRuleGroupDTO,
{ rulerConfig: RulerDataSourceConfig; namespace: string; group: string } { rulerConfig: RulerDataSourceConfig; namespace: string; group: string }
>({ >({
@ -231,6 +222,17 @@ export const alertRuleApi = alertingApi.injectEndpoints({
providesTags: ['CombinedAlertRule'], providesTags: ['CombinedAlertRule'],
}), }),
deleteRuleGroupFromNamespace: build.mutation<
RulerRuleGroupDTO,
{ rulerConfig: RulerDataSourceConfig; namespace: string; group: string }
>({
query: ({ rulerConfig, namespace, group }) => {
const { path, params } = rulerUrlBuilder(rulerConfig).namespaceGroup(namespace, group);
return { url: path, params, method: 'DELETE' };
},
invalidatesTags: ['CombinedAlertRule'],
}),
getAlertRule: build.query<RulerGrafanaRuleDTO, { uid: string }>({ getAlertRule: build.query<RulerGrafanaRuleDTO, { uid: string }>({
// TODO: In future, if supported in other rulers, parametrize ruler source name // TODO: In future, if supported in other rulers, parametrize ruler source name
// For now, to make the consumption of this hook clearer, only support Grafana ruler // For now, to make the consumption of this hook clearer, only support Grafana ruler
@ -272,7 +274,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({
}), }),
exportModifiedRuleGroup: build.mutation< exportModifiedRuleGroup: build.mutation<
string, string,
{ payload: ModifyExportPayload; format: ExportFormats; nameSpaceUID: string } { payload: PostableRulerRuleGroupDTO; format: ExportFormats; nameSpaceUID: string }
>({ >({
query: ({ payload, format, nameSpaceUID }) => ({ query: ({ payload, format, nameSpaceUID }) => ({
url: `/api/ruler/grafana/api/v1/rules/${nameSpaceUID}/export/`, url: `/api/ruler/grafana/api/v1/rules/${nameSpaceUID}/export/`,
@ -298,13 +300,20 @@ export const alertRuleApi = alertingApi.injectEndpoints({
}), }),
keepUnusedDataFor: 0, keepUnusedDataFor: 0,
}), }),
updateRuleGroupForNamespace: build.mutation<
AlertGroupUpdated,
{ rulerConfig: RulerDataSourceConfig; namespace: string; payload: PostableRulerRuleGroupDTO }
>({
query: ({ payload, namespace, rulerConfig }) => {
const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace);
updateRule: build.mutation<AlertRuleUpdated, { nameSpaceUID: string; payload: ModifyExportPayload }>({ return {
query: ({ payload, nameSpaceUID }) => ({ url: path,
url: `/api/ruler/grafana/api/v1/rules/${nameSpaceUID}/`, params,
data: payload, data: payload,
method: 'POST', method: 'POST',
}), };
},
invalidatesTags: ['CombinedAlertRule'], invalidatesTags: ['CombinedAlertRule'],
}), }),
}), }),

@ -1,13 +1,16 @@
import { produce } from 'immer';
import React from 'react'; import React from 'react';
import { Menu } from '@grafana/ui'; import { Menu } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi'; import {
import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from 'app/features/alerting/unified/utils/rules'; isGrafanaRulerRule,
isGrafanaRulerRulePaused,
getRuleGroupLocationFromCombinedRule,
} from 'app/features/alerting/unified/utils/rules';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { grafanaRulerConfig } from '../hooks/useCombinedRule'; import { usePauseRuleInGroup } from '../hooks/useProduceNewRuleGroup';
import { stringifyErrorLike } from '../utils/misc';
interface Props { interface Props {
rule: CombinedRule; rule: CombinedRule;
@ -22,12 +25,9 @@ interface Props {
* and triggering API call to do so * and triggering API call to do so
*/ */
const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => { const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => {
// we need to fetch the group again, as maybe the group has been filtered
const [getGroup] = alertRuleApi.endpoints.rulerRuleGroup.useLazyQuery();
const notifyApp = useAppNotification(); const notifyApp = useAppNotification();
const [pauseRule, updateState] = usePauseRuleInGroup();
// Add any dependencies here
const [updateRule] = alertRuleApi.endpoints.updateRule.useMutation();
const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule); const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule);
const icon = isPaused ? 'play' : 'pause'; const icon = isPaused ? 'play' : 'pause';
const title = isPaused ? 'Resume evaluation' : 'Pause evaluation'; const title = isPaused ? 'Resume evaluation' : 'Pause evaluation';
@ -39,40 +39,16 @@ const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => {
if (!isGrafanaRulerRule(rule.rulerRule)) { if (!isGrafanaRulerRule(rule.rulerRule)) {
return; return;
} }
const ruleUid = rule.rulerRule.grafana_alert.uid;
const targetGroup = await getGroup({
rulerConfig: grafanaRulerConfig,
namespace: rule.namespace.uid || rule.rulerRule.grafana_alert.namespace_uid,
group: rule.group.name,
}).unwrap();
if (!targetGroup) { try {
notifyApp.error( const ruleGroupId = getRuleGroupLocationFromCombinedRule(rule);
`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule. Could not get the target group to update the rule.` const ruleUID = rule.rulerRule.grafana_alert.uid;
);
return;
}
// Parse the rules into correct format for API await pauseRule(ruleGroupId, ruleUID, newIsPaused);
const modifiedRules = targetGroup.rules.map((groupRule) => { } catch (error) {
if (!(isGrafanaRulerRule(groupRule) && groupRule.grafana_alert.uid === ruleUid)) { notifyApp.error(`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule: ${stringifyErrorLike(error)}`);
return groupRule; return;
} }
return produce(groupRule, (updatedGroupRule) => {
updatedGroupRule.grafana_alert.is_paused = newIsPaused;
});
});
const payload = {
interval: targetGroup.interval!,
name: targetGroup.name,
rules: modifiedRules,
};
await updateRule({
nameSpaceUID: rule.namespace.uid || rule.rulerRule.grafana_alert.namespace_uid,
payload,
}).unwrap();
onPauseChange?.(); onPauseChange?.();
}; };
@ -81,6 +57,7 @@ const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => {
<Menu.Item <Menu.Item
label={title} label={title}
icon={icon} icon={icon}
disabled={updateState.isLoading}
onClick={() => { onClick={() => {
setRulePause(!isPaused); setRulePause(!isPaused);
}} }}

@ -171,7 +171,7 @@ describe('contact points', () => {
} }
// check buttons in Notification Templates // check buttons in Notification Templates
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Tab Notification Templates' }); const notificationTemplatesTab = screen.getByRole('tab', { name: 'Notification Templates' });
await userEvent.click(notificationTemplatesTab); await userEvent.click(notificationTemplatesTab);
expect(screen.getByRole('link', { name: 'Add notification template' })).toHaveAttribute('aria-disabled', 'true'); expect(screen.getByRole('link', { name: 'Add notification template' })).toHaveAttribute('aria-disabled', 'true');
}); });
@ -388,7 +388,7 @@ describe('contact points', () => {
expect(viewProvisioned).not.toBeDisabled(); expect(viewProvisioned).not.toBeDisabled();
// check buttons in Notification Templates // check buttons in Notification Templates
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Tab Notification Templates' }); const notificationTemplatesTab = screen.getByRole('tab', { name: 'Notification Templates' });
await userEvent.click(notificationTemplatesTab); await userEvent.click(notificationTemplatesTab);
expect(screen.queryByRole('link', { name: 'Add notification template' })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'Add notification template' })).not.toBeInTheDocument();
}); });

@ -4,7 +4,7 @@ import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-h
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config, locationService } from '@grafana/runtime';
import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui'; import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
@ -12,7 +12,11 @@ import { contextSrv } from 'app/core/core';
import { useCleanup } from 'app/core/hooks/useCleanup'; import { useCleanup } from 'app/core/hooks/useCleanup';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule'; import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule';
import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from 'app/features/alerting/unified/utils/rules'; import {
getRuleGroupLocationFromRuleWithLocation,
isGrafanaRulerRule,
isGrafanaRulerRulePaused,
} from 'app/features/alerting/unified/utils/rules';
import { useDispatch } from 'app/types'; import { useDispatch } from 'app/types';
import { RuleWithLocation } from 'app/types/unified-alerting'; import { RuleWithLocation } from 'app/types/unified-alerting';
@ -23,8 +27,9 @@ import {
trackAlertRuleFormCancelled, trackAlertRuleFormCancelled,
trackAlertRuleFormSaved, trackAlertRuleFormSaved,
} from '../../../Analytics'; } from '../../../Analytics';
import { useDeleteRuleFromGroup } from '../../../hooks/useProduceNewRuleGroup';
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions'; import { saveRuleFormAction } from '../../../state/actions';
import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
import { initialAsyncRequestState } from '../../../utils/redux'; import { initialAsyncRequestState } from '../../../utils/redux';
import { import {
@ -36,7 +41,6 @@ import {
ignoreHiddenQueries, ignoreHiddenQueries,
normalizeDefaultAnnotations, normalizeDefaultAnnotations,
} from '../../../utils/rule-form'; } from '../../../utils/rule-form';
import * as ruleId from '../../../utils/rule-id';
import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
import { AlertRuleNameInput } from '../AlertRuleNameInput'; import { AlertRuleNameInput } from '../AlertRuleNameInput';
import AnnotationsStep from '../AnnotationsStep'; import AnnotationsStep from '../AnnotationsStep';
@ -60,6 +64,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
const [queryParams] = useQueryParams(); const [queryParams] = useQueryParams();
const [showEditYaml, setShowEditYaml] = useState(false); const [showEditYaml, setShowEditYaml] = useState(false);
const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL);
const [deleteRuleFromGroup, _deleteRuleState] = useDeleteRuleFromGroup();
const routeParams = useParams<{ type: string; id: string }>(); const routeParams = useParams<{ type: string; id: string }>();
const ruleType = translateRouteParamToRuleType(routeParams.type); const ruleType = translateRouteParamToRuleType(routeParams.type);
@ -151,16 +156,12 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
); );
}; };
const deleteRule = () => { const deleteRule = async () => {
if (existing) { if (existing) {
const identifier = ruleId.fromRulerRule( const ruleGroupIdentifier = getRuleGroupLocationFromRuleWithLocation(existing);
existing.ruleSourceName,
existing.namespace,
existing.group.name,
existing.rule
);
dispatch(deleteRuleAction(identifier, { navigateTo: '/alerting/list' })); await deleteRuleFromGroup(ruleGroupIdentifier, existing.rule);
locationService.replace(returnTo);
} }
}; };

@ -7,8 +7,12 @@ import { useAppNotification } from 'app/core/copy/appNotification';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { AppChromeUpdate } from '../../../../../../core/components/AppChrome/AppChromeUpdate'; import { AppChromeUpdate } from '../../../../../../core/components/AppChrome/AppChromeUpdate';
import { RulerRuleDTO, RulerRuleGroupDTO } from '../../../../../../types/unified-alerting-dto'; import {
import { alertRuleApi, ModifyExportPayload } from '../../../api/alertRuleApi'; PostableRulerRuleGroupDTO,
RulerRuleDTO,
RulerRuleGroupDTO,
} from '../../../../../../types/unified-alerting-dto';
import { alertRuleApi } from '../../../api/alertRuleApi';
import { fetchRulerRulesGroup } from '../../../api/ruler'; import { fetchRulerRulesGroup } from '../../../api/ruler';
import { useDataSourceFeatures } from '../../../hooks/useCombinedRule'; import { useDataSourceFeatures } from '../../../hooks/useCombinedRule';
import { RuleFormValues } from '../../../types/rule-form'; import { RuleFormValues } from '../../../types/rule-form';
@ -133,7 +137,7 @@ export const getPayloadToExport = (
uid: string, uid: string,
formValues: RuleFormValues, formValues: RuleFormValues,
existingGroup: RulerRuleGroupDTO<RulerRuleDTO> | null | undefined existingGroup: RulerRuleGroupDTO<RulerRuleDTO> | null | undefined
): ModifyExportPayload => { ): PostableRulerRuleGroupDTO => {
const grafanaRuleDto = formValuesToRulerGrafanaRuleDTO(formValues); const grafanaRuleDto = formValuesToRulerGrafanaRuleDTO(formValues);
const updatedRule = { ...grafanaRuleDto, grafana_alert: { ...grafanaRuleDto.grafana_alert, uid: uid } }; const updatedRule = { ...grafanaRuleDto, grafana_alert: { ...grafanaRuleDto.grafana_alert, uid: uid } };
@ -167,7 +171,7 @@ export const getPayloadToExport = (
const useGetPayloadToExport = (values: RuleFormValues, uid: string) => { const useGetPayloadToExport = (values: RuleFormValues, uid: string) => {
const rulerGroupDto = useGetGroup(values.folder?.uid ?? '', values.group); const rulerGroupDto = useGetGroup(values.folder?.uid ?? '', values.group);
const payload: ModifyExportPayload = useMemo(() => { const payload: PostableRulerRuleGroupDTO = useMemo(() => {
return getPayloadToExport(uid, values, rulerGroupDto?.value); return getPayloadToExport(uid, values, rulerGroupDto?.value);
}, [uid, rulerGroupDto, values]); }, [uid, rulerGroupDto, values]);
return { payload, loadingGroup: rulerGroupDto.loading }; return { payload, loadingGroup: rulerGroupDto.loading };

@ -1,17 +1,19 @@
import React, { useState, useCallback, useMemo } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { locationService } from '@grafana/runtime';
import { ConfirmModal } from '@grafana/ui'; import { ConfirmModal } from '@grafana/ui';
import { dispatch } from 'app/store/store'; import { dispatch } from 'app/store/store';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { deleteRuleAction } from '../../state/actions'; import { useDeleteRuleFromGroup } from '../../hooks/useProduceNewRuleGroup';
import { getRulesSourceName } from '../../utils/datasource'; import { fetchPromAndRulerRulesAction } from '../../state/actions';
import { fromRulerRule } from '../../utils/rule-id'; import { getRuleGroupLocationFromCombinedRule } from '../../utils/rules';
type DeleteModalHook = [JSX.Element, (rule: CombinedRule) => void, () => void]; type DeleteModalHook = [JSX.Element, (rule: CombinedRule) => void, () => void];
export const useDeleteModal = (): DeleteModalHook => { export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule | undefined>(); const [ruleToDelete, setRuleToDelete] = useState<CombinedRule | undefined>();
const [deleteRuleFromGroup, _deleteState] = useDeleteRuleFromGroup();
const dismissModal = useCallback(() => { const dismissModal = useCallback(() => {
setRuleToDelete(undefined); setRuleToDelete(undefined);
@ -22,20 +24,25 @@ export const useDeleteModal = (): DeleteModalHook => {
}, []); }, []);
const deleteRule = useCallback( const deleteRule = useCallback(
(ruleToDelete?: CombinedRule) => { async (rule?: CombinedRule) => {
if (ruleToDelete && ruleToDelete.rulerRule) { if (!rule?.rulerRule) {
const identifier = fromRulerRule( return;
getRulesSourceName(ruleToDelete.namespace.rulesSource), }
ruleToDelete.namespace.name,
ruleToDelete.group.name, const location = getRuleGroupLocationFromCombinedRule(rule);
ruleToDelete.rulerRule await deleteRuleFromGroup(location, rule.rulerRule);
);
// refetch rules for this rules source
// @TODO remove this when we moved everything to RTKQ – then the endpoint will simply invalidate the tags
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: location.dataSourceName }));
dispatch(deleteRuleAction(identifier, { navigateTo: '/alerting/list' }));
dismissModal(); dismissModal();
if (redirectToListView) {
locationService.replace('/alerting/list');
} }
}, },
[dismissModal] [deleteRuleFromGroup, dismissModal, redirectToListView]
); );
const modal = useMemo( const modal = useMemo(

@ -1,7 +1,7 @@
import { produce } from 'immer'; import { produce } from 'immer';
import React from 'react'; import React from 'react';
import { render, screen, userEvent } from 'test/test-utils'; import { render, screen, userEvent } from 'test/test-utils';
import { byLabelText } from 'testing-library-selector'; import { byLabelText, byRole } from 'testing-library-selector';
import { config, setPluginExtensionsHook } from '@grafana/runtime'; import { config, setPluginExtensionsHook } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
@ -24,7 +24,9 @@ jest.mock('app/core/services/context_srv');
const mockContextSrv = jest.mocked(contextSrv); const mockContextSrv = jest.mocked(contextSrv);
const ui = { const ui = {
menu: byRole('menu'),
moreButton: byLabelText(/More/), moreButton: byLabelText(/More/),
pauseButton: byRole('menuitem', { name: /Pause evaluation/ }),
}; };
const grantAllPermissions = () => { const grantAllPermissions = () => {
@ -76,6 +78,19 @@ describe('RuleActionsButtons', () => {
expect(await getMenuContents()).toMatchSnapshot(); expect(await getMenuContents()).toMatchSnapshot();
}); });
it('should be able to pause a Grafana rule', async () => {
const user = userEvent.setup();
grantAllPermissions();
const mockRule = getGrafanaRule();
render(<RuleActionsButtons rule={mockRule} rulesSource="grafana" />);
await user.click(await ui.moreButton.find());
await user.click(await ui.pauseButton.find());
expect(ui.menu.query()).not.toBeInTheDocument();
});
it('renders correct options for Cloud rule', async () => { it('renders correct options for Cloud rule', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
grantAllPermissions(); grantAllPermissions();

@ -44,7 +44,9 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton
const dispatch = useDispatch(); const dispatch = useDispatch();
const location = useLocation(); const location = useLocation();
const style = useStyles2(getStyles); const style = useStyles2(getStyles);
const [deleteModal, showDeleteModal] = useDeleteModal();
const redirectToListView = compact ? false : true;
const [deleteModal, showDeleteModal] = useDeleteModal(redirectToListView);
const [showSilenceDrawer, setShowSilenceDrawer] = useState<boolean>(false); const [showSilenceDrawer, setShowSilenceDrawer] = useState<boolean>(false);

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

Loading…
Cancel
Save