diff --git a/.betterer.results b/.betterer.results
index adbcfdd1d1e..0cbbf2c2539 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -1283,14 +1283,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "1"]
],
- "public/app/core/navigation/testRoutes.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "2"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "3"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "4"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "5"]
- ],
"public/app/core/navigation/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@@ -1825,9 +1817,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/receivers/DuplicateTemplateView.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
],
- "public/app/features/alerting/unified/components/receivers/EditReceiverView.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
- ],
"public/app/features/alerting/unified/components/receivers/EditTemplateView.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
],
@@ -4790,11 +4779,7 @@ exports[`better eslint`] = {
"public/app/features/playlist/PlaylistTableRows.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"],
- [0, 0, 0, "Styles should be written using objects.", "4"],
- [0, 0, 0, "Styles should be written using objects.", "5"],
- [0, 0, 0, "Styles should be written using objects.", "6"]
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "2"]
],
"public/app/features/playlist/StartModal.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
@@ -4988,9 +4973,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Do not use any type assertions.", "12"]
],
- "public/app/features/plugins/extensions/getPluginExtensions.test.tsx:5381": [
- [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
- ],
"public/app/features/plugins/extensions/usePluginComponents.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@@ -5867,8 +5849,7 @@ exports[`better eslint`] = {
],
"public/app/plugins/datasource/alertmanager/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "2"]
+ [0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/azuremonitor/azureMetadata/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7a998bb1099..5bc3cab07ce 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -693,6 +693,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/commands.yml @torkelo
/.github/workflows/community-release.yml @grafana/grafana-release-guild
/.github/workflows/detect-breaking-changes-* @grafana/plugins-platform-frontend
+/.github/workflows/auto-triager.yml @grafana/plugins-platform-frontend
/.github/workflows/doc-validator.yml @grafana/docs-tooling
/.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina
/.github/workflows/github-release.yml @grafana/grafana-release-guild
diff --git a/.github/commands.json b/.github/commands.json
index 5f02fc4967e..dac2f7ed60d 100644
--- a/.github/commands.json
+++ b/.github/commands.json
@@ -458,5 +458,637 @@
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/471"
}
+ },
+ {
+ "type": "label",
+ "name": "area/alerting",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/52"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/backend",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/665"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboard",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "type/accessibility",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/78"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/transformations",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/56"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboard/templating",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/auth",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/660"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/plugins",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/76"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/annotations",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/56"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/annotations",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/provisioning",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/599"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/provisioning",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/52"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/scenes",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Postgres",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/logs",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/203"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboards/panel",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/public-dashboards",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/482"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/exploremetrics",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/112"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/auth/oauth",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/660"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/traceview",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/221"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/auth/rbac",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/660"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/expressions",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/112"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboard/data-links",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/backend/api",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/319"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/security",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/47"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/admin/user",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/660"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/node-graph",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/221"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboard/snapshot",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboard/folders",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/configuration",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/96"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/frontend/library-panels",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/auth/ldap",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/660"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboard/timerange",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/MSSQL",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/190"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Jaeger",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/navigation",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/78"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/search",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/78"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/search",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/flame-graph",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/221"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/auth/serviceaccount",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/660"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/field/overrides",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/backend/db/sql",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/image-rendering",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/482"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/backend/db/postgres",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/96"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/permissions",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/660"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboard/import",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/backend/db/sqlite",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/599"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/backend/db/mysql",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/45"
+ }
+ },
+ {
+ "type": "label",
+ "name": "type/browser-compatibility",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/78"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/text",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/78"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/dashboard/links",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Zipkin",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/streaming",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/319"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/frontend/login",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/78"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Parca",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/CSV",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/datagrid",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/56"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/dashboard-list",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "team/grafana-aws-datasources",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/97"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Zabbix",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/TestDataDB",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/76"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Splunk",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/55"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/SiteWIse",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/97"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Phlare",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/221"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/JSON",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/BigQuery",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Alertmanager",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/52"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/recorded-queries",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/112"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/singlestat",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/56"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/panel/annotation-list",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/202"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/editor",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/grafana/grafana/projects/21"
+ }
+ },
+ {
+ "type": "label",
+ "name": "area/data/export",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/56"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/azure-cosmosdb",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/190"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/X-Ray",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/97"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/Timestream",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/97"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/OpenSearch",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/97"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/GoogleSheets",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/GitHub",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
+ },
+ {
+ "type": "label",
+ "name": "datasource/MySQL",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/457"
+ }
}
-]
\ No newline at end of file
+]
diff --git a/.github/issue-opened.json b/.github/issue-opened.json
index 86b0164d2b0..276c9464a76 100644
--- a/.github/issue-opened.json
+++ b/.github/issue-opened.json
@@ -6,6 +6,6 @@
},
"noLabels": true,
"addLabel": "internal",
- "comment": " please add one or more appropriate labels. Here are some tips:\r\n\r\n- if you are making an issue, TODO, or reminder for yourself or your team, please add one label that best describes the product or feature area. Please also add the issue to your project board. :rocket:\r\n\r\n- if you are making an issue for any other reason (docs typo, you found a bug, etc), please add at least one label that best describes the product or feature that you are discussing (e.g. `area/alerting`, `datasource/loki`, `type/docs`, `type/bug`, etc). [Our issue triage](https://github.com/grafana/grafana/blob/main/ISSUE_TRIAGE.md#3-categorizing-an-issue) doc also provides additional guidance on labeling. :rocket:\r\n\r\n Thank you! :heart:"
+ "comment": " please add one or more appropriate labels. Here are some tips:\r\n\r\n- if you are making an issue, TODO, or reminder for yourself or your team, please add one label that best describes the product or feature area. Please also add the issue to your project board. :rocket:\r\n\r\n- if you are making an issue for any other reason (docs typo, you found a bug, etc), please add at least one label that best describes the product or feature that you are discussing (e.g. `area/alerting`, `datasource/loki`, `type/docs`, `type/bug`, etc). [Our issue triage](https://github.com/grafana/grafana/blob/main/contribute/ISSUE_TRIAGE.md#3-categorize-an-issue) doc also provides additional guidance on labeling. :rocket:\r\n\r\n Thank you! :heart:"
}
]
\ No newline at end of file
diff --git a/.github/workflows/auto-triager.yml b/.github/workflows/auto-triager.yml
new file mode 100644
index 00000000000..80eeba554cb
--- /dev/null
+++ b/.github/workflows/auto-triager.yml
@@ -0,0 +1,75 @@
+# This workflow is triggered when a new issue is opened
+# It will run an internal github action to try to automate the triage process
+name: Auto Triage Issues
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ config:
+ 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 != '' &&
+ secrets.OPENAI_API_KEY != '' &&
+ secrets.SLACK_WEBHOOK_URL != ''
+ ) || '' }}" ]; then
+ echo "has-secrets=1" >> "$GITHUB_OUTPUT"
+ fi
+ auto-triage:
+ needs: config
+ if: needs.config.outputs.has-secrets
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Generate token"
+ id: generate_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: Checkout auto-triager repository
+ uses: actions/checkout@v4
+ with:
+ repository: grafana/auto-triager
+ path: auto-triager
+ token: ${{ steps.generate_token.outputs.token }}
+
+ - name: Send issue to the auto triager action
+ id: auto_triage
+ # https://github.com/grafana/auto-triager/blob/main/action.yml
+ #uses: grafana/auto-triager@main
+ uses: ./auto-triager
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ issue_number: ${{ github.event.issue.number }}
+ openai_api_key: ${{ secrets.OPENAI_API_KEY }}
+ # Leaving the actionin monitoring mode for now
+ # should be set to true when ready to use
+ # add_labels: true
+ add_labels: false
+
+ - name: Labels from auto triage
+ run: |
+ echo ${{ steps.auto_triage.outputs.triage_labels }}
+
+ - name: "Send Slack notification"
+ if : ${{ steps.auto_triage.outputs.triage_labels != '' }}
+ uses: slackapi/slack-github-action@v1.27.0
+ with:
+ payload: >
+ {
+ "icon_emoji": ":robocto:",
+ "username": "Auto Triager",
+ "type": "mrkdwn",
+ "text": "Auto triager found the following labels: ${{ steps.auto_triage.outputs.triage_labels }} for [issue #${{ github.event.issue.number }}](${{ github.event.issue.html_url }})",
+ "channel": "#triage-automation-ci"
+ }
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
diff --git a/docs/sources/administration/announcement-banner/_index.md b/docs/sources/administration/announcement-banner/_index.md
index 2d62de69734..b67e0d7ca1d 100644
--- a/docs/sources/administration/announcement-banner/_index.md
+++ b/docs/sources/administration/announcement-banner/_index.md
@@ -1,5 +1,4 @@
---
-draft: true
keywords:
- grafana
- announcement
@@ -52,9 +51,6 @@ To create or update an announcement banner, follow these steps:
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.
- **Authenticated users** - The banner is visible to only authenticated users.
1. 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.
diff --git a/docs/sources/administration/roles-and-permissions/access-control/_index.md b/docs/sources/administration/roles-and-permissions/access-control/_index.md
index 967f2f440ad..d4aac02464b 100644
--- a/docs/sources/administration/roles-and-permissions/access-control/_index.md
+++ b/docs/sources/administration/roles-and-permissions/access-control/_index.md
@@ -92,7 +92,7 @@ Assign fixed roles when the basic roles do not meet your permission requirements
- [Alerting]({{< relref "../../../alerting/" >}})
- [Annotations]({{< relref "../../../dashboards/build-dashboards/annotate-visualizations" >}})
-- [API keys]({{< relref "../../api-keys/" >}})
+- [API keys](/docs/grafana//administration/service-accounts/migrate-api-keys/)
- [Dashboards and folders]({{< relref "../../../dashboards/" >}})
- [Data sources]({{< relref "../../../datasources/" >}})
- [Explore]({{< relref "../../../explore/" >}})
diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md
index c2d81caf443..35d8a9a572a 100644
--- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md
+++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md
@@ -14,13 +14,13 @@ weight: 80
# RBAC permissions, actions, and scopes
-{{% admonition type="note" %}}
-Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise/" >}}) and [Grafana Cloud](/docs/grafana-cloud).
-{{% /admonition %}}
+{{< admonition type="note" >}}
+Available in [Grafana Enterprise](/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/).
+{{< /admonition >}}
-A permission is comprised of an action and a scope. When creating a custom role, consider the actions the user can perform and the resource(s) on which they can perform those actions.
+A permission is comprised of an action and a scope. When creating a custom role, consider the actions the user can perform and the resources on which they can perform those actions.
-To learn more about the Grafana resources to which you can apply RBAC, refer to [Resources with RBAC permissions]({{< relref "../#fixed-roles" >}}).
+To learn more about the Grafana resources to which you can apply RBAC, refer to [Resources with RBAC permissions](/docs/grafana//administration/roles-and-permissions/access-control/#fixed-roles).
- **Action:** An action describes what tasks a user can perform on a resource.
- **Scope:** A scope describes where an action can be performed, such as reading a specific user profile. In this example, a permission is associated with the scope `users:` to the relevant role.
@@ -29,222 +29,223 @@ To learn more about the Grafana resources to which you can apply RBAC, refer to
The following list contains role-based access control actions.
-| Action | Applicable scope | Description |
-| ------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `alert.instances.external:read` | `datasources:*` `datasources:uid:*` | Read alerts and silences in data sources that support alerting. |
-| `alert.instances.external:write` | `datasources:*` `datasources:uid:*` | Manage alerts and silences in data sources that support alerting. |
-| `alert.instances:create` | n/a | Create silences in the current organization. |
-| `alert.instances:read` | n/a | Read alerts and silences in the current organization. |
-| `alert.instances:write` | n/a | Update and expire silences in the current organization. |
-| `alert.notifications.external:read` | `datasources:*` `datasources:uid:*` | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. |
-| `alert.notifications.external:write` | `datasources:*` `datasources:uid:*` | Manage templates, contact points, notification policies, and mute timings in data sources that support alerting. |
-| `alert.notifications:write` | n/a | Manage templates, contact points, notification policies, and mute timings in the current organization. |
-| `alert.notifications:read` | n/a | Read all templates, contact points, notification policies, and mute timings in the current organization. |
-| `alert.rules.external:read` | `datasources:*` `datasources:uid:*` | Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki) |
-| `alert.rules.external:write` | `datasources:*` `datasources:uid:*` | Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). |
-| `alert.rules:create` | `folders:*` `folders:uid:*` | Create Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
-| `alert.rules:delete` | `folders:*` `folders:uid:*` | Delete Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
-| `alert.rules:read` | `folders:*` `folders:uid:*` | Read Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
-| `alert.rules:write` | `folders:*` `folders:uid:*` | Update Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
-| `alert.silences:create` | `folders:*` `folders:uid:*` | Create rule-specific silences in a folder and its subfolders. |
-| `alert.silences:read` | `folders:*` `folders:uid:*` | Read all general silences and rule-specific silences in a folder and its subfolders. |
-| `alert.silences:write` | `folders:*` `folders:uid:*` | Update and expire rule-specific silences in a folder and its subfolders. |
-| `alert.provisioning:read` | n/a | Read all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. |
-| `alert.provisioning.secrets:read` | n/a | Same as `alert.provisioning:read` plus ability to export resources with decrypted secrets. |
-| `alert.provisioning:write` | n/a | Update all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. |
-| `alert.provisioning.provenance:write` | n/a | Set provisioning status for alerting resources. Cannot be used alone. Requires user to have permissions to access resources |
-| `annotations:create` | `annotations:*` `annotations:type:*` | Create annotations. |
-| `annotations:delete` | `annotations:*` `annotations:type:*` | Delete annotations. |
-| `annotations:read` | `annotations:*` `annotations:type:*` | Read annotations and annotation tags. |
-| `annotations:write` | `annotations:*` `annotations:type:*` | Update annotations. |
-| `apikeys:create` | n/a | Create API keys. |
-| `apikeys:read` | `apikeys:*` `apikeys:id:*` | Read API keys. |
-| `apikeys:delete` | `apikeys:*` `apikeys:id:*` | Delete API keys. |
-| `dashboards:create` | `folders:*` `folders:uid:*` | Create dashboards in one or more folders and their subfolders. |
-| `dashboards:delete` | `dashboards:*` `dashboards:uid:*` `folders:*` `folders:uid:*` | Delete one or more dashboards. |
-| `dashboards.insights:read` | n/a | Read dashboard insights data and see presence indicators. |
-| `dashboards.permissions:read` | `dashboards:*` `dashboards:uid:*` `folders:*` `folders:uid:*` | Read permissions for one or more dashboards. |
-| `dashboards.permissions:write` | `dashboards:*` `dashboards:uid:*` `folders:*` `folders:uid:*` | Update permissions for one or more dashboards. |
-| `dashboards:read` | `dashboards:*` `dashboards:uid:*` `folders:*` `folders:uid:*` | Read one or more dashboards. |
-| `dashboards:write` | `dashboards:*` `dashboards:uid:*` `folders:*` `folders:uid:*` | Update one or more dashboards. |
-| `dashboards.public:write` | `dashboards:*` `dashboards:uid:*` | Write public dashboard configuration. |
-| `datasources.caching:read` | `datasources:*` `datasources:uid:*` | Read data source query caching settings. |
-| `datasources.caching:write` | `datasources:*` `datasources:uid:*` | Update data source query caching settings. |
-| `datasources:create` | n/a | Create data sources. |
-| `datasources:delete` | `datasources:*` `datasources:uid:*` | Delete data sources. |
-| `datasources:explore` | n/a | Enable access to the **Explore** tab. |
-| `datasources.id:read` | `datasources:*` `datasources:uid:*` | Read data source IDs. |
-| `datasources.insights:read` | n/a | Read data sources insights data. |
-| `datasources.permissions:read` | `datasources:*` `datasources:uid:*` | List data source permissions. |
-| `datasources.permissions:write` | `datasources:*` `datasources:uid:*` | Update data source permissions. |
-| `datasources:query` | `datasources:*` `datasources:uid:*` | Query data sources. |
-| `datasources:read` | `datasources:*` `datasources:uid:*` | List data sources. |
-| `datasources:write` | `datasources:*` `datasources:uid:*` | Update data sources. |
-| `featuremgmt.read` | n/a | Read feature toggles. |
-| `featuremgmt.write` | n/a | Write feature toggles. |
-| `folders.permissions:read` | `folders:*` `folders:uid:*` | Read permissions for one or more folders and their subfolders. |
-| `folders.permissions:write` | `folders:*` `folders:uid:*` | Update permissions for one or more folders and their subfolders. |
-| `folders:create` | n/a | Create folders in the root level. If granted together with `folders:write`, also allows creating subfolders under all folders that the user can update. |
-| `folders:delete` | `folders:*` `folders:uid:*` | Delete one or more folders and their subfolders. |
-| `folders:read` | `folders:*` `folders:uid:*` | Read one or more folders and their subfolders. |
-| `folders:write` | `folders:*` `folders:uid:*` | Update one or more folders and their subfolders. If granted together with `folders:create` permission, also allows creating subfolders under these folders. |
-| `ldap.config:reload` | n/a | Reload the LDAP configuration. |
-| `ldap.status:read` | n/a | Verify the availability of the LDAP server or servers. |
-| `ldap.user:read` | n/a | Read users via LDAP. |
-| `ldap.user:sync` | n/a | Sync users via LDAP. |
-| `library.panels:create` | `folders:*` `folders:uid:*` | Create a library panel in one or more folders and their subfolders. |
-| `library.panels:read` | `folders:*` `folders:uid:*` `library.panels:*` `library.panels:uid:*` | Read one or more library panels. |
-| `library.panels:write` | `folders:*` `folders:uid:*` `library.panels:*` `library.panels:uid:*` | Update one or more library panels. |
-| `library.panels:delete` | `folders:*` `folders:uid:*` `library.panels:*` `library.panels:uid:*` | Delete one or more library panels. |
-| `licensing.reports:read` | n/a | Get custom permission reports. |
-| `licensing:delete` | n/a | Delete the license token. |
-| `licensing:read` | n/a | Read licensing information. |
-| `licensing:write` | n/a | Update the license token. |
-| `org.users:write` | `users:*` `users:id:*` | Update the organization role (`None`, `Viewer`, `Editor`, or `Admin`) of a user. |
-| `org.users:add` | `users:*` `users:id:*` | Add a user to an organization or invite a new user to an organization. |
-| `org.users:read` | `users:*` `users:id:*` | Get user profiles within an organization. |
-| `org.users:remove` | `users:*` `users:id:*` | Remove a user from an organization. |
-| `orgs.preferences:read` | n/a | Read organization preferences. |
-| `orgs.preferences:write` | n/a | Update organization preferences. |
-| `orgs.quotas:read` | n/a | Read organization quotas. |
-| `orgs.quotas:write` | n/a | Update organization quotas. |
-| `orgs:create` | n/a | Create an organization. |
-| `orgs:delete` | n/a | Delete one or more organizations. |
-| `orgs:read` | n/a | Read one or more organizations. |
-| `orgs:write` | n/a | Update one or more organizations. |
-| `plugins.app:access` | `plugins:*` `plugins:id:*` | Access one or more application plugins (still enforcing the organization role) |
-| `plugins:install` | n/a | Install and uninstall plugins. |
-| `plugins:write` | `plugins:*` `plugins:id:*` | Edit settings for one or more plugins. |
-| `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, see [Scope definitions]({{< relref "#scope-definitions" >}}). |
-| `reports:create` | n/a | Create reports. |
-| `reports:write` | `reports:*` `reports:id:*` | Update reports. |
-| `reports.settings:read` | n/a | Read report settings. |
-| `reports.settings:write` | n/a | Update report settings. |
-| `reports:delete` | `reports:*` `reports:id:*` | Delete reports. |
-| `reports:read` | `reports:*` `reports:id:*` | List all available reports or get a specific report. |
-| `reports:send` | `reports:*` `reports:id:*` | Send a report email. |
-| `roles:delete` | `permissions:type:delegate` | Delete a custom role. |
-| `roles:read` | `roles:*` `roles:uid:*` | List roles and read a specific role with its permissions. |
-| `roles:write` | `permissions:type:delegate` | Create or update a custom role. |
-| `roles:write` | `permissions:type:escalate` | Reset basic roles to their default permissions. |
-| `server.stats:read` | n/a | Read Grafana instance statistics. |
-| `server.usagestats.report:read` | n/a | View usage statistics report. |
-| `serviceaccounts:write` | `serviceaccounts:*` | Create Grafana service accounts. |
-| `serviceaccounts:create` | n/a | Update Grafana service accounts. |
-| `serviceaccounts:delete` | `serviceaccounts:*` `serviceaccounts:id:*` | Delete Grafana service accounts. |
-| `serviceaccounts:read` | `serviceaccounts:*` `serviceaccounts:id:*` | Read Grafana service accounts. |
-| `serviceaccounts.permissions:write` | `serviceaccounts:*` `serviceaccounts:id:*` | Update Grafana service account permissions to control who can do what with the service account. |
-| `serviceaccounts.permissions:read` | `serviceaccounts:*` `serviceaccounts:id:*` | Read Grafana service account permissions to see who can do what with the service account. |
-| `settings:read` | `settings:*` `settings:auth.saml:*` `settings:auth.saml:enabled` (property level) | Read the [Grafana configuration settings]({{< relref "../../../../setup-grafana/configure-grafana/" >}}) |
-| `settings:write` | `settings:*` `settings:auth.saml:*` `settings:auth.saml:enabled` (property level) | Update any Grafana configuration settings that can be [updated at runtime]({{< relref "../../../../setup-grafana/configure-grafana/settings-updates-at-runtime" >}}). |
-| `support.bundles:create` | n/a | Create support bundles. |
-| `support.bundles:delete` | n/a | Delete support bundles. |
-| `support.bundles:read` | n/a | List and download support bundles. |
-| `status:accesscontrol` | `services:accesscontrol` | Get access-control enabled status. |
-| `teams.permissions:read` | `teams:*` `teams:id:*` | Read members and Team Sync setup for teams. |
-| `teams.permissions:write` | `teams:*` `teams:id:*` | Add, remove and update members and manage Team Sync setup for teams. |
-| `teams.roles:add` | `permissions:type:delegate` | Assign a role to a team. |
-| `teams.roles:read` | `teams:*` `teams:id:*` | List roles assigned directly to a team. |
-| `teams.roles:remove` | `permissions:type:delegate` | Unassign a role from a team. |
-| `teams:create` | n/a | Create teams. |
-| `teams:delete` | `teams:*` `teams:id:*` | Delete one or more teams. |
-| `teams:read` | `teams:*` `teams:id:*` | Read one or more teams and team preferences. To list teams through the UI one of the following permissions is required in addition to `teams:read`: `teams:write`, `teams.permissions:read` or `teams.permissions:write`. |
-| `teams:write` | `teams:*` `teams:id:*` | Update one or more teams and team preferences. |
-| `users.authtoken:read` | `global.users:*` `global.users:id:*` | List authentication tokens that are assigned to a user. |
-| `users.authtoken:write` | `global.users:*` `global.users:id:*` | Update authentication tokens that are assigned to a user. |
-| `users.password:write` | `global.users:*` `global.users:id:*` | Update a user’s password. |
-| `users.permissions:read` | `users:*` | List permissions of a user. |
-| `users.permissions:write` | `global.users:*` `global.users:id:*` | Update a user’s organization-level permissions. |
-| `users.quotas:read` | `global.users:*` `global.users:id:*` | List a user’s quotas. |
-| `users.quotas:write` | `global.users:*` `global.users:id:*` | Update a user’s quotas. |
-| `users.roles:add` | `permissions:type:delegate` | Assign a role to a user or a service account. |
-| `users.roles:read` | `users:*` | List roles assigned directly to a user or a service account. |
-| `users.roles:remove` | `permissions:type:delegate` | Unassign a role from a user or a service account. |
-| `users:create` | n/a | Create a user. |
-| `users:delete` | `global.users:*` `global.users:id:*` | Delete a user. |
-| `users:disable` | `global.users:*` `global.users:id:*` | Disable a user. |
-| `users:enable` | `global.users:*` `global.users:id:*` | Enable a user. |
-| `users:logout` | `global.users:*` `global.users:id:*` | Sign out a user. |
-| `users:read` | `global.users:*` | Read or search user profiles. |
-| `users:write` | `global.users:*` `global.users:id:*` | Update a user’s profile. |
+
+| Action | Applicable scopes | Description |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `alert.instances.external:read` |
`datasources:*`
`datasources:uid:*`
| Read alerts and silences in data sources that support alerting. |
+| `alert.instances.external:write` |
`datasources:*`
`datasources:uid:*`
| Manage alerts and silences in data sources that support alerting. |
+| `alert.instances:create` | None | Create silences in the current organization. |
+| `alert.instances:read` | None | Read alerts and silences in the current organization. |
+| `alert.instances:write` | None | Update and expire silences in the current organization. |
+| `alert.notifications.external:read` |
`datasources:*`
`datasources:uid:*`
| Read templates, contact points, notification policies, and mute timings in data sources that support alerting. |
+| `alert.notifications.external:write` |
`datasources:*`
`datasources:uid:*`
| Manage templates, contact points, notification policies, and mute timings in data sources that support alerting. |
+| `alert.notifications:write` | None | Manage templates, contact points, notification policies, and mute timings in the current organization. |
+| `alert.notifications:read` | None | Read all templates, contact points, notification policies, and mute timings in the current organization. |
+| `alert.rules.external:read` |
`datasources:*`
`datasources:uid:*`
| Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki) |
+| `alert.rules.external:write` |
`datasources:*`
`datasources:uid:*`
| Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). |
+| `alert.rules:create` |
`folders:*`
`folders:uid:*`
| Create Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
+| `alert.rules:delete` |
`folders:*`
`folders:uid:*`
| Delete Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
+| `alert.rules:read` |
`folders:*`
`folders:uid:*`
| Read Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
+| `alert.rules:write` |
`folders:*`
`folders:uid:*`
| Update Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
+| `alert.silences:create` |
`folders:*`
`folders:uid:*`
| Create rule-specific silences in a folder and its subfolders. |
+| `alert.silences:read` |
`folders:*`
`folders:uid:*`
| Read all general silences and rule-specific silences in a folder and its subfolders. |
+| `alert.silences:write` |
`folders:*`
`folders:uid:*`
| Update and expire rule-specific silences in a folder and its subfolders. |
+| `alert.provisioning:read` | None | Read all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. |
+| `alert.provisioning.secrets:read` | None | Same as `alert.provisioning:read` plus ability to export resources with decrypted secrets. |
+| `alert.provisioning:write` | None | Update all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. |
+| `alert.provisioning.provenance:write` | None | Set provisioning status for alerting resources. Cannot be used alone. Requires user to have permissions to access resources |
+| `annotations:create` |
`annotations:*`
`annotations:type:*`
| Create annotations. |
+| `annotations:delete` |
`annotations:*`
`annotations:type:*`
| Delete annotations. |
+| `annotations:read` |
`annotations:*`
`annotations:type:*`
| Read annotations and annotation tags. |
+| `annotations:write` |
| Create dashboards in one or more folders and their subfolders. |
+| `dashboards:delete` |
`dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*`
| Delete one or more dashboards. |
+| `dashboards.insights:read` | None | Read dashboard insights data and see presence indicators. |
+| `dashboards.permissions:read` |
`dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*`
| Read permissions for one or more dashboards. |
+| `dashboards.permissions:write` |
`dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*`
| Update permissions for one or more dashboards. |
+| `dashboards:read` |
`dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*`
| Read one or more dashboards. |
+| `dashboards:write` |
`dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*`
| Update one or more dashboards. |
+| `dashboards.public:write` |
`dashboards:*`
`dashboards:uid:*`
| Write public dashboard configuration. |
+| `datasources.caching:read` |
| Read permissions for one or more folders and their subfolders. |
+| `folders.permissions:write` |
`folders:*`
`folders:uid:*`
| Update permissions for one or more folders and their subfolders. |
+| `folders:create` |
`folders:*`
`folders:uid:*`
`folders:uid:general`
| Create folders or subfolders. If granted with scope `folders:uid:general`, it allows to create root level folders. Otherwise, it allows creating subfolders under the specified folders. |
+| `folders:delete` |
`folders:*`
`folders:uid:*`
| Delete one or more folders and their subfolders. |
+| `folders:read` |
`folders:*`
`folders:uid:*`
| Read one or more folders and their subfolders. |
+| `folders:write` |
`folders:*`
`folders:uid:*`
| Update one or more folders and their subfolders. |
+| `ldap.config:reload` | None | Reload the LDAP configuration. |
+| `ldap.status:read` | None | Verify the availability of the LDAP server or servers. |
+| `ldap.user:read` | None | Read users via LDAP. |
+| `ldap.user:sync` | None | Sync users via LDAP. |
+| `library.panels:create` |
`folders:*`
`folders:uid:*`
| Create a library panel in one or more folders and their subfolders. |
+| `library.panels:read` |
`folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*`
| Read one or more library panels. |
+| `library.panels:write` |
`folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*`
| Update one or more library panels. |
+| `library.panels:delete` |
`folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*`
| Delete one or more library panels. |
+| `licensing.reports:read` | None | Get custom permission reports. |
+| `licensing:delete` | None | Delete the license token. |
+| `licensing:read` | None | Read licensing information. |
+| `licensing:write` | None | Update the license token. |
+| `org.users:write` |
`users:*`
`users:id:*`
| Update the organization role (`None`, `Viewer`, `Editor`, or `Admin`) of a user. |
+| `org.users:add` |
`users:*`
`users:id:*`
| Add a user to an organization or invite a new user to an organization. |
+| `org.users:read` |
`users:*`
`users:id:*`
| Get user profiles within an organization. |
+| `org.users:remove` |
`users:*`
`users:id:*`
| Remove a user from an organization. |
+| `orgs.preferences:read` | None | Read organization preferences. |
+| `orgs.preferences:write` | None | Update organization preferences. |
+| `orgs.quotas:read` | None | Read organization quotas. |
+| `orgs.quotas:write` | None | Update organization quotas. |
+| `orgs:create` | None | Create an organization. |
+| `orgs:delete` | None | Delete one or more organizations. |
+| `orgs:read` | None | Read one or more organizations. |
+| `orgs:write` | None | Update one or more organizations. |
+| `plugins.app:access` |
`plugins:*`
`plugins:id:*`
| Access one or more application plugins (still enforcing the organization role) |
+| `plugins:install` | None | Install and uninstall plugins. |
+| `plugins:write` |
`plugins:*`
`plugins:id:*`
| Edit settings for one or more plugins. |
+| `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, refer to [Scope definitions](#scope-definitions). |
+| `reports:create` | None | Create reports. |
+| `reports:write` |
(property level) | Update any Grafana configuration settings that can be [updated at runtime](/docs/grafana//setup-grafana/configure-grafana/settings-updates-at-runtime/). |
+| `support.bundles:create` | None | Create support bundles. |
+| `support.bundles:delete` | None | Delete support bundles. |
+| `support.bundles:read` | None | List and download support bundles. |
+| `status:accesscontrol` |
`services:accesscontrol`
| Get access-control enabled status. |
+| `teams.permissions:read` |
`teams:*`
`teams:id:*`
| Read members and Team Sync setup for teams. |
+| `teams.permissions:write` |
`teams:*`
`teams:id:*`
| Add, remove and update members and manage Team Sync setup for teams. |
+| `teams.roles:add` |
`permissions:type:delegate`
| Assign a role to a team. |
+| `teams.roles:read` |
`teams:*`
`teams:id:*`
| List roles assigned directly to a team. |
+| `teams.roles:remove` |
`permissions:type:delegate`
| Unassign a role from a team. |
+| `teams:create` | None | Create teams. |
+| `teams:delete` |
`teams:*`
`teams:id:*`
| Delete one or more teams. |
+| `teams:read` |
`teams:*`
`teams:id:*`
| Read one or more teams and team preferences. To list teams through the UI one of the following permissions is required in addition to `teams:read`: `teams:write`, `teams.permissions:read` or `teams.permissions:write`. |
+| `teams:write` |
`teams:*`
`teams:id:*`
| Update one or more teams and team preferences. |
+| `users.authtoken:read` |
`global.users:*`
`global.users:id:*`
| List authentication tokens that are assigned to a user. |
+| `users.authtoken:write` |
`global.users:*`
`global.users:id:*`
| Update authentication tokens that are assigned to a user. |
+| `users.password:write` |
`global.users:*`
`global.users:id:*`
| Update a user’s password. |
+| `users.permissions:read` |
`users:*`
| List permissions of a user. |
+| `users.permissions:write` |
`global.users:*`
`global.users:id:*`
| Update a user’s organization-level permissions. |
+| `users.quotas:read` |
`global.users:*`
`global.users:id:*`
| List a user’s quotas. |
+| `users.quotas:write` |
`global.users:*`
`global.users:id:*`
| Update a user’s quotas. |
+| `users.roles:add` |
`permissions:type:delegate`
| Assign a role to a user or a service account. |
+| `users.roles:read` |
`users:*`
| List roles assigned directly to a user or a service account. |
+| `users.roles:remove` |
`permissions:type:delegate`
| Unassign a role from a user or a service account. |
+| `users:create` | None | Create a user. |
+| `users:delete` |
`global.users:*`
`global.users:id:*`
| Delete a user. |
+| `users:disable` |
`global.users:*`
`global.users:id:*`
| Disable a user. |
+| `users:enable` |
`global.users:*`
`global.users:id:*`
| Enable a user. |
+| `users:logout` |
`global.users:*`
`global.users:id:*`
| Sign out a user. |
+| `users:read` |
`global.users:*`
| Read or search user profiles. |
+| `users:write` |
`global.users:*`
`global.users:id:*`
| Update a user’s profile. |
+{ .no-spacing-list }
+
### Grafana OnCall action definitions (beta)
-> **Note:** Available from Grafana 9.4 in early access.
-
-> **Note:** This feature is behind the `accessControlOnCall` feature toggle.
-> You can enable feature toggles through configuration file or environment variables. See configuration [docs]({{< relref "../../../../setup-grafana/configure-grafana/#feature_toggles" >}}) for details.
-
The following list contains role-based access control actions used by Grafana OnCall application plugin.
-| Action | Applicable scope | Description |
-| ------------------------------------------------ | ---------------- | ------------------------------------------------- |
-| `grafana-oncall-app.alert-groups:read` | n/a | Read OnCall alert groups. |
-| `grafana-oncall-app.alert-groups:write` | n/a | Create, edit and delete OnCall alert groups. |
-| `grafana-oncall-app.integrations:read` | n/a | Read OnCall integrations. |
-| `grafana-oncall-app.integrations:write` | n/a | Create, edit and delete OnCall integrations. |
-| `grafana-oncall-app.integrations:test` | n/a | Test OnCall integrations. |
-| `grafana-oncall-app.escalation-chains:read` | n/a | Read OnCall escalation chains. |
-| `grafana-oncall-app.escalation-chains:write` | n/a | Create, edit and delete OnCall escalation chains. |
-| `grafana-oncall-app.schedules:read` | n/a | Read OnCall schedules. |
-| `grafana-oncall-app.schedules:write` | n/a | Create, edit and delete OnCall schedules. |
-| `grafana-oncall-app.schedules:export` | n/a | Export OnCall schedules. |
-| `grafana-oncall-app.chatops:read` | n/a | Read OnCall ChatOps. |
-| `grafana-oncall-app.chatops:write` | n/a | Edit OnCall ChatOps. |
-| `grafana-oncall-app.chatops:update-settings` | n/a | Edit OnCall ChatOps settings. |
-| `grafana-oncall-app.maintenance:read` | n/a | Read OnCall maintenance. |
-| `grafana-oncall-app.maintenance:write` | n/a | Edit OnCall maintenance. |
-| `grafana-oncall-app.api-keys:read` | n/a | Read OnCall API keys. |
-| `grafana-oncall-app.api-keys:write` | n/a | Create, edit and delete OnCall API keys. |
-| `grafana-oncall-app.notifications:read` | n/a | Receive OnCall notifications. |
-| `grafana-oncall-app.notification-settings:read` | n/a | Read OnCall notification settings. |
-| `grafana-oncall-app.notification-settings:write` | n/a | Edit OnCall notification settings. |
-| `grafana-oncall-app.user-settings:read` | n/a | Read user's own OnCall user settings. |
-| `grafana-oncall-app.user-settings:write` | n/a | Edit user's own OnCall user settings. |
-| `grafana-oncall-app.user-settings:admin` | n/a | Read and edit all users' OnCall user settings. |
-| `grafana-oncall-app.other-settings:read` | n/a | Read OnCall settings. |
-| `grafana-oncall-app.other-settings:write` | n/a | Edit OnCall settings. |
+| Action | Applicable scopes | Description |
+| ------------------------------------------------ | ----------------- | ------------------------------------------------- |
+| `grafana-oncall-app.alert-groups:read` | None | Read OnCall alert groups. |
+| `grafana-oncall-app.alert-groups:write` | None | Create, edit and delete OnCall alert groups. |
+| `grafana-oncall-app.integrations:read` | None | Read OnCall integrations. |
+| `grafana-oncall-app.integrations:write` | None | Create, edit and delete OnCall integrations. |
+| `grafana-oncall-app.integrations:test` | None | Test OnCall integrations. |
+| `grafana-oncall-app.escalation-chains:read` | None | Read OnCall escalation chains. |
+| `grafana-oncall-app.escalation-chains:write` | None | Create, edit and delete OnCall escalation chains. |
+| `grafana-oncall-app.schedules:read` | None | Read OnCall schedules. |
+| `grafana-oncall-app.schedules:write` | None | Create, edit and delete OnCall schedules. |
+| `grafana-oncall-app.schedules:export` | None | Export OnCall schedules. |
+| `grafana-oncall-app.chatops:read` | None | Read OnCall ChatOps. |
+| `grafana-oncall-app.chatops:write` | None | Edit OnCall ChatOps. |
+| `grafana-oncall-app.chatops:update-settings` | None | Edit OnCall ChatOps settings. |
+| `grafana-oncall-app.maintenance:read` | None | Read OnCall maintenance. |
+| `grafana-oncall-app.maintenance:write` | None | Edit OnCall maintenance. |
+| `grafana-oncall-app.api-keys:read` | None | Read OnCall API keys. |
+| `grafana-oncall-app.api-keys:write` | None | Create, edit and delete OnCall API keys. |
+| `grafana-oncall-app.notifications:read` | None | Receive OnCall notifications. |
+| `grafana-oncall-app.notification-settings:read` | None | Read OnCall notification settings. |
+| `grafana-oncall-app.notification-settings:write` | None | Edit OnCall notification settings. |
+| `grafana-oncall-app.user-settings:read` | None | Read user's own OnCall user settings. |
+| `grafana-oncall-app.user-settings:write` | None | Edit user's own OnCall user settings. |
+| `grafana-oncall-app.user-settings:admin` | None | Read and edit all users' OnCall user settings. |
+| `grafana-oncall-app.other-settings:read` | None | Read OnCall settings. |
+| `grafana-oncall-app.other-settings:write` | None | Edit OnCall settings. |
### Grafana Adaptive Metrics action definitions
The following list contains role-based access control actions used by Grafana Adaptive Metrics.
-| Action | Applicable scope | Description |
-| ---------------------------------------------------- | ---------------- | ----------------------------------------------------- |
-| `grafana‑adaptive‑metrics‑app.plugin:access` | n/a | Access the Adaptive Metrics plugin in Grafana Cloud. |
-| `grafana‑adaptive‑metrics‑app.config:read` | n/a | Read the Adaptive Metrics app configuration. |
-| `grafana‑adaptive‑metrics‑app.config:write` | n/a | Update the Adaptive Metrics app configuration. |
-| `grafana‑adaptive‑metrics‑app.recommendations:read` | n/a | Read aggregation recommendations. |
-| `grafana‑adaptive‑metrics‑app.recommendations:apply` | n/a | Apply aggregation recommendations. |
-| `grafana‑adaptive‑metrics‑app.rules:read` | n/a | Read aggregation rules. |
-| `grafana‑adaptive‑metrics‑app.rules:write` | n/a | Create aggregation rules. |
-| `grafana‑adaptive‑metrics‑app.rules:delete` | n/a | Delete aggregation rules. |
-| `grafana‑adaptive‑metrics‑app.exemptions:read` | n/a | Read recommendation exemptions. |
-| `grafana‑adaptive‑metrics‑app.exemptions:write` | n/a | Create, update, and delete recommendation exemptions. |
+| Action | Applicable scopes | Description |
+| ---------------------------------------------------- | ----------------- | ----------------------------------------------------- |
+| `grafana‑adaptive‑metrics‑app.plugin:access` | None | Access the Adaptive Metrics plugin in Grafana Cloud. |
+| `grafana‑adaptive‑metrics‑app.config:read` | None | Read the Adaptive Metrics app configuration. |
+| `grafana‑adaptive‑metrics‑app.config:write` | None | Update the Adaptive Metrics app configuration. |
+| `grafana‑adaptive‑metrics‑app.recommendations:read` | None | Read aggregation recommendations. |
+| `grafana‑adaptive‑metrics‑app.recommendations:apply` | None | Apply aggregation recommendations. |
+| `grafana‑adaptive‑metrics‑app.rules:read` | None | Read aggregation rules. |
+| `grafana‑adaptive‑metrics‑app.rules:write` | None | Create aggregation rules. |
+| `grafana‑adaptive‑metrics‑app.rules:delete` | None | Delete aggregation rules. |
+| `grafana‑adaptive‑metrics‑app.exemptions:read` | None | Read recommendation exemptions. |
+| `grafana‑adaptive‑metrics‑app.exemptions:write` | None | Create, update, and delete recommendation exemptions. |
## Scope definitions
The following list contains role-based access control scopes.
-| Scopes | Descriptions |
-| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `annotations:*` `annotations:type:*` | Restrict an action to a set of annotations. For example, `annotations:*` matches any annotation, `annotations:type:dashboard` matches annotations associated with dashboards and `annotations:type:organization` matches organization annotations. |
-| `apikeys:*` `apikeys:id:*` | Restrict an action to a set of API keys. For example, `apikeys:*` matches any API key, `apikey:id:1` matches the API key whose id is `1`. |
-| `dashboards:*` `dashboards:uid:*` | Restrict an action to a set of dashboards. For example, `dashboards:*` matches any dashboard, and `dashboards:uid:1` matches the dashboard whose UID is `1`. |
-| `datasources:*` `datasources:uid:*` | Restrict an action to a set of data sources. For example, `datasources:*` matches any data source, and `datasources:uid:1` matches the data source whose UID is `1`. |
-| `folders:*` `folders:uid:*` | Restrict an action to a set of folders. For example, `folders:*` matches any folder, and `folders:uid:1` matches the folder whose UID is `1`. Note that permissions granted to a folder cascade down to subfolders located under it |
-| `global.users:*` `global.users:id:*` | Restrict an action to a set of global users. For example, `global.users:*` matches any user and `global.users:id:1` matches the user whose ID is `1`. |
-| `library.panels:*` `library.panels:uid:*` | Restrict an action to a set of library panels. For example, `library.panels:*` matches any library panel, and `library.panel:uid:1` matches the library panel whose UID is `1`. |
-| `orgs:*` `orgs:id:*` | Restrict an action to a set of organizations. For example, `orgs:*` matches any organization and `orgs:id:1` matches the organization whose ID is `1`. |
-| `permissions:type:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. |
-| `permissions:type:escalate` | The scope is required to trigger the reset of basic roles permissions. It indicates that users might acquire additional permissions they did not previously have. |
-| `plugins:*` `plugins:id:*` | Restrict an action to a set of plugins. For example, `plugins:id:grafana-oncall-app` matches Grafana OnCall plugin, and `plugins:*` matches all plugins. |
-| `provisioners:*` | Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the role-based access control [provisioner]({{< relref "./rbac-grafana-provisioning/" >}}). |
-| `reports:*` `reports:id:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:id:1` matches the report whose ID is `1`. |
-| `roles:*` `roles:uid:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role and `roles:uid:randomuid` matches only the role whose UID is `randomuid`. |
-| `services:accesscontrol` | Restrict an action to target only the role-based access control service. You can use this in conjunction with the `status:accesscontrol` actions. |
-| `serviceaccounts:*` `serviceaccounts:id:*` | Restrict an action to a set of service account from an organization. For example, `serviceaccounts:*` matches any service account and `serviceaccount:id:1` matches the service account whose ID is `1`. |
-| `settings:*` | Restrict an action to a subset of settings. For example, `settings:*` matches all settings, `settings:auth.saml:*` matches all SAML settings, and `settings:auth.saml:enabled` matches the enable property on the SAML settings. |
-| `teams:*` `teams:id:*` | Restrict an action to a set of teams from an organization. For example, `teams:*` matches any team and `teams:id:1` matches the team whose ID is `1`. |
-| `users:*` `users:id:*` | Restrict an action to a set of users from an organization. For example, `users:*` matches any user and `users:id:1` matches the user whose ID is `1`. |
-| `n/a` | `n/a` means not applicable. If an action has `n/a` specified for the scope, then the action does not require a scope. For example, the `teams:create` action does not require a scope and allows users to create teams. |
+
+| Scopes | Descriptions |
+| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+|
`annotations:*`
`annotations:type:*`
| Restrict an action to a set of annotations. For example, `annotations:*` matches any annotation, `annotations:type:dashboard` matches annotations associated with dashboards and `annotations:type:organization` matches organization annotations. |
+|
`apikeys:*`
`apikeys:id:*`
| Restrict an action to a set of API keys. For example, `apikeys:*` matches any API key, `apikey:id:1` matches the API key whose id is `1`. |
+|
`dashboards:*`
`dashboards:uid:*`
| Restrict an action to a set of dashboards. For example, `dashboards:*` matches any dashboard, and `dashboards:uid:1` matches the dashboard whose UID is `1`. |
+|
`datasources:*`
`datasources:uid:*`
| Restrict an action to a set of data sources. For example, `datasources:*` matches any data source, and `datasources:uid:1` matches the data source whose UID is `1`. |
+|
`folders:*`
`folders:uid:*`
| Restrict an action to a set of folders. For example, `folders:*` matches any folder, and `folders:uid:1` matches the folder whose UID is `1`. Note that permissions granted to a folder cascade down to subfolders located under it. |
+|
`global.users:*`
`global.users:id:*`
| Restrict an action to a set of global users. For example, `global.users:*` matches any user and `global.users:id:1` matches the user whose ID is `1`. |
+|
`library.panels:*`
`library.panels:uid:*`
| Restrict an action to a set of library panels. For example, `library.panels:*` matches any library panel, and `library.panel:uid:1` matches the library panel whose UID is `1`. |
+|
`orgs:*`
`orgs:id:*`
| Restrict an action to a set of organizations. For example, `orgs:*` matches any organization and `orgs:id:1` matches the organization whose ID is `1`. |
+|
`permissions:type:delegate`
| The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. |
+|
`permissions:type:escalate`
| The scope is required to trigger the reset of basic roles permissions. It indicates that users might acquire additional permissions they did not previously have. |
+|
`plugins:*`
`plugins:id:*`
| Restrict an action to a set of plugins. For example, `plugins:id:grafana-oncall-app` matches Grafana OnCall plugin, and `plugins:*` matches all plugins. |
+|
`provisioners:*`
| Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the role-based access control [provisioner]({{< relref "./rbac-grafana-provisioning/" >}}). |
+|
`reports:*`
`reports:id:*`
| Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:id:1` matches the report whose ID is `1`. |
+|
`roles:*`
`roles:uid:*`
| Restrict an action to a set of roles. For example, `roles:*` matches any role and `roles:uid:randomuid` matches only the role whose UID is `randomuid`. |
+|
`services:accesscontrol`
| Restrict an action to target only the role-based access control service. You can use this in conjunction with the `status:accesscontrol` actions. |
+|
`serviceaccounts:*`
`serviceaccounts:id:*`
| Restrict an action to a set of service account from an organization. For example, `serviceaccounts:*` matches any service account and `serviceaccount:id:1` matches the service account whose ID is `1`. |
+|
`settings:*`
| Restrict an action to a subset of settings. For example, `settings:*` matches all settings, `settings:auth.saml:*` matches all SAML settings, and `settings:auth.saml:enabled` matches the enable property on the SAML settings. |
+|
`teams:*`
`teams:id:*`
| Restrict an action to a set of teams from an organization. For example, `teams:*` matches any team and `teams:id:1` matches the team whose ID is `1`. |
+|
`users:*`
`users:id:*`
| Restrict an action to a set of users from an organization. For example, `users:*` matches any user and `users:id:1` matches the user whose ID is `1`. |
+|
None
| If an action has "None" specified for the scope, then the action doesn't require a scope. For example, the `teams:create` action doesn't require a scope and allows users to create teams. |
+{ .no-spacing-list }
+
diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md
index 07bc725d2ac..e6864815761 100644
--- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md
+++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md
@@ -78,9 +78,9 @@ To learn how to use the roles API to determine the role UUIDs, refer to [Manage
| `fixed:datasources.insights:reader` | `fixed_EBZ3NwlfecNPp2p0XcZRC1nfEYk` | `datasources.insights:read` | Read data source insights data. |
| `fixed:datasources.permissions:reader` | `fixed_ErYA-cTN3yn4h4GxaVPcawRhiOY` | `datasources.permissions:read` | Read data source permissions. |
| `fixed:datasources.permissions:writer` | `fixed_aiQh9YDfLOKjQhYasF9_SFUjQiw` | All permissions from `fixed:datasources.permissions:reader` and `datasources.permissions:write` | Create, read, or delete permissions of a data source. |
-| `fixed:folders:creator` | `fixed_gGLRbZGAGB6n9uECqSh_W382RlQ` | `folders:create` | Create folders in the root level. If granted together with `folders:write` permission, also allows creating subfolders under all folders. |
+| `fixed:folders:creator` | `fixed_gGLRbZGAGB6n9uECqSh_W382RlQ` | `folders:create` | Create folders in the root level. |
| `fixed:folders:reader` | `fixed_yeW-5QPeo-i5PZUIUXMlAA97GnQ` | `folders:read` `dashboards:read` | Read all folders and dashboards. |
-| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and `folders:read` `folders:write` `folders:create` `folders:delete` `folders.permissions:read` `folders.permissions:write` | Read, create, update, and delete all folders and dashboards. If granted together with `fixed:folders:creator`, allows creating subfolders under all folders. |
+| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and `folders:read` `folders:write` `folders:create` `folders:delete` `folders.permissions:read` `folders.permissions:write` | Read, update, and delete all folders and dashboards. Create folders and subfolders. |
| `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. |
| `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and `folders.permissions:write` | Read and update all folder permissions. |
| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read` `ldap.status:read` | Read the LDAP configuration and LDAP status information. |
diff --git a/docs/sources/administration/service-accounts/index.md b/docs/sources/administration/service-accounts/_index.md
similarity index 98%
rename from docs/sources/administration/service-accounts/index.md
rename to docs/sources/administration/service-accounts/_index.md
index ff6971f2096..c5c44cb4dac 100644
--- a/docs/sources/administration/service-accounts/index.md
+++ b/docs/sources/administration/service-accounts/_index.md
@@ -23,7 +23,7 @@ weight: 800
You can use a service account to run automated workloads in Grafana, such as dashboard provisioning, configuration, or report generation. Create service accounts and tokens to authenticate applications, such as Terraform, with the Grafana API.
{{% admonition type="note" %}}
-Service accounts replace [API keys]({{< relref "../api-keys/" >}}) as the primary way to authenticate applications that interact with Grafana.
+Service accounts replace [API keys](/docs/grafana//administration/service-accounts/migrate-api-keys/) as the primary way to authenticate applications that interact with Grafana.
{{% /admonition %}}
A common use case for creating a service account is to perform operations on automated or triggered tasks. You can use service accounts to:
diff --git a/docs/sources/administration/api-keys/index.md b/docs/sources/administration/service-accounts/migrate-api-keys.md
similarity index 61%
rename from docs/sources/administration/api-keys/index.md
rename to docs/sources/administration/service-accounts/migrate-api-keys.md
index fa3bb643b08..b6801913780 100644
--- a/docs/sources/administration/api-keys/index.md
+++ b/docs/sources/administration/service-accounts/migrate-api-keys.md
@@ -1,93 +1,37 @@
---
aliases:
- - about-api-keys/
- - create-api-key/
-description: This section contains information about API keys in Grafana
+ - ../api-keys/ # /docs/grafana//administration/api-keys/
+ - ../about-api-keys/ # /docs/grafana//administration/about-api-keys/
+ - ../create-api-key/ # /docs/grafana//administration/create-api-key/
+description: Learn how to migrate legacy API keys to service account tokens.
keywords:
- API keys
- Service accounts
labels:
products:
- enterprise
+ - cloud
- oss
-menuTitle: API keys
-title: API keys
+menuTitle: Migrate API keys
+title: Migrate API keys to service account tokens
weight: 700
---
-# API keys
+# Migrate API keys to service account tokens
{{% admonition type="note" %}}
-Deprecated: [Service accounts]({{< relref "../service-accounts/" >}}) have replaced API keys as the primary way to authenticate applications that interact with Grafana.
+API keys are deprecated. [Service accounts](/docs/grafana/latest/administration/service-accounts/) now replace API keys for authenticating with the **HTTP APIs** and interacting with Grafana.
{{% /admonition %}}
-An API key is a randomly generated string that external systems use to interact with Grafana HTTP APIs.
+API keys specify a role—either **Admin**, **Editor**, or **Viewer**—that determine the permissions associated with interacting with Grafana.
-When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources.
+Compared to API keys, service accounts have limited scopes that provide more security. For more information on the benefits of service accounts, refer to [service account benefits](/docs/grafana/latest/administration/service-accounts/#service-account-benefits).
-{{% admonition type="note" %}}
-If you use Grafana v9.1 or newer, use service accounts instead of API keys. For more information, refer to [Grafana service accounts]({{< relref "../service-accounts/" >}}).
-{{% /admonition %}}
+When you migrate an API key to a service account, a service account is created with a service account token. Your existing API key—now migrated to a service account token—will continue working as before.
-{{< section >}}
+To find the migrated API keys, click **Administration** in the left-side menu, then **Users and access -> Service Accounts**, select the service account, and locate the **Token**.
-## Create an API key
-
-Create an API key when you want to manage your computed workload with a user.
-
-This topic shows you how to create an API key using the Grafana UI. You can also create an API key using the Grafana HTTP API. For more information about creating API keys via the API, refer to [Create API key via API]({{< relref "../../developers/http_api/create-api-tokens-for-org/#how-to-create-a-new-organization-and-an-api-token" >}}).
-
-### Before you begin
-
-To follow these instructions, you need at least one of the following:
-
-- Administrator permissions
-- Editor permissions
-- Service account writer
-
-- To ensure you have permission to create and edit API keys, follow the instructions in [Roles and permissions]({{< relref "../roles-and-permissions/#" >}}).
-
-### Steps
-
-To create an API key, complete the following steps:
-
-1. Sign in to Grafana.
-1. Click **Administration** in the left-side menu, **Users and access**, and select **API Keys**.
-1. Click **Add API key**.
-1. Enter a unique name for the key.
-1. In the **Role** field, select one of the following access levels you want to assign to the key.
- - **Admin**: Enables a user to use APIs at the broadest, most powerful administrative level.
- - **Editor** or **Viewer** to limit the key's users to those levels of power.
-1. In the **Time to live** field, specify how long you want the key to be valid.
- - The maximum length of time is 30 days (one month). You enter a number and a letter. Valid letters include `s` for seconds,`m` for minutes, `h` for hours, `d `for days, `w` for weeks, and `M `for month. For example, `12h` is 12 hours and `1M` is 1 month (30 days).
- - If you are unsure about how long an API key should be valid, we recommend that you choose a short duration, such as a few hours. This approach limits the risk of having API keys that are valid for a long time.
-1. Click **Add**.
-
-## Migrate API keys to Grafana service accounts
-
-As an alternative to using API keys for authentication, you can use a service account-based authentication system. When compared to API keys, service accounts have limited scopes that provide more security than using API keys.
-
-For more information about the benefits of service accounts, refer to [Grafana service account benefits]({{< relref "../service-accounts/#service-account-benefits" >}}).
-
-The service account endpoints generate a machine user for authentication instead of using API keys. When you migrate an API key to a service account, a service account will be created with a service account token.
-
-{{% admonition type="note" %}}
-If you're currently using API keys for authentication, we strongly recommend to use Grafana Service Accounts instead. Rest assured, when migrating to Service Accounts, your existing API keys will continue working as before. To find the migrated API keys, navigate to the Service Accounts section and select the Service Account Tokens tab. For more information, please refer to the [Grafana service account tokens]({{< relref "../service-accounts/#service-account-tokens" >}}) details.
-{{% /admonition %}}
-
-## Ways of migrating API keys to service accounts
-
-If you are currently using API keys in your environment, you need to reconfigure your setup to use service accounts.
-
-Depending on your current setup, you may need to use one or all of the following methods to migrate your environment to service accounts:
-
-- The Grafana user interface: Use this method if you have been using the UI to manage your API keys and want to switch to using service accounts.
-- The Grafana API: Use this method if you have been using API calls to manage your API keys and want to switch to using service accounts programmatically.
-- Terraform: If you have a Terraform configuration that sets up API keys, you need to reconfigure your Terraform to use service accounts instead.
-
-By following these steps, you can successfully migrate your integration from API keys to service accounts and continue using Grafana seamlessly.
-
-### Migrate API keys to Grafana service accounts using the Grafana user interface
+## Migrate API keys using the Grafana user interface
This section shows you how to migrate API keys to Grafana service accounts using the Grafana user interface. You can choose to migrate a single API key or all API keys. When you migrate all API keys, you can no longer create API keys and must use service accounts instead.
@@ -118,9 +62,9 @@ To migrate a single API key to a service account, complete the following steps:
1. Find the API Key you want to migrate.
1. Click **Migrate to service account**.
-### Migrate API keys to Grafana service accounts for API calls
+## Migrate API keys using the HTTP API
-This section shows you how to migrate API keys to Grafana service accounts for Grafana API workflows. For references see: [Grafana Service Accounts for the Grafana API]({{< relref "../../developers/http_api/serviceaccount/#create-service-account" >}}).
+This section shows you how to programmatically migrate API keys to Grafana service accounts using the HTTP API. For API additional information, refer to [Service account HTTP APIs](/docs/grafana/latest/developers/http_api/serviceaccount/).
#### Before you begin
@@ -180,7 +124,7 @@ curl --request GET --url http://localhost:3000/api/folders --header 'Authorizati
[{"id":1,"uid":"a5261a84-eebc-4733-83a9-61f4713561d1","title":"gdev dashboards"}]%
```
-### Migrate API keys to Grafana service accounts in Terraform
+## Migrate API keys using Terraform
{{< admonition type="note" >}}
The terraform resource `api_key` is removed from the Grafana Terraform Provider in v3.0.0.
@@ -201,9 +145,7 @@ terraform {
}
```
-This section shows you how to migrate your Terraform configuration for API keys to Grafana service accounts. For resources, see [Grafana Service Accounts in Terraform](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/service_account_token).
-
-For migration your cloud stack api keys, use the `grafana_cloud_stack_service_account` and `gafana_cloud_stack_service_account_token` resources see [Grafana Cloud Stack Service Accounts in Terraform](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/cloud_stack_service_account).
+This section shows you how to migrate your Terraform configuration for API keys to Grafana service accounts. For additional information, refer to [Grafana Service Accounts in Terraform](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/service_account_token).
#### Steps
@@ -283,9 +225,11 @@ resource "grafana_service_account_token" "sat-foo" {
}
```
-### Migrate Cloud **Stack** API keys to Grafana cloud stack service accounts in Terraform
+## Migrate Cloud Stack API keys using Terraform
-This section shows you how to migrate your Terraform configuration for Grafana cloud stack API keys to Grafana cloud stack service accounts. For migration your cloud stack api keys, use the `grafana_cloud_stack_service_account` and `gafana_cloud_stack_service_account_token` resources see [Grafana Cloud Stack Service Accounts in Terraform](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/cloud_stack_service_account).
+This section shows you how to migrate your Terraform configuration for Grafana cloud stack API keys to Grafana cloud stack service accounts.
+
+For migration your cloud stack api keys, use the `grafana_cloud_stack_service_account` and `gafana_cloud_stack_service_account_token` resources. For additional information, refer to [Grafana Cloud Stack Service Accounts in Terraform](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/cloud_stack_service_account).
{{% admonition type="note" %}}
This is only relevant for Grafana Cloud **Stack** API keys `grafana_cloud_stack_api_key`. Grafana Cloud API keys resource `grafana_cloud_api_key` are not deprecated and should be used for authentication for managing your Grafana cloud.
diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md
index 3ef02d5c1b5..d8ec2362cbd 100644
--- a/docs/sources/alerting/fundamentals/_index.md
+++ b/docs/sources/alerting/fundamentals/_index.md
@@ -57,7 +57,11 @@ refs:
# Introduction to Alerting
-Whether you’re just starting out or you're a more experienced user of Grafana Alerting, learn more about the fundamentals and available features that help you create, manage, and respond to alerts; and improve your team’s ability to resolve issues quickly. For a hands-on introduction, refer to our [tutorial to get started with Grafana Alerting](http://grafana.com/tutorials/alerting-get-started/).
+Whether you’re just starting out or you're a more experienced user of Grafana Alerting, learn more about the fundamentals and available features that help you create, manage, and respond to alerts; and improve your team’s ability to resolve issues quickly.
+
+{{< admonition type="tip" >}}
+For a hands-on introduction, refer to our [tutorial to get started with Grafana Alerting](http://grafana.com/tutorials/alerting-get-started/).
+{{< /admonition >}}
The following diagram gives you an overview of Grafana Alerting and introduces you to some of the fundamental features that are the principles of how Grafana Alerting works.
diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md
index 6b516e80a92..4ea464f6eed 100644
--- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md
+++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md
@@ -72,6 +72,15 @@ In [Configure no data and error handling](ref:no-data-and-error-handling), you c
{{< figure src="/media/docs/alerting/alert-rule-configure-no-data-and-error.png" alt="A screenshot of the `Configure no data and error handling` option in Grafana Alerting." max-width="500px" >}}
+To reduce the number of **No Data** or **Error** state alerts received, try the following.
+
+1. Use the **Keep last state** option. For more information, refer to the section below. This option allows the alert to retain its last known state when there is no data available, rather than switching to a **No Data** state.
+1. For **No Data** alerts, you can optimize your alert rule by expanding the time range of the query. However, if the time range is too big, it affects the performance of the query and can lead to errors due to timeout.
+
+ To minimize timeouts resulting in the **Error** state, reduce the time range to request less data every evaluation cycle.
+
+1. Change the default [evaluation time out](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#evaluation_timeout). The default is set at 30 seconds. To increase the default evaluation timeout, open a support ticket from the [Cloud Portal](https://grafana.com/docs/grafana-cloud/account-management/support/#grafana-cloud-support-options). Note that this should be a last resort, because it may affect the performance of all alert rules and cause missed evaluations if the timeout is too long.
+
#### Keep last state
The "Keep Last State" option helps mitigate temporary data source issues, preventing alerts from unintentionally firing, resolving, and re-firing.
diff --git a/docs/sources/breaking-changes/breaking-changes-v10-0.md b/docs/sources/breaking-changes/breaking-changes-v10-0.md
index 750c713ee19..946ba52c93c 100644
--- a/docs/sources/breaking-changes/breaking-changes-v10-0.md
+++ b/docs/sources/breaking-changes/breaking-changes-v10-0.md
@@ -99,11 +99,11 @@ Grafana's [HTTP API endpoints for generating and managing API Keys]({{< relref "
#### Migration path
-While upgrading to Grafana v10, you don't need to take any action; your API keys will be automatically migrated. To test or perform the migration from API keys to service accounts before upgrading to Grafana v10, follow our [migration documentation]({{< relref "../administration/api-keys/#migrate-api-keys-to-grafana-service-account" >}}").
+While upgrading to Grafana v10, you don't need to take any action; your API keys will be automatically migrated. To test or perform the migration from API keys to service accounts before upgrading to Grafana v10, follow our [migration documentation](https://grafana.com/docs/grafana//administration/service-accounts/migrate-api-keys/).
#### Learn more
-- [Documentation on migrating from API keys to service accounts]({{< relref "../administration/api-keys/" >}})
+- [Documentation on migrating from API keys to service accounts](https://grafana.com/docs/grafana//administration/service-accounts/migrate-api-keys/)
- [Blog post announcement with a video demo including how to migrate](https://grafana.com/blog/2022/08/24/new-in-grafana-9.1-service-accounts-are-now-ga/)
diff --git a/docs/sources/dashboards/manage-dashboards/index.md b/docs/sources/dashboards/manage-dashboards/index.md
index 7a9e7371ceb..6853bcb02a7 100644
--- a/docs/sources/dashboards/manage-dashboards/index.md
+++ b/docs/sources/dashboards/manage-dashboards/index.md
@@ -70,7 +70,7 @@ If you have permission to view all folders, you won't see a **Shared with me**.
Folders help you organize and group dashboards, which is useful when you have many dashboards or multiple teams using the same Grafana instance.
-> **Before you begin:** Ensure you have Editor permissions or greater to create folders. For more information about dashboard permissions, refer to [Dashboard permissions](ref:dashboard-permissions).
+> **Before you begin:** Ensure you have organization Editor permissions or greater to create root level folders or Edit or Admin access to a parent folder to create subfolders. For more information about dashboard permissions, refer to [Dashboard permissions](ref:dashboard-permissions).
**To create a dashboard folder:**
diff --git a/docs/sources/developers/http_api/_index.md b/docs/sources/developers/http_api/_index.md
index 806ab1f8650..28ec50df492 100644
--- a/docs/sources/developers/http_api/_index.md
+++ b/docs/sources/developers/http_api/_index.md
@@ -80,40 +80,41 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
- [Admin API]({{< relref "admin/" >}})
- [Alerting API (unstable)](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json)
-- [Alerting Provisioning API]({{< relref "alerting_provisioning/" >}})
+- [Alerting provisioning API]({{< relref "alerting_provisioning/" >}})
- [Annotations API]({{< relref "annotations/" >}})
- [Correlations API]({{< relref "correlations/" >}})
- [Dashboard API]({{< relref "dashboard/" >}})
-- [Dashboard Permissions API]({{< relref "dashboard_permissions/" >}})
-- [Dashboard Versions API]({{< relref "dashboard_versions/" >}})
+- [Dashboard permissions API]({{< relref "dashboard_permissions/" >}})
+- [Dashboard versions API]({{< relref "dashboard_versions/" >}})
- [Data source API]({{< relref "data_source/" >}})
- [Folder API]({{< relref "folder/" >}})
-- [Folder Permissions API]({{< relref "folder_permissions/" >}})
-- [Folder/Dashboard Search API]({{< relref "folder_dashboard_search/" >}})
-- [Library Element API]({{< relref "library_element/" >}})
+- [Folder permissions API]({{< relref "folder_permissions/" >}})
+- [Folder/Dashboard search API]({{< relref "folder_dashboard_search/" >}})
+- [Library element API]({{< relref "library_element/" >}})
- [Organization API]({{< relref "org/" >}})
- [Other API]({{< relref "other/" >}})
- [Playlists API]({{< relref "playlist/" >}})
- [Preferences API]({{< relref "preferences/" >}})
-- [Short URL API]({{< relref "short_url/" >}})
+- [Public dashboard API]({{< relref "dashboard_public/" >}})
- [Query history API]({{< relref "query_history/" >}})
+- [Service account API]({{< relref "serviceaccount/" >}})
+- [Short URL API]({{< relref "short_url/" >}})
- [Snapshot API]({{< relref "snapshot/" >}})
+- [SSO settings API]({{< relref "sso-settings/" >}})
- [Team API]({{< relref "team/" >}})
- [User API]({{< relref "user/" >}})
## Deprecated HTTP APIs
-- [Alerting Notification Channels API]({{< relref "alerting_notification_channels/" >}})
-- [Alerting API]({{< relref "alerting/" >}})
- [Authentication API]({{< relref "auth/" >}})
## Grafana Enterprise HTTP APIs
Grafana Enterprise includes all of the Grafana OSS APIs as well as those that follow:
-- [Role-based access control API]({{< relref "access_control/" >}})
- [Data source permissions API]({{< relref "datasource_permissions/" >}})
-- [Team sync API]({{< relref "team_sync/" >}})
- [License API]({{< relref "licensing/" >}})
-- [Reporting API]({{< relref "reporting/" >}})
- [Query and resource caching API]({{< relref "query_and_resource_caching/" >}})
+- [Reporting API]({{< relref "reporting/" >}})
+- [Role-based access control API]({{< relref "access_control/" >}})
+- [Team sync API]({{< relref "team_sync/" >}})
diff --git a/docs/sources/developers/http_api/auth.md b/docs/sources/developers/http_api/auth.md
index 4d5ce4d8740..71b1b5c491f 100644
--- a/docs/sources/developers/http_api/auth.md
+++ b/docs/sources/developers/http_api/auth.md
@@ -85,7 +85,7 @@ This endpoint has been made obsolete in Grafana 11.3.0.
{{% /admonition %}}
-Endpoint is obsolete and has been moved to [Grafana service account API]({{< relref "./serviceaccount/" >}}). For more information, refer to [Migrate to Grafana service account API]({{< relref "../../administration/api-keys/#migrate-api-keys-to-grafana-service-accounts-using-the-api" >}}).
+Endpoint is obsolete and has been moved to [Grafana service account API]({{< relref "./serviceaccount/" >}}). For more information, refer to [Migrate to Grafana service account API](/docs/grafana//administration/service-accounts/migrate-api-keys/).
`POST /api/auth/keys`
diff --git a/docs/sources/developers/http_api/folder.md b/docs/sources/developers/http_api/folder.md
index aaae48dc486..fcb4f806063 100644
--- a/docs/sources/developers/http_api/folder.md
+++ b/docs/sources/developers/http_api/folder.md
@@ -143,11 +143,11 @@ Creates a new folder.
See note in the [introduction]({{< ref "#folder-api" >}}) for an explanation.
-`folders:create` allows creating folders in the root level. To create a subfolder, `folders:write` scoped to the parent folder is required in addition to `folders:create`.
+`folders:create` allows creating folders and subfolders. If granted with scope `folders:uid:general`, allows creating root level folders. Otherwise, allows creating subfolders under the specified folders.
| Action | Scope |
| ---------------- | ----------- |
-| `folders:create` | n/a |
+| `folders:create` | `folders:*` |
| `folders:write` | `folders:*` |
**Example Request**:
@@ -411,14 +411,14 @@ See note in the [introduction]({{< ref "#folder-api" >}}) for an explanation.
If moving the folder under another folder:
-| Action | Scope |
-| --------------- | -------------------------------------- |
-| `folders:write` | `folders:uid:` |
+| Action | Scope |
+| ---------------- | ----------------------------------------------------- |
+| `folders:create` | `folders:uid:` `folders:*` |
If moving the folder under root:
| Action | Scope |
| -------------- | ------------- |
-| `folders:create` | `folders:*` |
+| `folders:create` | `folders:uid:general` `folders:*` |
JSON body schema:
diff --git a/docs/sources/developers/http_api/preferences.md b/docs/sources/developers/http_api/preferences.md
index 91a81849cea..42847e1e2eb 100644
--- a/docs/sources/developers/http_api/preferences.md
+++ b/docs/sources/developers/http_api/preferences.md
@@ -13,7 +13,7 @@ labels:
products:
- enterprise
- oss
-title: 'HTTP Preferences API '
+title: 'Preferences API'
---
# User and Org Preferences API
diff --git a/docs/sources/developers/http_api/snapshot.md b/docs/sources/developers/http_api/snapshot.md
index 54360b768dd..8329d4b62b2 100644
--- a/docs/sources/developers/http_api/snapshot.md
+++ b/docs/sources/developers/http_api/snapshot.md
@@ -13,7 +13,7 @@ labels:
products:
- enterprise
- oss
-title: 'HTTP Snapshot API '
+title: 'Snapshot API'
---
# Snapshot API
diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json
index 1913d9aef08..3442ae7d2a4 100644
--- a/docs/sources/developers/plugins/plugin.schema.json
+++ b/docs/sources/developers/plugins/plugin.schema.json
@@ -308,6 +308,10 @@
"type": "boolean",
"description": "For data source plugins, if the plugin supports metric queries. Used to enable the plugin in the panel editor."
},
+ "multiValueFilterOperators": {
+ "type": "boolean",
+ "description": "For data source plugins, if the plugin supports multi value operators in adhoc filters."
+ },
"pascalName": {
"type": "string",
"description": "[internal only] The PascalCase name for the plugin. Used for creating machine-friendly identifiers, typically in code generation. If not provided, defaults to name, but title-cased and sanitized (only alphabetical characters allowed).",
diff --git a/docs/sources/panels-visualizations/visualizations/alert-list/index.md b/docs/sources/panels-visualizations/visualizations/alert-list/index.md
index d40d3acf325..e0bcecfcc9a 100644
--- a/docs/sources/panels-visualizations/visualizations/alert-list/index.md
+++ b/docs/sources/panels-visualizations/visualizations/alert-list/index.md
@@ -50,6 +50,10 @@ Once you’ve created a [dashboard](https://grafana.com/docs/grafana/ We would love your feedback on Canvas. Please check out the [open Github issues](https://github.com/grafana/grafana/issues?page=1&q=is%3Aopen+is%3Aissue+label%3Aarea%2Fpanel%2Fcanvas) and [submit a new feature request](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request,area%2Fpanel%2Fcanvas&title=Canvas:&projects=grafana-dataviz&template=1-feature_requests.md) as needed.
+{{< video-embed src="/static/img/docs/canvas-panel/canvas-beta-overview-9-2-0.mp4" max-width="750px" alt="Canvas beta overview" >}}
-{{< video-embed src="/static/img/docs/canvas-panel/canvas-beta-overview-9-2-0.mp4" max-width="750px" caption="Canvas beta overview" >}}
+If you've used popular UI and web design tools, then designing canvases will feel very familiar.
+With all of these dynamic elements, there's almost no limit to what a canvas can display.
+
+{{< admonition type="note" >}}
+We'd love your feedback on the canvas visualization. Please check out the [open Github issues](https://github.com/grafana/grafana/issues?page=1&q=is%3Aopen+is%3Aissue+label%3Aarea%2Fpanel%2Fcanvas) and [submit a new feature request](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request,area%2Fpanel%2Fcanvas&title=Canvas:&projects=grafana-dataviz&template=1-feature_requests.md) as needed.
+{{< /admonition >}}
+
+## Supported data formats
+
+The canvas visualization is unique in that it doesn't have any specific data requirements. You can even start adding and configuring visual elements without providing any data. However, any data you plan to consume should be accessible through supported Grafana data sources and structured in a way that ensures smooth integration with your custom elements.
+
+If your canvas is going to update in real time, your data should support refreshes at your desired intervals without degrading the user experience.
+
+You can tie [Elements](#elements) and [Connections](#connections) to data through options like text, colors, and background pattern images, etc. available in the canvas visualization.
## Elements
diff --git a/docs/sources/panels-visualizations/visualizations/histogram/index.md b/docs/sources/panels-visualizations/visualizations/histogram/index.md
index 79a2bf1bac2..70fcc675d02 100644
--- a/docs/sources/panels-visualizations/visualizations/histogram/index.md
+++ b/docs/sources/panels-visualizations/visualizations/histogram/index.md
@@ -100,6 +100,10 @@ The data is converted as follows:
Use the following options to refine your histogram visualization.
+### Bucket count
+
+Specifies the number of bins used to group your data in the histogram, affecting the granularity of the displayed distribution. Leave this empty for automatic bucket count of 30.
+
### Bucket size
The size of the buckets. Leave this empty for automatic bucket sizing (~10% of the full range).
@@ -112,6 +116,14 @@ If the first bucket should not start at zero. A non-zero offset has the effect o
This will merge all series and fields into a combined histogram.
+### Stacking
+
+Controls how multiple series are displayed in the histogram. Choose from the following:
+
+- **Off** - Series are not stacked, but instead shown side by side.
+- **Normal** - Series are stacked on top of each other, showing cumulative values.
+- **100%** - Series are stacked to fill 100% of the chart, showing the relative proportion of each series.
+
### Line width
Controls line width of the bars.
@@ -126,17 +138,12 @@ Set the mode of the gradient fill. Fill gradient is based on the line color. To
Gradient display is influenced by the **Fill opacity** setting.
-#### None
+Choose from the following:
-No gradient fill. This is the default setting.
-
-#### Opacity
-
-Transparency of the gradient is calculated based on the values on the Y-axis. The opacity of the fill is increasing with the values on the Y-axis.
-
-#### Hue
-
-Gradient color is generated based on the hue of the line color.
+- **None** - No gradient fill. This is the default setting.
+- **Opacity** - Transparency of the gradient is calculated based on the values on the Y-axis. The opacity of the fill is increasing with the values on the Y-axis.
+- **Hue** - Gradient color is generated based on the hue of the line color.
+- **Scheme** - The selected [color palette](https://grafana.com/docs/grafana/latest/panels-visualizations/configure-standard-options/#color-scheme) is applied to the histogram bars.
## Tooltip options
diff --git a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md
index 01f49e3de1a..9f56def9011 100644
--- a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md
+++ b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md
@@ -23,9 +23,18 @@ refs:
# Pie chart
-{{< figure src="/static/img/docs/pie-chart-panel/pie-chart-example.png" max-width="1200px" lightbox="true" caption="Pie charts" >}}
+A pie chart is a graph that displays data as segments of a circle proportional to the whole, making it look like a sliced pie. Each slice corresponds to a value or measurement.
-Pie charts display reduced series, or values in a series, from one or more queries, as they relate to each other, in the form of slices of a pie. The arc length, area and central angle of a slice are all proportional to the slices value, as it relates to the sum of all values. This type of chart is best used when you want a quick comparison of a small set of values in an aesthetically pleasing form.
+{{< figure src="/static/img/docs/pie-chart-panel/pie-chart-example.png" max-width="1200px" lightbox="true" alt="Pie charts" >}}
+
+The pie chart visualization is ideal when you have data that adds up to a total and you want to show the proportion of each value compared to other slices, as well as to the whole of the pie.
+
+You can use a pie chart if you need to compare:
+
+- Browser share distribution in the market
+- Incident causes per category
+- Network traffic sources
+- User demographics
## Configure a pie chart visualization
@@ -35,6 +44,60 @@ The following video guides you through the creation steps and common customizati
{{< docs/play title="Grafana Bar Charts and Pie Charts" url="https://play.grafana.org/d/ktMs4D6Mk/" >}}
+## Supported data formats
+
+The pie chart is different from other visualizations in that it will only display one pie, regardless of the number of datasets, fields, or records queried in it.
+
+To create a pie chart visualization, you need a dataset containing a set of numeric values either in rows, columns, or both.
+
+### Example - One row
+
+The easiest way to provide data for a pie chart visualization is in a dataset with a single record (or row) containing the fields (or columns) that you want in the pie, as in the following example. The default settings of the pie chart visualization automatically display each column as a slice of the pie.
+
+| Value1 | Value2 | Value3 | Optional |
+| ------ | ------ | ------ | -------- |
+| 5 | 3 | 2 | Sums10 |
+
+
+
+### Example - Multiple rows
+
+If you need to use numeric data that's in multiple rows, the default **Show** parameter of the visualization [Value options](#value-options) is set to **Calculate** and use data from the last row.
+
+| Value | Label |
+| ----- | ------ |
+| 5 | Value1 |
+| 3 | Value2 |
+| 2 | Value3 |
+
+
+
+By default, the visualization is configured to [calculate](#value-options) a single value per column or series and to display only the last row of data.
+
+To allow values in multiple rows to be displayed, change the **Show** setting in the [Value options](#value-options) from **Calculate** to **All values**.
+
+
+
+### Example - Multiple rows and columns
+
+If your dataset contains multiple rows and columns with numeric data, by default only the last row's values are summed.
+
+| Value1 | Value2 | Value3 | Optional |
+| ------ | ------ | ------ | -------- |
+| 5 | 3 | 2 | Sums10 |
+| 10 | 6 | 4 | Sums20 |
+| 20 | 8 | 2 | Sums30 |
+
+
+
+If you want to display all the cells, change the **Show** setting in the [Value options](#value-options) from **Calculate** to **All values**. This also labels the elements by concatenating all the text fields (if you have any) with the column name.
+
+
+
+If you want to display only the values from a given field (or column), once the **Show** setting in the [Value options](#value-options) is set to **All values**, set the **Fields** option to the column you wish to sum in the display. The value labels are also concatenated as indicated before.
+
+
+
## Panel options
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="" >}}
@@ -96,10 +159,6 @@ The following example shows a pie chart with **Name** and **Percent** labels dis

-## Standard options
-
-{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}}
-
## Tooltip options
{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}}
@@ -137,6 +196,14 @@ Select values to display in the legend. You can select more than one.
- **Percent:** The percentage of the whole.
- **Value:** The raw numerical value.
+## Standard options
+
+{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}}
+
+## Data links
+
+{{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}}
+
## Value mappings
{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}}
diff --git a/docs/sources/panels-visualizations/visualizations/status-history/index.md b/docs/sources/panels-visualizations/visualizations/status-history/index.md
index 67326723ea5..b7825b94b82 100644
--- a/docs/sources/panels-visualizations/visualizations/status-history/index.md
+++ b/docs/sources/panels-visualizations/visualizations/status-history/index.md
@@ -107,6 +107,10 @@ Use these options to refine the visualization.
Controls whether values are rendered inside the value boxes. Auto will render values if there is sufficient space.
+### Row height
+
+Controls the height of boxes. 1 = maximum space and 0 = minimum space.
+
### Column width
Controls the width of boxes. 1 = maximum space and 0 = minimum space.
@@ -119,10 +123,6 @@ Controls line width of state regions.
Controls the opacity of state regions.
-## Standard options
-
-{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}}
-
## Legend options
{{< docs/shared lookup="visualizations/legend-options-2.md" source="grafana" version="" >}}
@@ -131,6 +131,10 @@ Controls the opacity of state regions.
{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}}
+## Standard options
+
+{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}}
+
## Data links
{{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}}
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index 66328509bd7..a0fae3e97e7 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -193,6 +193,7 @@ Experimental features might be changed or removed without prior notice.
| `backgroundPluginInstaller` | Enable background plugin installer |
| `dataplaneAggregator` | Enable grafana dataplane aggregator |
| `adhocFilterOneOf` | Exposes a new 'one of' operator for ad-hoc filters. This operator allows users to filter by multiple values in a single filter. |
+| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture |
| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying |
| `singleTopNav` | Unifies the top search bar and breadcrumb bar into one |
| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards |
diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md
index 3c4ca721bf4..d18be432199 100644
--- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md
+++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md
@@ -241,7 +241,7 @@ This app registration will be used as a Service Account to retrieve more informa
#### Set up permissions for the application
1. In the overview pane, look for **API permissions** section and select **Add a permission**.
-1. In the **Request API permissions** pane, select **Microsoft Graph**.
+1. In the **Request API permissions** pane, select **Microsoft Graph**, and click **Application permissions**.
1. In the **Select permissions** pane, under the **GroupMember** section, select **GroupMember.Read.All**.
1. In the **Select permissions** pane, under the **User** section, select **User.Read**.
1. Select **Add permissions** at the bottom of the page.
diff --git a/docs/sources/setup-grafana/configure-security/planning-iam-strategy/index.md b/docs/sources/setup-grafana/configure-security/planning-iam-strategy/index.md
index b665009cda2..46979fc11ab 100644
--- a/docs/sources/setup-grafana/configure-security/planning-iam-strategy/index.md
+++ b/docs/sources/setup-grafana/configure-security/planning-iam-strategy/index.md
@@ -96,7 +96,7 @@ These are just a few examples of how Grafana can be used in M2M scenarios. The p
You can use a service account to run automated workloads in Grafana, such as dashboard provisioning, configuration, or report generation. Create service accounts and service accounts tokens to authenticate applications, such as Terraform, with the Grafana API.
{{% admonition type="note" %}}
-Service accounts will eventually replace [API keys]({{< relref ".#api-keys" >}}) as the primary way to authenticate applications that interact with Grafana.
+Service accounts will eventually replace [API keys](/docs/grafana//administration/service-accounts/migrate-api-keys/) as the primary way to authenticate applications that interact with Grafana.
{{% /admonition %}}
A common use case for creating a service account is to perform operations on automated or triggered tasks. You can use service accounts to:
diff --git a/docs/sources/setup-grafana/configure-security/secret-scan.md b/docs/sources/setup-grafana/configure-security/secret-scan.md
index dbca7a971cf..031c1c06175 100644
--- a/docs/sources/setup-grafana/configure-security/secret-scan.md
+++ b/docs/sources/setup-grafana/configure-security/secret-scan.md
@@ -46,7 +46,7 @@ Secret scanning is disabled by default. Outgoing connections are made once you e
## Before you begin
- Ensure all your API keys have been migrated to service accounts.
- For more information about service account migration, refer to [Migrate API keys to Grafana service accounts]({{< relref "../../administration/api-keys#migrate-api-keys-to-grafana-service-accounts" >}}).
+ For more information about service account migration, refer to [Migrate API keys to Grafana service accounts](/docs/grafana//administration/service-accounts/migrate-api-keys/).
## Configure secret scanning
diff --git a/docs/sources/tutorials/create-alerts-with-logs/index.md b/docs/sources/tutorials/create-alerts-with-logs/index.md
index 87eaf7bb994..cc04b4731e7 100644
--- a/docs/sources/tutorials/create-alerts-with-logs/index.md
+++ b/docs/sources/tutorials/create-alerts-with-logs/index.md
@@ -10,423 +10,304 @@ labels:
products:
- enterprise
- oss
+ - cloud
- loki
-status: draft
-summary: Create alerts with Logs
+ - alerting
tags:
- advanced
title: How to create alerts with log data
weight: 70
+killercoda:
+ title: How to create alerts with log data
+ description: Learn how to use Loki with Grafana Alerting to keep track of what’s happening in your environment with real log data.
+ preprocessing:
+ substitutions:
+ - regexp: docker compose
+ replacement: docker-compose
+ backend:
+ imageid: ubuntu
---
-# How to create alerts with logs
+
-Loki stores your logs and only indexes labels for each log stream. Using Loki with Grafana Alerting is a powerful way to keep track of what's happening in your environment. You can create metric alerts based on content in your log lines to notify your team. Even better, you can add label data from the log message directly into your alert notification.
+# How to create alert rules with log data
+
+Loki stores your logs and only indexes labels for each log stream. Using Loki with Grafana Alerting is a powerful way to keep track of what’s happening in your environment. You can create metric alert rules based on content in your log lines to notify your team. What’s even better is that you can add label data from the log message directly into your alert notification.
In this tutorial, you'll:
-- Create a conditional alert using Loki.
-- Create a custom alert message template.
-- Configure an email notification that includes part of the log message.
+- Generate sample logs and pull them with Promtail to Grafana.
+- Create an alert rule based on a Loki query (LogQL).
+- Create a Webhook contact point to send alert notifications to.
+
+
+
+{{< admonition type="tip" >}}
+Check out our [advanced alerting tutorial](https://grafana.com/tutorials/alerting-get-started-pt2/) to explore advanced topics such as alert instances and notification routing.
+{{< /admonition >}}
+
+
+
+{{< docs/ignore >}}
+
+> Check out our [advanced alerting tutorial](https://grafana.com/tutorials/alerting-get-started-pt2/) to explore advanced topics such as alert instances and notification routing.
+
+{{< /docs/ignore >}}
+
+
+
+
## Before you begin
-- Ensure you’ve [configured a Loki datasource](https://grafana.com/docs/grafana/latest/datasources/loki/#configure-the-data-source) in Grafana.
-- If you already have logs to work with, you can skip the optional sections and go straight to [create an alert](#create-an-alert).
-- If you want to use a log-generating sample script to create the logs demonstrated in this tutorial, refer to the optional steps:
- - [Use promtail and log-generating script](#optional-use-promtail-and-a-python-script-to-create-sample-logs-and-send-them-to-loki)
- - [Use docker with promtail and the log-generating script](#optional-running-the-tutorial-using-grafana-loki-and-promtail-with-docker-compose)
+
-## Create an alert
+### Grafana Cloud users
-In these steps you'll create an alert and define an expression to evaluate. These examples use a classic condition.
+As a Grafana Cloud user, you don't have to install anything.
-### Create a Grafana-managed alert
+Continue to [Generate sample logs](#generate-sample-logs).
-1. Navigate in Grafana to **Alerting**, then to **Alert Rules** and click **+ New alert rule**.
-1. Choose **Grafana Managed Alert** to create an alert that uses expressions.
-1. Select your Loki datasource from the drop-down.
-1. Enter the alert query in the query editor, switch to **code** mode in the top right corner of the editor to paste the query below:
+
- ```
- sum by (message)(count_over_time({filename="/var/log/web_requests.log"} != `status=200` | pattern `<_> duration<_>` [10m]))
+### Grafana OSS users
+
+
+
+In order to run a Grafana stack locally, ensure you have the following applications installed.
+
+- [Docker Compose](https://docs.docker.com/get-docker/) (included in Docker for Desktop for macOS and Windows)
+- [Git](https://git-scm.com/)
+
+
+
+To demonstrate the observation of data using the Grafana stack, download the files to your local machine.
+
+1. Download and save a Docker compose file to run Grafana, Loki and Promtail.
+
+ ```bash
+ wget https://raw.githubusercontent.com/grafana/loki/v2.8.0/production/docker-compose.yaml -O docker-compose.yaml
```
- This query will count the number of log lines with a status code that is not 200 (OK), then sum the result set by message type using an **instant query** and the time interval indicated in brackets. It uses the logql pattern parser to add a new label called `message` that contains the level, method, url, and status from the log line.
+2. Run the Grafana stack.
- You can use the **explain query** toggle button for a full explanation of the query syntax. The optional log-generating script creates a sample log line similar to the one below:
-
- ```
- 2023-04-22T02:49:32.562825+00:00 level=info method=GET url=test.com status=200 duration=171ms
+ ```bash
+ docker compose up -d
```
- {{% admonition type="note" %}}If you're using your own logs, modify the logql query to match your own log message. Refer to the Loki docs to understand the [pattern parser](https://grafana.com/docs/loki/latest/logql/log_queries/#pattern).
- {{% / admonition %}}
+The first time you run `docker compose up -d`, Docker downloads all the necessary resources for the tutorial. This might take a few minutes, depending on your internet connection.
-1. Update the default expressions to match the values shown in the tables below:
+
- **Box B - reduce expression**
+{{< admonition type="note" >}}
- | | |
- | -------- | ------ |
- | Function | Sum |
- | Input | A |
- | Mode | Strict |
+If you already have Grafana, Loki, or Prometheus running on your system, you might see errors, because the Docker image is trying to use ports that your local installations are already using. If this is the case, stop the services, then run the command again.
- **Box C - threshold expression**
- | | |
- | ---------------- | --------------------------|
- | Input | B |
- | Expression value | Is above 5 |
- | Alert condition |This is the alert condition|
+{{< /admonition >}}
-1. Expand **Options** and select **Instant** as the query type.
+
-1. Click **preview** to see a preview of the query result and alert evaluation.
+{{< docs/ignore >}}
-1. Expression B shows a table of labels and values returned. The message label captured the message string from the log line
- and the value shows the number of times that string occurred during the evaluation interval.
+> If you already have Grafana, Loki, or Prometheus running on your system, you might see errors, because the Docker image is trying to use ports that your local installations are already using. If this is the case, stop the services, then run the command again.
- | labels | values |
- | ------------------------------------------------------ | ------ |
- | message=level=info method=GET url=test.com status=500 | 27 |
- | message=level=info method=POST url=test.com status=500 | 1 |
+{{< /docs/ignore >}}
-1. Configure your alert evaluation behavior.
+
- - Choose a folder or use **+add new** to add a new folder for this alert.
- - Select an existing evaluation group from the drop-down or create a new one if this is your first alert.
- - Set the **for** value to **0s** so the alert will fire instantly.
- - Leave Configure no data and error handling No data handling on the default values.
+{{< admonition type="tip" >}}
+Alternatively, you can try out this example in our interactive learning environment: [Get started with Grafana Alerting](https://killercoda.com/grafana-labs/course/grafana/alerting-loki-logs).
-1. Add an annotation that refers to labels and values from the query result in your alert notification.
+It's a fully configured environment with all the dependencies already installed.
- - Choose **+Add new** in the drop down and type the annotation name **AlertValues** into the blank box.
- - In the blank `text` box paste `{{ $labels.message }} has returned an error status {{$values.B}} times.`
+
-1. Click the **Save and exit** button at the top of the alert definition page.
+Provide feedback, report bugs, and raise issues in the [Grafana Killercoda repository](https://github.com/grafana/killercoda).
+{{< /admonition >}}
-### Create a Loki managed alert
+
-[Loki managed alerts](https://grafana.com/docs/loki/latest/rules/#alerting-and-recording-rules) are stored and evaluated by Loki. They use LogQL for their expressions.
+
-1. Choose Mimir or Loki managed alert to create an alert using Loki.
-1. Select your Loki data source from the drop-down.
-1. The optional script will output a sample log line similar to this:
+
- ```
- 2023-04-22T02:49:32.562825+00:00 level=info method=GET url=test.com status=200 duration=171ms
+## Generate sample logs
+
+1. Download and save a Python file that generates logs.
+
+ ```bash
+ wget https://raw.githubusercontent.com/grafana/tutorial-environment/master/app/loki/web-server-logs-simulator.py
```
-1. Enter the alert query below if you’re using the sample logs or modify it for your own file path and condition.
+1. Execute the log-generating Python script.
+ ```bash
+ python3 ./web-server-logs-simulator.py | sudo tee -a /var/log/web_requests.log
```
- sum by (message)(count_over_time({filename="/var/log/web_requests.log"} != `status=200` | pattern `<_> duration<_>` [5m])) > 5
- ```
-
- This query will search the interval period and count the number of log lines with a status code that is not 200 (OK), then sum the result set by message type. It uses the logql pattern parser to add a new label called `message` that captured the level, method, url, and status from the log line.
-
- For loki alerts, the interval needs to be specified in brackets instead of a variable and the alert threshold is added to the query. For this example, the interval is 5m and the alert will fire if there are more than 5 non-200 status messages.
-
-1. Click **preview alert** to see a preview of the labels and value. Hover over the **i** icon under the info column to see the query values.
-
-1. Add an annotation that refers to labels and values from the query result in your alert notification.
-
- - Choose **+Add new** in the drop down and type the annotation name **AlertValues** into the blank box.
- - In the blank `text` box, paste the following:
-
- ```
- {{ $labels.message }} has returned an error status {{$values.B}} times
- ```
-
-1. Click **Save rule and exit** at the top of the alert screen.
-
-## Create a message template
-
-1. **Add an alert message template** and reference the annotation from your alert.
-
- - In Alerting under the Contact points tab:
-
- - Choose **Grafana** to use the built-in alertmanager
- - Click **+Add template**
- - Name the template `mynotification`
- - Add the snippet below to your alert template in the **Content** field. Notice that you will reference the annotation from your alert by name `(.Annotations.AlertValues)` to insert the annotation string into the alert notification:
-
- ```
- {{ define "myalert" }}
- [{{.Status}}] {{ .Labels.alertname }}
- {{ .Annotations.AlertValues }}
- {{ end }}
- {{ define "mymessage" }}
- {{ if gt (len .Alerts.Firing) 0 }}
- {{ len .Alerts.Firing }} firing:
- {{ range .Alerts.Firing }} {{ template "myalert" .}} {{ end }}
- {{ end }}
- {{ if gt (len .Alerts.Resolved) 0 }}
- {{ len .Alerts.Resolved }} resolved:
- {{ range .Alerts.Resolved }} {{ template "myalert" .}} {{ end }}
- {{ end }}
- {{ end }}
- ```
-
- - There are two sections to the notification template:
- 1. The `myalert` template creates a single alert notification based on a specific alert.
- 1. The `mymessage` template will find all of the grouped alerts that are firing and send them in a single notification.
- - Save the template.
-
-1. Add the template to your contact point
-
- 1. Navigate to **Alerts > Contact point** and edit the email contact point. If you're using Grafana Cloud, SMTP is already enabled. Otherwise, for local installations you'll need to [configure SMTP](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#smtp).
- 1. Add an email address in the to field for the recipient.
- 1. Expand Optional Email Settings and refer to the template by adding this to the body field:
-
- ```
- {{ template "mynotification" . }}
- ```
-
-**Tada! You're finished!** Grafana will email an alert with a message that looks similar to the one below. The format varies slightly depending on which type of alert you created - Loki or Grafana managed. The contents should be the same:
-
-```
-1 firing: [firing] LokiAlertTest1 Error message level=info method=GET url=test.com status=500 has occurred 12 times.
-```
-
-## Optional: Use promtail with a sample log-generating script
-
-This optional step uses a python script to generate the sample logs used in this tutorial to create alerts.
-
-1. [Install promtail](https://grafana.com/docs/loki/latest/clients/promtail/installation/) on your local machine and configure it to send logs to your Loki instance.
-1. Install Python3 on your local machine if needed.
-1. Copy the python script below and paste it into a new file on your local machine.
-
-```
-
-#!/bin/env python3
-
-import datetime
-import math
-import random
-import sys
-import time
-
-
-requests_per_second = 2
-failure_rate = 0.05
-get_post_ratio = 0.9
-get_average_duration_ms = 500
-post_average_duration_ms = 2000
-
-
-while True:
-
- # Exponential distribution random value of average 1/lines_per_second.
- d = random.expovariate(requests_per_second)
- time.sleep(d)
- if random.random() < failure_rate:
- status = "500"
- else:
- status = "200"
- if random.random() < get_post_ratio:
- method = "GET"
- duration_ms = math.floor(random.expovariate(1/get_average_duration_ms))
- else:
- method = "POST"
- duration_ms = math.floor(random.expovariate(1/post_average_duration_ms))
- timestamp = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
- print(f"{timestamp} level=info method={method} url=/ status={status} duration={duration_ms}ms")
- sys.stdout.flush()
-
-```
-
-1. Give the script executable permissions.
-
-In a terminal window on linux-based systems run the command:
-
-```
-
-chmod 755 ./web-server-logs-simulator.py
-
-
-```
-
-1. Run the script.
-
-- Use `tee` to direct the script output to the console and the specified file path. For example, if promtail is
- configured to monitor `/var/log` for `.log` files you can direct the script output to `/var/log/web_requests.log` file.
-
-- To avoid running the script with elevated permissions, create the log file manually and change the permissions for the output file only.
-
- ```
- sudo touch /var/log/web_requests.log
- chmod 755 /var/log/web_requests.log
- python3 ./web-server-logs-simulator.py | tee -a /var/log/web_requests.log
- ```
-
-1. Verify that the logs are showing up in Grafana’s Explore view:
-
-- Navigate to explore in Grafana.
-- Select the Loki datasource from the drop-down.
-- Check the toggle for **builder | code** in the top right corner of the query box and switch the query mode to builder if it’s not already selected.
-- Select the filename label from the drop-down and choose your `web_requests.log` file from the value drop-down.
-- Click **Run Query**.
-- You should see logs and a graph of log volume.
### Troubleshooting the script
If you don't see the sample logs in Explore:
-- Does the output file exist, check /var/log/web_requests.log to see if it contains logs.
-- If the file is empty, check that you followed the steps above to create the file and change the permissions.
-- If the file exists, verify that promtail is running and check that it is configured correctly.
+- Does the output file exist, check `/var/log/web_requests.log` to see if it contains logs.
+- If the file is empty, check that you followed the steps above to create the file.
+- If the file exists, verify that promtail container is running.
- In Grafana Explore, check that the time range is only for the last 5 minutes.
-## Optional: Use Docker compose to create the tutorial environment
+
-These optional steps walk you through installing Grafana, Loki and Promtail with Docker compose. You'll also configure a log-generating script
-that generates the sample logs used in this tutorial to create alerts.
+
-### Pre-requisites
+## Create a contact point
-- [Docker Compose](https://docs.docker.com/compose/install/)
-- Python 3
+Besides being an open-source observability tool, Grafana has its own built-in alerting service. This means that you can receive notifications whenever there is an event of interest in your data, and even see these events graphed in your visualizations.
-1. Start a command line from a directory of your choice.
-1. From that directory, get a `docker-compose.yaml` file to run Grafana, Loki, and Promtail:
+In this step, we'll set up a new [contact point](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/). This contact point will use the _webhooks_ integration. In order to make this work, we also need an endpoint for our webhook integration to receive the alert. We will use [Webhook.site](https://webhook.site/) to quickly set up that test endpoint. This way we can make sure that our alert is actually sending a notification somewhere.
-**Bash**
+
+
+1. In your browser, **sign in** to your Grafana Cloud account.
+
+ OSS users: To log in, navigate to [http://localhost:3000](http://localhost:3000), where Grafana is running.
+
+1. In another tab, go to [Webhook.site](https://webhook.site/).
+1. Copy Your unique URL.
+
+
+{{< docs/ignore >}}
+
+1. Navigate to [http://localhost:3000](http://localhost:3000), where Grafana is running.
+1. In another tab, go to [Webhook.site](https://webhook.site/).
+1. Copy Your unique URL.
+ {{< /docs/ignore >}}
+
+Your webhook endpoint is now waiting for the first request.
+
+Next, let's configure a contact point in Grafana's Alerting UI to send notifications to our webhook endpoint.
+
+1. Return to Grafana. In Grafana's sidebar, hover over the **Alerting** (bell) icon and then click **Contact points**.
+1. Click **+ Add contact point**.
+1. In **Name**, write **Webhook**.
+1. In **Integration**, choose **Webhook**.
+1. In **URL**, paste the endpoint to your webhook endpoint.
+1. Click **Test**, and then click **Send test notification** to send a test alert to your webhook endpoint.
+1. Navigate back to [Webhook.site](https://webhook.site/). On the left side, there's now a `POST /` entry. Click it to see what information Grafana sent.
+
+ {{< figure src="/media/docs/alerting/alerting-webhook-detail.png" max-width="1200px" caption="A POST entry in Webhook.site" >}}
+
+1. Return to Grafana and click **Save contact point**.
+
+We have created a dummy Webhook endpoint and created a new Alerting contact point in Grafana. Now, we can create an alert rule and link it to this new integration.
+
+
+
+
+
+## Create an alert rule
+
+Next, we'll establish an [alert rule](http://grafana.com/docs/grafana/next/alerting/fundamentals/alert-rule-evaluation/) within Grafana Alerting to notify us whenever alert rules are triggered and resolved.
+
+1. In Grafana, **navigate to Alerting** > **Alert rules**.
+1. Click on **New alert rule**.
+1. Enter alert rule name for your alert rule. Make it short and descriptive as this will appear in your alert notification. For instance, **web-requests-logs**
+
+### Define query and alert condition
+
+In this section, we define queries, expressions (used to manipulate the data), and the condition that must be met for the alert to be triggered.
+
+1. Select the **Loki** datasource from the drop-down.
+2. In the Query editor, switch to Code mode by clicking the button on the right.
+3. Paste the query below.
+
+ ```
+ sum by (message)(count_over_time({filename="/var/log/web_requests.log"} != "status=200" | pattern "<_> duration<_>" [10m]))
+ ```
+
+This query will count the number of log lines with a status code that is not 200 (OK), then sum the result set by message type using an **instant query** and the time interval indicated in brackets. It uses the LogQL pattern parser to add a new label called `message` that contains the level, method, url, and status from the log line.
+
+You can use the **explain query** toggle button for a full explanation of the query syntax. The optional log-generating script creates a sample log line similar to the one below:
```
-
-wget https://raw.githubusercontent.com/grafana/loki/v2.8.0/production/docker-compose.yaml -O docker-compose.yaml
-
+2023-04-22T02:49:32.562825+00:00 level=info method=GET url=test.com status=200 duration=171ms
```
-**Windows Powershell**
+
-```
+{{% admonition type="note" %}}
-$client = new-object System.Net.WebClient
-$client.DownloadFile("https://raw.githubusercontent.com/grafana/loki/v2.8.0/production/docker-compose.yaml",
-"C:\Users\$Env:UserName\Desktop\docker-compose.yaml")
-#downloads the file to the Desktop
+If you're using your own logs, modify the LogQL query to match your own log message. Refer to the Loki docs to understand the [pattern parser](https://grafana.com/docs/loki/latest/logql/log_queries/#pattern).
-```
+{{% / admonition %}}
-1. Run the container
+
-```
+{{< docs/ignore >}}
-docker compose up -d
+If you're using your own logs, modify the LogQL query to match your own log message. Refer to the Loki docs to understand the [pattern parser](https://grafana.com/docs/loki/latest/logql/log_queries/#pattern).
-```
+{{< /docs/ignore >}}
-1. Create and edit a python file that will generate logs.
+4. Remove the ‘B’ **Reduce expression** (click the bin icon). The Reduce expression comes by default, and in this case, it is not needed since the queried data is already reduced. Note that the Threshold expression is now your **Alert condition**.
-**Bash**
+5. In the ‘C’ **Threshold expression**:
-```
+ - Change the **Input** to **'A'** to select the data source.
+ - Enter `0` as the threshold value. This is the value above which the alert rule should trigger.
-touch web-server-logs-simulator.py && nano web-server-logs-simulator.py
+6. Click **Preview** to run the queries.
-```
+ It should return alert instances from log lines with a status code that is not 200 (OK), and that has met the alert condition. The condition for the alert rule to fire is any occurrence that goes over the threshold of `0`. Since the Loki query has returned more than zero alert instances, the alert rule is `Firing`.
-**Windows Powershell**
+ {{< figure src="/media/docs/alerting/expression-loki-alert.png" max-width="1200px" caption="Preview of a firing alert instances" >}}
-```
+### Set evaluation behavior
-New-Item web-server-logs-simulator.py ; notepad web-server-logs-simulator.py
+An [evaluation group](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/) defines when an alert rule fires, and it’s based on two settings:
-```
+- **Evaluation group**: how frequently the alert rule is evaluated.
+- **Evaluation interval**: how long the condition must be met to start firing. This allows your data time to stabilize before triggering an alert, helping to reduce the frequency of unnecessary notifications.
-1. Paste the following code into the file
+To set up the evaluation:
-```
+1. In **Folder**, click **+ New folder** and enter a name. For example: _web-server-alerts_. This folder will contain our alerts.
+1. In the **Evaluation group**, repeat the above step to create a new evaluation group. We will name it _1m-evaluation_.
+1. Choose an **Evaluation interval** (how often the alert will be evaluated).
+ For example, every `1m` (1 minute).
+1. Set the pending period to, `0s` (zero seconds), so the alert rule fires the moment the condition is met.
-#!/bin/env python3
+### Configure labels and notifications
-import datetime
-import math
-import random
-import sys
-import time
+Choose the contact point where you want to receive your alert notifications.
+1. Under **Contact point**, select **Webhook** from the drop-down menu.
+1. Click **Save rule and exit** at the top right corner.
+
-requests_per_second = 2
-failure_rate = 0.05
-get_post_ratio = 0.9
-get_average_duration_ms = 500
-post_average_duration_ms = 2000
+
+## Trigger the alert rule
-while True:
+Since the Python script will continue to generate log data that matches the alert rule condition, once the evaluation interval has concluded, you should receive an alert notification in the Webhook endpoint.
- # Exponential distribution random value of average 1/lines_per_second.
- d = random.expovariate(requests_per_second)
- time.sleep(d)
- if random.random() < failure_rate:
- status = "500"
- else:
- status = "200"
- if random.random() < get_post_ratio:
- method = "GET"
- duration_ms = math.floor(random.expovariate(1/get_average_duration_ms))
- else:
- method = "POST"
- duration_ms = math.floor(random.expovariate(1/post_average_duration_ms))
- timestamp = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
- print(f"{timestamp} level=info method={method} url=/ status={status} duration={duration_ms}ms")
- sys.stdout.flush()
+{{< figure src="/media/docs/alerting/alerting-webhook-firing-alert.png" max-width="1200px" caption="Firing alert notification details" >}}
-```
+
-1. Execute the log-generating python script.
+
-In a terminal window on linux-based systems run the command:
+
-```
+{{< admonition type="tip" >}}
+Check out our [advanced alerting tutorial](https://grafana.com/tutorials/alerting-get-started-pt2/) to explore advanced topics such as alert instances and notification routing.
+{{< /admonition >}}
-chmod 755 ./web-server-logs-simulator.py
+
-```
+{{< docs/ignore >}}
-- Use `tee` to direct the script output to the console and the specified file path. For example, if promtail is
- configured to monitor `/var/log` for `.log` files you can direct the script output to `/var/log/web_requests.log` file.
+> Check out our [advanced alerting tutorial](https://grafana.com/tutorials/alerting-get-started-pt2/) to explore advanced topics such as alert instances and notification routing.
-- To avoid running the script with elevated permissions, create the log file manually and change the permissions for the output file only.
+{{< /docs/ignore >}}
-```
-
-sudo touch /var/log/web_requests.log
-chmod 755 /var/log/web_requests.log
-python3 ./web-server-logs-simulator.py | tee -a /var/log/web_requests.log
-
-
-```
-
-**Running on Windows**
-
-Run Powershell as administrator
-
-```
-
-python ./web-server-logs-simulator.py | Tee-Object "C:\ProgramFiles\GrafanaLabs\grafana\var\log\web_requests.log"
-
-```
-
-1. Verify that the logs are showing up in Grafana’s Explore view:
-
-- Navigate to explore in Grafana.
-- Select the Loki datasource from the drop-down.
-- Check the toggle for **builder | code** in the top right corner of the query box and switch the query mode to builder if it’s not already selected.
-- Select the filename label from the drop-down and choose your `web_requests.log` file from the value drop-down.
-- Click **Run Query**.
-- You should see logs and a graph of log volume.
-
-### Troubleshooting the script
-
-If you don't see the logs in Explore, check these things:
-
-- Does the output file exist, check /var/log/web_requests.log to see if it contains logs.
-- If the file is empty, check that you followed the steps above to create the file and change the permissions.
-- If the file exists, verify that promtail is running and check that it is configured correctly.
-- In Grafana Explore, check that the time range is only for the last 5 minutes.
+
diff --git a/docs/sources/tutorials/grafana-fundamentals/index.md b/docs/sources/tutorials/grafana-fundamentals/index.md
index f92da6381bb..5262044de5c 100644
--- a/docs/sources/tutorials/grafana-fundamentals/index.md
+++ b/docs/sources/tutorials/grafana-fundamentals/index.md
@@ -16,8 +16,15 @@ tags:
- beginner
title: Grafana fundamentals
weight: 10
+killercoda:
+ title: Grafana fundamentals
+ description: Learn how to use Grafana to set up a monitoring solution for your application. You will explore metrics and logs, build and annotate dashboards, and set up alert rules.
+ backend:
+ imageid: ubuntu
---
+
+
## Introduction
In this tutorial, you'll learn how to use Grafana to set up a monitoring solution for your application, and:
@@ -25,7 +32,9 @@ In this tutorial, you'll learn how to use Grafana to set up a monitoring solutio
- Explore metrics and logs
- Build dashboards
- Annotate dashboards
-- Set up alerts
+- Set up alert rules
+
+
Alternatively, you can also watch our Grafana for Beginners series where we discuss fundamental concepts to help you get started with Grafana.
@@ -42,12 +51,24 @@ Alternatively, you can also watch our Grafana for Beginners series where we disc
- [Docker Compose](https://docs.docker.com/compose/) (included in Docker for Desktop for macOS and Windows)
- [Git](https://git-scm.com/)
-### KillerCoda sandbox environment (Alternative)
-
-If you would prefer to follow along with this tutorial without needing to set up a local environment, you can use the [KillerCoda sandbox environment](https://killercoda.com/grafana-labs/course/full-stack/tutorial-enviroment).
-
{{% /class %}}
+{{< admonition type="tip" >}}
+Alternatively, you can try out this example in our interactive learning environment: [Grafana Fundamentals](https://killercoda.com/grafana-labs/course/grafana/grafana-fundamentals).
+
+It's a fully configured environment with all the dependencies already installed.
+
+
+
+Provide feedback, report bugs, and raise issues in the [Grafana Killercoda repository](https://github.com/grafana/killercoda).
+{{< /admonition >}}
+
+
+
+
+
+
+
## Set up the sample application
This tutorial uses a sample application to demonstrate some of the features in Grafana. To complete the exercises in this tutorial, you need to download the files to your local machine.
@@ -58,19 +79,19 @@ In this step, you'll set up the sample application, as well as supporting servic
1. Clone the [github.com/grafana/tutorial-environment](https://github.com/grafana/tutorial-environment) repository.
- ```
+ ```bash
git clone https://github.com/grafana/tutorial-environment.git
```
1. Change to the directory where you cloned this repository:
- ```
+ ```bash
cd tutorial-environment
```
1. Make sure Docker is running:
- ```
+ ```bash
docker ps
```
@@ -78,7 +99,7 @@ In this step, you'll set up the sample application, as well as supporting servic
1. Start the sample application:
- ```
+ ```bash
docker-compose up -d
```
@@ -88,13 +109,13 @@ In this step, you'll set up the sample application, as well as supporting servic
1. Ensure all services are up-and-running:
- ```
+ ```bash
docker-compose ps
```
In the **State** column, it should say `Up` for all services.
-1. Browse to the sample application on [localhost:8081](http://localhost:8081).
+1. Browse to the sample application on [http://localhost:8081](http://localhost:8081).
### Grafana News
@@ -110,24 +131,49 @@ To add a link:
To vote for a link, click the triangle icon next to the name of the link.
+
+
+
+
## Open Grafana
-Grafana is an open-source platform for monitoring and observability that lets you visualize and explore the state of your systems.
+Grafana is an open source platform for monitoring and observability that lets you visualize and explore the state of your systems.
+
+
1. Open a new tab.
-1. Browse to [localhost:3000](http://localhost:3000).
+1. Browse to [http://localhost:3000](http://localhost:3000).
+
+
+{{< docs/ignore >}}
+
+1. Browse to [http://localhost:3000](http://localhost:3000).
+ {{< /docs/ignore >}}
+
+
{{< admonition type="note" >}}
This demo does not require a login page or credentials. However, if you choose to install Grafana locally, you will need to log in and provide credentials. In that case, the default username and password is `admin`.
{{< /admonition >}}
+
+
+{{< docs/ignore >}}
+
+> This demo does not require a login page or credentials. However, if you choose to install Grafana locally, you will need to log in and provide credentials. In that case, the default username and password is `admin`.
+> {{< /docs/ignore >}}
+
The first thing you see is the Home dashboard, which helps you get started.
In the top left corner, you can see the menu icon. Clicking it will open the _sidebar_, the main menu for navigating Grafana.
+
+
+
+
## Explore your metrics
-Grafana Explore is a workflow for troubleshooting and data exploration. In this step, you'll be using Explore to create ad-hoc queries to understand the metrics exposed by the sample application.
+Grafana Explore is a workflow for troubleshooting and data exploration. In this step, you'll be using Explore to create ad-hoc queries to understand the metrics exposed by the sample application. Specifically, you'll explore requests received by the sample application.
> Ad-hoc queries are queries that are made interactively, with the purpose of exploring data. An ad-hoc query is commonly followed by another, more specific query.
@@ -163,6 +209,10 @@ Grafana Explore is a workflow for troubleshooting and data exploration. In this
Depending on your use case, you might want to group on other labels. Try grouping by other labels, such as `status_code`, by changing the `by(route)` part of the query to `by(status_code)`.
+
+
+
+
## Add a logging data source
Grafana supports log data sources, like [Loki](/oss/loki/). Just like for metrics, you first need to add your data source to Grafana.
@@ -170,11 +220,15 @@ Grafana supports log data sources, like [Loki](/oss/loki/). Just like for metric
1. Click the menu icon and, in the sidebar, click **Connections** and then **Data sources**.
1. Click **+ Add new data source**.
1. In the list of data sources, click **Loki**.
-1. In the URL box, enter [http://loki:3100](http://loki:3100).
+1. In the URL box, enter `http://loki:3100`
1. Scroll to the bottom of the page and click **Save & Test** to save your changes.
You should see the message "Data source successfully connected." Loki is now available as a data source in Grafana.
+
+
+
+
## Explore your logs
Grafana Explore not only lets you make ad-hoc queries for metrics, but lets you explore your logs as well.
@@ -203,12 +257,16 @@ Let's generate an error, and analyze it with Explore.
{filename="/var/log/tns-app.log"} |= "error"
```
-1. Click on the log line that says `level=error msg="empty url"` to see more information about the error.
+1. Click the log line that says `level=error msg="empty url"` to see more information about the error.
> **Note:** If you're in Live mode, clicking logs will not show more information about the error. Instead, stop and exit the live stream, then click the log line there.
Logs are helpful for understanding what went wrong. Later in this tutorial, you'll see how you can correlate logs with metrics from Prometheus to better understand the context of the error.
+
+
+
+
## Build a dashboard
A _dashboard_ gives you an at-a-glance view of your data and lets you track metrics through different visualizations.
@@ -236,6 +294,10 @@ Every panel consists of a _query_ and a _visualization_. The query defines _what
{{< figure src="/media/tutorials/grafana-fundamentals-dashboard.png" alt="A panel in a Grafana dashboard" caption="A panel in a Grafana dashboard" >}}
+
+
+
+
## Annotate events
When things go bad, it often helps if you understand the context in which the failure occurred. Time of last deploy, system changes, or database migration can offer insight into what might have caused an outage. Annotations allow you to represent such events directly on your graphs.
@@ -273,9 +335,9 @@ Manually annotating your dashboard is fine for those single events. For regularl
```
1. Click **Apply**. Grafana displays the Annotations list, with your new annotation.
-1. Click on your dashboard name to return to your dashboard.
+1. Click your dashboard name to return to your dashboard.
1. At the top of your dashboard, there is now a toggle to display the results of the newly created annotation query. Press it if it's not already enabled.
-1. Click the **Save dashboard** icon to save the changes.
+1. Click the **Save dashboard** (disk) icon to save the changes.
1. To test the changes, go back to the [sample application](http://localhost:8081), post a new link without a URL to generate an error in your browser that says `empty url`.
The log lines returned by your query are now displayed as annotations in the graph.
@@ -284,17 +346,33 @@ The log lines returned by your query are now displayed as annotations in the gra
Being able to combine data from multiple data sources in one graph allows you to correlate information from both Prometheus and Loki.
-Annotations also work very well alongside alerts. In the next and final section, we will set up an alert for our app `grafana.news` and then we will trigger it. This will provide a quick intro to our new Alerting platform.
+Annotations also work very well alongside alert rules. In the next and final section, we will set up an alert rules for our app `grafana.news` and then we will trigger it. This will provide a quick intro to our new Alerting platform.
-## Create a Grafana Managed Alert
+
-Alerts allow you to identify problems in your system moments after they occur. By quickly identifying unintended changes in your system, you can minimize disruptions to your services.
+
-Grafana's new alerting platform debuted with Grafana 8. A year later, with Grafana 9, it became the default alerting method. In this step we will create a Grafana Managed Alert. Then we will trigger our new alert and send a test message to a dummy endpoint.
+## Create a Grafana-managed alert rule
-The most basic alert consists of two parts:
+Alert rules allow you to identify problems in your system moments after they occur. By quickly identifying unintended changes in your system, you can minimize disruptions to your services.
-1. A _Contact point_ - A Contact point defines how Grafana delivers an alert. When the conditions of an _alert rule_ are met, Grafana notifies the contact points, or channels, configured for that alert.
+Grafana's new alerting platform debuted with Grafana 8. A year later, with Grafana 9, it became the default alerting method. In this step we will create a Grafana-managed alert rule. Then we will trigger our new alert rule and send a test message to a dummy endpoint.
+
+The most basic alert rule consists of two parts:
+
+1. A _Contact point_ - A Contact point defines how Grafana delivers an alert instance. When the conditions of an _alert rule_ are met, Grafana notifies the contact points, or channels, configured for that alert rule.
+
+
+
+ {{< admonition type="note" >}}
+ An [alert instance](https://grafana.com/docs/grafana/latest/alerting/fundamentals/#alert-instances) is a specific occurrence that matches a condition defined by an alert rule, such as when the rate of requests for a specific route suddenly increases.
+ {{< /admonition >}}
+
+
+ {{< docs/ignore >}}
+
+ > An [alert instance](https://grafana.com/docs/grafana/latest/alerting/fundamentals/#alert-instances) is a specific occurrence that matches a condition defined by an alert rule, such as when the rate of requests for a specific route suddenly increases.
+ > {{< /docs/ignore >}}
Some popular channels include:
@@ -304,13 +382,17 @@ The most basic alert consists of two parts:
- [Slack](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/configure-slack/)
- [PagerDuty](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/pager-duty/)
-1. An _Alert rule_ - An Alert rule defines one or more _conditions_ that Grafana regularly evaluates. When these evaluations meet the rule's criteria, the alert is triggered.
+1. An _Alert rule_ - An Alert rule defines one or more _conditions_ that Grafana regularly evaluates. When these evaluations meet the rule's criteria, the alert rule is triggered.
To begin, let's set up a webhook contact point. Once we have a usable endpoint, we'll write an alert rule and trigger a notification.
-### Create a contact point for Grafana Managed Alerts
+
-In this step, we'll set up a new contact point. This contact point will use the _webhooks_ channel. In order to make this work, we also need an endpoint for our webhook channel to receive the alert. We will use [requestbin.com](https://requestbin.com) to quickly set up that test endpoint. This way we can make sure that our alert is actually sending a notification somewhere.
+
+
+### Create a contact point for Grafana-managed alert rules
+
+In this step, we'll set up a new contact point. This contact point will use the _webhooks_ channel. In order to make this work, we also need an endpoint for our webhook channel to receive the alert notification. We will use [requestbin.com](https://requestbin.com) to quickly set up that test endpoint. This way we can make sure that our alert manager is actually sending a notification somewhere.
1. Browse to [requestbin.com](https://requestbin.com).
1. Under the **Create Request Bin** button, click the link to create a **public bin** instead.
@@ -320,26 +402,30 @@ Your Request Bin is now waiting for the first request.
Next, let's configure a Contact Point in Grafana's Alerting UI to send notifications to our Request Bin.
-1. Return to Grafana. In Grafana's sidebar, hover over the **Alerting** (bell) icon and then click **Contact points**.
+1. Return to Grafana. In Grafana's sidebar, hover over the **Alerting** (bell) icon and then click **Manage Contact points**.
1. Click **+ Add contact point**.
1. In **Name**, write **RequestBin**.
1. In **Integration**, choose **Webhook**.
1. In **URL**, paste the endpoint to your request bin.
-1. Click **Test**, and then click **Send test notification** to send a test alert to your request bin.
+1. Click **Test**, and then click **Send test notification** to send a test alert notification to your request bin.
1. Navigate back to the Request Bin you created earlier. On the left side, there's now a `POST /` entry. Click it to see what information Grafana sent.
1. Return to Grafana and click **Save contact point**.
We have now created a dummy webhook endpoint and created a new Alerting Contact Point in Grafana. Now we can create an alert rule and link it to this new channel.
-### Add an Alert Rule to Grafana
+
+
+
+
+### Add an alert rule to Grafana
Now that Grafana knows how to notify us, it's time to set up an alert rule:
1. In Grafana's sidebar, hover over the **Alerting** (bell) icon and then click **Alert rules**.
1. Click **+ New alert rule**.
1. For **Section 1**, name the rule `fundamentals-test`.
-1. For **Section 2**, Find the **query A** box. Choose your Prometheus datasource. Note that the rule type should automatically switch to Grafana-managed alert.
+1. For **Section 2**, Find the **query A** box. Choose your Prometheus data source. Note that the rule type should automatically switch to Grafana-managed alert rule.
1. Switch to code mode by checking the Builder/Code toggle.
1. Enter the same Prometheus query that we used in our earlier panel:
@@ -347,47 +433,60 @@ Now that Grafana knows how to notify us, it's time to set up an alert rule:
sum(rate(tns_request_duration_seconds_count[5m])) by(route)
```
-1. Press **Preview**. You should see some data returned.
-1. Keep expressions “B” and "C" as they are. These expressions (Reduce and Threshold, respectively) come by default when creating a new rule. Expression "B", selects the last value of our query “A”, while the Threshold expression "C" will check if the last value from expression "B" is above a specific value. In addition, the Threshold expression is the alert condition by default. Enter `0.2` as threshold value. [You can read more about queries and conditions here](/docs/grafana/latest/alerting/fundamentals/alert-rules/queries-conditions/#expression-queries).
-1. In **Section 3**, in Folder, create a new folder, by clicking `New folder` and typing a name for the folder. This folder will contain our alerts. For example: `fundamentals`. Then, click `create`.
+1. Scroll down to bottom of section #2 and click the **Preview** button. You should see some data returned.
+1. Keep expressions “B” and "C" as they are. These expressions (Reduce and Threshold, respectively) are included by default when creating a new rule. Expression "B", selects the last value of our query “A”, while the Threshold expression "C" will check if the last value from expression "B" is above a specific value. In addition, the Threshold expression is the alert rule condition by default. Enter `0.2` as threshold value. [You can read more about queries and conditions here](/docs/grafana/latest/alerting/fundamentals/alert-rules/queries-conditions/#expression-queries).
+1. In **Section 3**, in Folder, create a new folder, by clicking `New folder` and typing a name for the folder. This folder will contain our alert rules. For example: `fundamentals`. Then, click `create`.
1. In the Evaluation group, repeat the above step to create a new one. We will name it `fundamentals` too.
-1. Choose an Evaluation interval (how often the alert will be evaluated). For example, every `10s` (10 seconds).
-1. Set the pending period. This is the time that a condition has to be met until the alert enters in Firing state and a notification is sent. Enter `0s`. For the purposes of this tutorial, the evaluation interval is intentionally short. This makes it easier to test. This setting makes Grafana wait until an alert has fired for a given time before Grafana sends the notification.
+1. Choose an Evaluation interval (how often the alert rule will be evaluated). For example, every `10s` (10 seconds).
+1. Set the pending period. This is the time that a condition has to be met until the alert instance enters in Firing state and a notification is sent. Enter `0s`. For the purposes of this tutorial, the evaluation interval is intentionally short. This makes it easier to test. This setting makes Grafana wait until an alert instance has fired for a given time before Grafana sends the notification.
1. In **Section 4**, choose **RequestBin** as the **Contact point**.
1. Click **Save rule and exit** at the top of the page.
-### Trigger a Grafana Managed Alert
+### Trigger a Grafana-managed alert rule
-We have now configured an alert rule and a contact point. Now let's see if we can trigger a Grafana Managed Alert by generating some traffic on our sample application.
+We have now configured an alert rule and a contact point. Now let's see if we can trigger a Grafana-managed alert rule by generating some traffic on our sample application.
1. Browse to [localhost:8081](http://localhost:8081).
1. Add a new title and URL, repeatedly click the vote button, or refresh the page to generate a traffic spike.
-Once the query `sum(rate(tns_request_duration_seconds_count[5m])) by(route)` returns a value greater than `0.2` Grafana will trigger our alert. Browse to the Request Bin we created earlier and find the sent Grafana alert notification with details and metadata.
+Once the query `sum(rate(tns_request_duration_seconds_count[5m])) by(route)` returns a value greater than `0.2` Grafana will trigger our alert rule. Browse to the Request Bin we created earlier and find the sent Grafana alert notification with details and metadata.
+
+
{{< admonition type="note" >}}
-The alert may be triggered by the `/metrics` endpoint which is frequently accessed by Grafana when pulling metrics from the application. If this happens, you can increase the **Threshold** value in **Section 2** for testing purposes.
+The alert rule may be triggered by the `/metrics` endpoint which is frequently accessed by Grafana when pulling metrics from the application. If this happens, you can increase the **Threshold** value in **Section 2** for testing purposes.
{{< /admonition >}}
-### Display Grafana Alerts to your dashboard
+
-In most cases, it's also valuable to display Grafana Alerts as annotations to your dashboard. Check out the video tutorial below to learn how to display alerting to your dashboard.
+{{< docs/ignore >}}
+
+> The alert rule may be triggered by the `/metrics` endpoint which is frequently accessed by Grafana when pulling metrics from the application. If this happens, you can increase the **Threshold** value in **Section 2** for testing purposes.
+> {{< /docs/ignore >}}
+
+### Display Grafana-managed alert rules to your dashboard
+
+In most cases, it's also valuable to display Grafana alert instances as annotations to your dashboard. Check out the video tutorial below to learn how to display alerting to your dashboard.
+
+
{{< youtube id="ClLp-iSoaSY" >}}
+
+
Let's see how we can configure this.
1. In Grafana's sidebar, hover over the **Alerting** (bell) icon and then click **Alert rules**.
1. Expand the `fundamentals > fundamentals` folder to view our created alert rule.
1. Click the **Edit** icon and scroll down to **Section 5**.
-1. Click the **Link dashboard and panel** button and select the dashboard and panel to which you want the alert to be added as an annotation.
+1. Click the **Link dashboard and panel** button and select the dashboard and panel to which you want the alert instance to be added as an annotation.
1. Click **Confirm** and **Save rule and exit** to save all the changes.
1. In Grafana's sidebar, navigate to the dashboard by clicking **Dashboards** and selecting the dashboard you created.
-1. To test the changes, follow the steps listed to [trigger a Grafana Managed Alert](#trigger-a-grafana-managed-alert).
+1. To test the changes, follow the steps listed to [trigger a Grafana-managed alert rule](#trigger-a-grafana-managed-alert).
- You should now see a red, broken heart icon beside the panel name, signifying that the alert has been triggered. An annotation for the alert, represented as a vertical red line, is also displayed.
+ You should now see a red, broken heart icon beside the panel name, indicating that the alert rule has been triggered. An annotation for the alert instance, represented as a vertical red line, is also displayed.
- {{< figure src="/media/tutorials/grafana-alert-on-dashboard.png" alt="A panel in a Grafana dashboard with alerting and annotations configured" caption="Displaying Grafana Alerts on a dashboard" >}}
+ {{< figure src="/media/tutorials/grafana-alert-on-dashboard.png" alt="A panel in a Grafana dashboard with alerting and annotations configured" caption="Displaying Grafana-managed alert rules on a dashboard" >}}
@@ -405,17 +504,27 @@ Check out our [advanced alerting tutorial](http://grafana.com/tutorials/alerting
{{< /docs/ignore >}}
+
+
+
+
## Summary
+
+
In this tutorial you learned about fundamental features of Grafana. To do so, we ran several Docker containers on your local machine. When you are ready to clean up this local tutorial environment, run the following command:
```
docker-compose down -v
```
-### KillerCoda sandbox environment (completed tutorial)
+
-Do you want to see the finished result? Check out our [completed KillerCoda sandbox environment](https://killercoda.com/grafana-labs/course/full-stack/tutorial-enviroment-completed) containing the entire demo with dashboards, checks, and data sources configured.
+{{< docs/ignore >}}
+
+In this tutorial you learned about fundamental features of Grafana.
+
+{{< /docs/ignore >}}
### Learn more
@@ -427,3 +536,5 @@ Check out the links below to continue your learning journey with Grafana's LGTM
- [Alerting Overview](/docs/grafana/latest/alerting/)
- [Alert rules](/docs/grafana/latest/alerting/create-alerts/)
- [Contact points](/docs/grafana/latest/alerting/fundamentals/contact-points/)
+
+
diff --git a/docs/sources/upgrade-guide/upgrade-v11.2/index.md b/docs/sources/upgrade-guide/upgrade-v11.2/index.md
index cedf970bfb3..d8e161897dc 100644
--- a/docs/sources/upgrade-guide/upgrade-v11.2/index.md
+++ b/docs/sources/upgrade-guide/upgrade-v11.2/index.md
@@ -19,51 +19,55 @@ weight: 1000
{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}}
-## Ensure that your data source UIDs are following the correct standard
+## Technical notes
-We have had a standard ways to define UIDs for Grafana objects for years (at least [since Grafana 5](https://github.com/grafana/grafana/issues/7883)). While all of our internal code is complying to this format, we did not yet had strict enforcement of this format in REST APIs and provisioning paths that allow creation and update of data sources.
+### Grafana data source UID format enforcement
-In Grafana `11.1` we [introduced](https://github.com/grafana/grafana/pull/86598) a warning that is sent to Grafana server logs every time a data source instance is being created or updated using an invalid UID format.
+**Ensure that your data source UIDs follow the correct standard**
-In Grafana `11.2` we [added](https://github.com/grafana/grafana/pull/89363/files) a new feature flag called `failWrongDSUID` that is turned off by default. When enabled, the REST APIs and provisioning will start rejecting and requests to create or update datasource instances that have a wrong UID.
+We've had a standard ways to define UIDs for Grafana objects for years (at least [since Grafana v5](https://github.com/grafana/grafana/issues/7883)). While all of our internal code complies to this format, we didn't yet have strict enforcement of this format in REST APIs and provisioning paths that allow the creation and update of data sources.
-In Grafana `11.5`, we are going to turn feature flag `failWrongDSUID` on by default, but there will still be an option to turn it off.
+In Grafana v11.1, we [introduced](https://github.com/grafana/grafana/pull/86598) a warning that is sent to Grafana server logs every time a data source instance is being created or updated using an invalid UID format.
-In Grafana `12`, this will be the default behavior and will not be configurable.
+In Grafana v11.2, we [added](https://github.com/grafana/grafana/pull/89363/files) a new feature flag called `failWrongDSUID` that is turned off by default. When enabled, the REST APIs and provisioning start rejecting any requests to create or update data source instances that have an incorrect UID.
-### Correct UID format
+In Grafana v11.5, we're going to turn feature flag `failWrongDSUID` on by default, but there will still be an option to turn it off.
-You can find the exact regex definition [here](https://github.com/grafana/grafana/blob/c92f5169d1c83508beb777f71a93336179fe426e/pkg/util/shortid_generator.go#L32-L45).
+In Grafana v12, this will be the default behavior and will not be configurable.
+
+#### Correct UID format
+
+You can find the exact regex definition [in the grafana repository](https://github.com/grafana/grafana/blob/c92f5169d1c83508beb777f71a93336179fe426e/pkg/util/shortid_generator.go#L32-L45).
A data source UID can only contain:
-- latin characters (`a-Z`)
-- numbers (`0-9`)
-- dash symbols (`-`)
+- Latin characters (`a-Z`)
+- Numbers (`0-9`)
+- Dash symbols (`-`)
-### How can I know if I am affected?
+#### How do I know if I'm affected?
-- You can fetch all your data sources via `/api/datasources` API ([docs](https://grafana.com/docs/grafana/latest/developers/http_api/data_source/#get-all-data-sources)). Look into `uid` fields comparing it to the correct format. Below you'll find a script that could help, but please note it is missing authentication that you would [have to add yourself](https://grafana.com/docs/grafana/latest/developers/http_api/#authenticating-api-requests).
+- You can fetch all your data sources using the `/api/datasources` API. Review the `uid` fields, comparing them to the correct format, as shown [in the docs](https://grafana.com/docs/grafana/latest/developers/http_api/data_source/#get-all-data-sources). Following is a script that can help, but note that it's missing authentication that you [have to add yourself](https://grafana.com/docs/grafana/latest/developers/http_api/#authenticating-api-requests):
```
curl http://localhost:3000/api/datasources | jq '.[] | select((.uid | test("^[a-zA-Z0-9\\-_]+$") | not) or (.uid | length > 40)) | {id, uid, name, type}'
```
-- Alternatively, you can check the server logs for the `Invalid datasource uid` ([reference](https://github.com/grafana/grafana/blob/68751ed3107c4d15d33f34b15183ee276611785c/pkg/services/datasources/service/store.go#L429))
+- Alternatively, you can check the server logs for the `Invalid datasource uid` [error](https://github.com/grafana/grafana/blob/68751ed3107c4d15d33f34b15183ee276611785c/pkg/services/datasources/service/store.go#L429).
-### What can I do if I am affected?
+#### What do I do if I'm affected?
-You will need to create a new data source with the correct UID and update your dashboards and alert rules to use it.
+You'll need to create a new data source with the correct UID and update your dashboards and alert rules to use it.
-### How can I update my dashboards to use the new or updated data source?
+#### How do I update my dashboards to use the new or updated data source?
-1. Go to the dashboard using this data source and update it by selecting the new or updated data source from the picker below your panel.
-2. Update the dashboard's JSON model directly via search and replace. Navigate to [dashboard json model](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/view-dashboard-json-model/) and carefully replace all the instances of old `uid` with the newly created `uid`.
+1. Go to the dashboard using the data source and update it by selecting the new or updated data source from the picker below your panel.
+1. Update the dashboard's JSON model directly using search and replace.
-{{< figure src="/media/docs/grafana/screenshot-grafana-11-datasource-uid-enforcement.png" alt="Updating JSON Model of a Dashboard">}}
+ Navigate to [dashboard json model](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/view-dashboard-json-model/) and carefully replace all the instances of old `uid` with the newly created `uid`.
-### How can I update my alert rules to use the new or updated data source?
+ {{< figure src="/media/docs/grafana/screenshot-grafana-11-datasource-uid-enforcement.png" alt="Updating JSON Model of a Dashboard">}}
-Open the alert rule you want to adjust and search for the data source that is being used for the query/alert condition. From there, select the new data source from the dropdown menu and save the alert rule.
+#### How do I update my alert rules to use the new or updated data source?
-## Technical notes
+Open the alert rule you want to adjust and search for the data source that is being used for the query/alert condition. From there, select the new data source from the drop-down list and save the alert rule.
diff --git a/docs/sources/upgrade-guide/upgrade-v9.5/index.md b/docs/sources/upgrade-guide/upgrade-v9.5/index.md
index 7203aa87541..d177ca70693 100644
--- a/docs/sources/upgrade-guide/upgrade-v9.5/index.md
+++ b/docs/sources/upgrade-guide/upgrade-v9.5/index.md
@@ -36,4 +36,4 @@ We are deprecating the dashboard previews feature and will remove it in Grafana
### Migrate your API keys to service accounts
-We are upgrading Grafana [API keys]({{< relref "../../administration/api-keys" >}}) to service accounts. Service accounts are a superset of API keys that include token rotation and compatibility with [Role-based access control]({{< relref "../../administration/roles-and-permissions/access-control" >}}). In a future release, we'll automatically migrate all existing API keys to service accounts. All of your existing tokens will continue to work; they will simply be migrated to service accounts. You can preempt this change by migrating your existing API keys to service accounts using Grafana's UI or API. Learn how to do this in the [API keys documentation]({{< relref "../../administration/api-keys#migrate-api-keys-to-grafana-service-accounts" >}}).
+We are upgrading Grafana [API keys](/docs/grafana//administration/service-accounts/migrate-api-keys/) to service accounts. Service accounts are a superset of API keys that include token rotation and compatibility with [Role-based access control]({{< relref "../../administration/roles-and-permissions/access-control" >}}). In a future release, we'll automatically migrate all existing API keys to service accounts. All of your existing tokens will continue to work; they will simply be migrated to service accounts. You can preempt this change by migrating your existing API keys to service accounts using Grafana's UI or API. Learn how to do this in the [API keys documentation](/docs/grafana//administration/service-accounts/migrate-api-keys/).
diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx
index dbfd00c36c4..201fa2305cd 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx
@@ -1,25 +1,22 @@
import { PluginPage, usePluginLinks } from '@grafana/runtime';
+import { Stack } from '@grafana/ui';
+import { ActionButton } from '../components/ActionButton';
import { testIds } from '../testIds';
export const LINKS_EXTENSION_POINT_ID = 'plugins/grafana-extensionstest-app/use-plugin-links/v1';
export function AddedLinks() {
- const { links, isLoading } = usePluginLinks({ extensionPointId: LINKS_EXTENSION_POINT_ID });
+ const { links } = usePluginLinks({ extensionPointId: LINKS_EXTENSION_POINT_ID });
return (
-
);
diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx
index e56c7fd44fb..c7dab986d29 100644
--- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx
@@ -65,10 +65,32 @@ describe('useContactPoints', () => {
expect(snapshot).toMatchSnapshot();
});
- it('returns matching responses with and without alertingApiServer', async () => {
+ it('returns ~matching responses with and without alertingApiServer', async () => {
+ // Compare the responses between the two implementations, but do not consider:
+ // - ID: k8s API will return id properties, but the AM config will fall back to the name of the contact point.
+ // These will be different, so we don't want to compare them
+ // - Metadata: k8s API includes metadata, AM config does not
+
const snapshotAmConfig = await getHookResponse(false);
const snapshotAlertingApiServer = await getHookResponse(true);
- expect(snapshotAmConfig).toEqual(snapshotAlertingApiServer);
+
+ const amContactPoints = snapshotAmConfig.contactPoints.map((receiver) => {
+ const { id, ...rest } = receiver;
+ return rest;
+ });
+
+ const k8sContactPoints = snapshotAlertingApiServer.contactPoints.map((receiver) => {
+ const { id, metadata, ...rest } = receiver;
+ return rest;
+ });
+
+ expect({
+ ...snapshotAmConfig,
+ contactPoints: amContactPoints,
+ }).toEqual({
+ ...snapshotAlertingApiServer,
+ contactPoints: k8sContactPoints,
+ });
});
describe('when having oncall plugin installed and no alert manager config data', () => {
diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx
index ce95cda1c65..ef7bb63bbff 100644
--- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx
@@ -7,14 +7,29 @@ import { produce } from 'immer';
import { remove } from 'lodash';
import { useMemo } from 'react';
+import { alertingApi } from 'app/features/alerting/unified/api/alertingApi';
+import { receiversApi } from 'app/features/alerting/unified/api/receiversK8sApi';
+import { useOnCallIntegration } from 'app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration';
import {
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver,
generatedReceiversApi,
} from 'app/features/alerting/unified/openapi/receiversApi.gen';
+import { updateAlertManagerConfigAction } from 'app/features/alerting/unified/state/actions';
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
-import { getK8sNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
+import {
+ getK8sNamespace,
+ isK8sEntityProvisioned,
+ shouldUseK8sApi,
+} from 'app/features/alerting/unified/utils/k8s/utils';
+import { updateConfigWithReceiver } from 'app/features/alerting/unified/utils/receiver-form';
+import {
+ GrafanaManagedContactPoint,
+ GrafanaManagedReceiverConfig,
+ Receiver,
+} from 'app/plugins/datasource/alertmanager/types';
+import { useDispatch } from 'app/types';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { onCallApi } from '../../api/onCallApi';
@@ -42,7 +57,13 @@ const {
useUpdateAlertmanagerConfigurationMutation,
} = alertmanagerApi;
const { useGrafanaOnCallIntegrationsQuery } = onCallApi;
-const { useListNamespacedReceiverQuery } = generatedReceiversApi;
+const {
+ useListNamespacedReceiverQuery,
+ useReadNamespacedReceiverQuery,
+ useDeleteNamespacedReceiverMutation,
+ useCreateNamespacedReceiverMutation,
+ useReplaceNamespacedReceiverMutation,
+} = receiversApi;
const defaultOptions = {
refetchOnFocus: true,
@@ -71,19 +92,26 @@ const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
type K8sReceiver = ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;
-// TODO: Make this typed as returning `GrafanaManagedContactPoint` - we can't yet do this as the schema thinks
-// its returning integration settings as a `string` rather than `Record`
-const parseK8sReceiver = (item: K8sReceiver) => {
- return { name: item.spec.title, grafana_managed_receiver_configs: item.spec.integrations };
+const parseK8sReceiver = (item: K8sReceiver): GrafanaManagedContactPoint => {
+ return {
+ id: item.metadata.uid || item.spec.title,
+ name: item.spec.title,
+ provisioned: isK8sEntityProvisioned(item),
+ grafana_managed_receiver_configs: item.spec.integrations,
+ metadata: item.metadata,
+ };
};
const useK8sContactPoints = (...[hookParams, queryOptions]: Parameters) => {
return useListNamespacedReceiverQuery(hookParams, {
...queryOptions,
- selectFromResult: ({ data, ...rest }) => {
+ selectFromResult: (result) => {
+ const data = result.data?.items.map((item) => parseK8sReceiver(item));
+
return {
- ...rest,
- data: data?.items.map((item) => parseK8sReceiver(item)),
+ ...result,
+ data,
+ currentData: data,
};
},
});
@@ -97,7 +125,21 @@ const useFetchGrafanaContactPoints = ({ skip }: Skippable = {}) => {
const namespace = getK8sNamespace();
const useK8sApi = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);
- const grafanaResponse = useGetContactPointsListQuery(undefined, { skip: skip || useK8sApi });
+ const grafanaResponse = useGetContactPointsListQuery(undefined, {
+ skip: skip || useK8sApi,
+ selectFromResult: (result) => {
+ const data = result.data?.map((item) => ({
+ ...item,
+ provisioned: item.grafana_managed_receiver_configs?.some((item) => item.provenance),
+ }));
+
+ return {
+ ...result,
+ data,
+ currentData: data,
+ };
+ },
+ });
const k8sResponse = useK8sContactPoints({ namespace }, { skip: skip || !useK8sApi });
return useK8sApi ? k8sResponse : grafanaResponse;
@@ -115,7 +157,7 @@ type GrafanaFetchOptions = {
};
/**
- * Fetch contact points from separate endpoint (i.e. not the Alertmanager config) and combine with
+ * Fetch list of contact points from separate endpoint (i.e. not the Alertmanager config) and combine with
* OnCall integrations and any additional metadata from list of notifiers
* (e.g. hydrate with additional names/descriptions)
*/
@@ -173,6 +215,71 @@ export const useGrafanaContactPoints = ({
]);
};
+/**
+ * Fetch single contact point via the alertmanager config
+ */
+const useGetAlertmanagerContactPoint = (
+ { alertmanager, name }: BaseAlertmanagerArgs & { name: string },
+ queryOptions?: Parameters[1]
+) => {
+ return useGetAlertmanagerConfigurationQuery(alertmanager, {
+ ...queryOptions,
+ selectFromResult: (result) => {
+ const matchedContactPoint = result.data?.alertmanager_config.receivers?.find(
+ (receiver) => receiver.name === name
+ );
+ return {
+ ...result,
+ data: matchedContactPoint,
+ currentData: matchedContactPoint,
+ };
+ },
+ });
+};
+
+/**
+ * Fetch single contact point via the k8s API, or the alertmanager config
+ */
+const useGetGrafanaContactPoint = (
+ { name }: { name: string },
+ queryOptions?: Parameters[1]
+) => {
+ const namespace = getK8sNamespace();
+ const useK8sApi = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);
+
+ const k8sResponse = useReadNamespacedReceiverQuery(
+ { namespace, name },
+ {
+ ...queryOptions,
+ selectFromResult: (result) => {
+ const data = result.data ? parseK8sReceiver(result.data) : undefined;
+ return {
+ ...result,
+ data,
+ currentData: data,
+ };
+ },
+ skip: queryOptions?.skip || !useK8sApi,
+ }
+ );
+
+ const grafanaResponse = useGetAlertmanagerContactPoint(
+ { alertmanager: GRAFANA_RULES_SOURCE_NAME, name },
+ { skip: queryOptions?.skip || useK8sApi }
+ );
+
+ return useK8sApi ? k8sResponse : grafanaResponse;
+};
+
+export const useGetContactPoint = ({ alertmanager, name }: { alertmanager: string; name: string }) => {
+ const isGrafana = alertmanager === GRAFANA_RULES_SOURCE_NAME;
+
+ const grafanaResponse = useGetGrafanaContactPoint({ name }, { skip: !isGrafana });
+ const alertmanagerResponse = useGetAlertmanagerContactPoint({ alertmanager, name }, { skip: isGrafana });
+
+ return isGrafana ? grafanaResponse : alertmanagerResponse;
+};
+
export function useContactPointsWithStatus({
alertmanager,
fetchStatuses,
@@ -203,30 +310,184 @@ export function useContactPointsWithStatus({
return isGrafanaAlertmanager ? grafanaResponse : alertmanagerConfigResponse;
}
-export function useDeleteContactPoint(selectedAlertmanager: string) {
+export function useDeleteContactPoint({ alertmanager }: BaseAlertmanagerArgs) {
const [fetchAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery();
- const [updateAlertManager, updateAlertmanagerState] = useUpdateAlertmanagerConfigurationMutation();
+ const [updateAlertManager] = useUpdateAlertmanagerConfigurationMutation();
+ const [deleteReceiver] = useDeleteNamespacedReceiverMutation();
- const deleteTrigger = (contactPointName: string) => {
- return fetchAlertmanagerConfig(selectedAlertmanager).then(({ data }) => {
- if (!data) {
- return;
- }
+ const useK8sApi = shouldUseK8sApi(alertmanager);
- const newConfig = produce(data, (draft) => {
- remove(draft?.alertmanager_config?.receivers ?? [], (receiver) => receiver.name === contactPointName);
- return draft;
- });
-
- return updateAlertManager({
- selectedAlertmanager,
- config: newConfig,
+ return async ({ name, resourceVersion }: { name: string; resourceVersion?: string }) => {
+ if (useK8sApi) {
+ const namespace = getK8sNamespace();
+ return deleteReceiver({
+ name,
+ namespace,
+ ioK8SApimachineryPkgApisMetaV1DeleteOptions: { preconditions: { resourceVersion } },
}).unwrap();
- });
- };
+ }
+ const config = await fetchAlertmanagerConfig(alertmanager).unwrap();
- return {
- deleteTrigger,
- updateAlertmanagerState,
+ const newConfig = produce(config, (draft) => {
+ remove(draft?.alertmanager_config?.receivers ?? [], (receiver) => receiver.name === name);
+ return draft;
+ });
+
+ return updateAlertManager({
+ selectedAlertmanager: alertmanager,
+ config: newConfig,
+ }).unwrap();
};
}
+
+const mapIntegrationSettings = (integration: GrafanaManagedReceiverConfig): GrafanaManagedReceiverConfig => {
+ return {
+ ...integration,
+ settings: { ...integration.settings, ...integration.secureSettings },
+ };
+};
+const grafanaContactPointToK8sReceiver = (
+ contactPoint: GrafanaManagedContactPoint,
+ id?: string,
+ resourceVersion?: string
+): K8sReceiver => {
+ return {
+ metadata: {
+ ...(id && { name: id }),
+ resourceVersion,
+ },
+ spec: {
+ title: contactPoint.name,
+ integrations: (contactPoint.grafana_managed_receiver_configs || []).map(mapIntegrationSettings),
+ },
+ };
+};
+
+type ContactPointOperationArgs = {
+ contactPoint: Receiver;
+};
+
+type CreateContactPointArgs = ContactPointOperationArgs;
+type UpdateContactPointArgs = ContactPointOperationArgs & {
+ /** ID of existing contact point to update - used when updating via k8s API */
+ id?: string;
+ resourceVersion?: string;
+ /** Name of the existing contact point - used for checking uniqueness of name when not using k8s API*/
+ originalName?: string;
+};
+
+export const useCreateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
+ const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
+ const useK8sApi = shouldUseK8sApi(alertmanager);
+
+ const { createOnCallIntegrations } = useOnCallIntegration();
+ const [getAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery();
+ const [createGrafanaContactPoint] = useCreateNamespacedReceiverMutation();
+ const dispatch = useDispatch();
+
+ return async ({ contactPoint }: CreateContactPointArgs) => {
+ const receiverWithPotentialOnCall = isGrafanaAlertmanager
+ ? await createOnCallIntegrations(contactPoint)
+ : contactPoint;
+
+ if (useK8sApi) {
+ const namespace = getK8sNamespace();
+
+ const contactPointToUse = grafanaContactPointToK8sReceiver(receiverWithPotentialOnCall);
+
+ return createGrafanaContactPoint({
+ namespace,
+ comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: contactPointToUse,
+ }).unwrap();
+ }
+
+ const config = await getAlertmanagerConfig(alertmanager).unwrap();
+ const newConfig = updateConfigWithReceiver(config, receiverWithPotentialOnCall);
+
+ return await dispatch(
+ updateAlertManagerConfigAction({
+ newConfig: newConfig,
+ oldConfig: config,
+ alertManagerSourceName: alertmanager,
+ })
+ )
+ .unwrap()
+ .then(() => {
+ dispatch(alertingApi.util.invalidateTags(['AlertmanagerConfiguration', 'ContactPoint', 'ContactPointsStatus']));
+ dispatch(generatedReceiversApi.util.invalidateTags(['Receiver']));
+ });
+ };
+};
+
+export const useUpdateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
+ const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
+ const useK8sApi = shouldUseK8sApi(alertmanager);
+
+ const { createOnCallIntegrations } = useOnCallIntegration();
+ const [getAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery();
+ const [replaceGrafanaContactPoint] = useReplaceNamespacedReceiverMutation();
+
+ const dispatch = useDispatch();
+
+ return async ({ contactPoint, id, originalName, resourceVersion }: UpdateContactPointArgs) => {
+ const receiverWithPotentialOnCall = isGrafanaAlertmanager
+ ? await createOnCallIntegrations(contactPoint)
+ : contactPoint;
+
+ if (useK8sApi && id) {
+ const namespace = getK8sNamespace();
+
+ const contactPointToUse = grafanaContactPointToK8sReceiver(receiverWithPotentialOnCall, id, resourceVersion);
+
+ return replaceGrafanaContactPoint({
+ name: id,
+ namespace,
+ comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: contactPointToUse,
+ }).unwrap();
+ }
+
+ const config = await getAlertmanagerConfig(alertmanager).unwrap();
+ const newConfig = updateConfigWithReceiver(config, receiverWithPotentialOnCall, originalName);
+
+ return dispatch(
+ updateAlertManagerConfigAction({
+ newConfig: newConfig,
+ oldConfig: config,
+ alertManagerSourceName: alertmanager,
+ })
+ )
+ .unwrap()
+ .then(() => {
+ dispatch(alertingApi.util.invalidateTags(['AlertmanagerConfiguration', 'ContactPoint', 'ContactPointsStatus']));
+ dispatch(generatedReceiversApi.util.invalidateTags(['Receiver']));
+ });
+ };
+};
+
+export const useValidateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
+ const useK8sApi = shouldUseK8sApi(alertmanager);
+
+ const [getConfig] = useLazyGetAlertmanagerConfigurationQuery();
+
+ // If we're using the kubernetes API, then we let the API response handle the validation instead
+ // as we don't expect to be able to fetch the intervals via the AM config
+ if (useK8sApi) {
+ return () => undefined;
+ }
+
+ return async (value: string, existingValue?: string) => {
+ // If we've been given an existing value, and the name has not changed,
+ // we can skip validation
+ // (as we don't want to incorrectly flag the existing name as matching itself)
+ if (existingValue && value === existingValue) {
+ return;
+ }
+ return getConfig(alertmanager)
+ .unwrap()
+ .then((config) => {
+ const { alertmanager_config } = config;
+ const duplicated = Boolean(alertmanager_config.receivers?.find((contactPoint) => contactPoint.name === value));
+ return duplicated ? `Contact point already exists with name "${value}"` : undefined;
+ });
+ };
+};
diff --git a/public/app/features/alerting/unified/components/contact-points/utils.test.ts b/public/app/features/alerting/unified/components/contact-points/utils.test.ts
index bd25cb886ee..e8ded92bf74 100644
--- a/public/app/features/alerting/unified/components/contact-points/utils.test.ts
+++ b/public/app/features/alerting/unified/components/contact-points/utils.test.ts
@@ -1,5 +1,3 @@
-import { GrafanaManagedContactPoint } from 'app/plugins/datasource/alertmanager/types';
-
import { ReceiverTypes } from '../receivers/grafanaAppReceivers/onCall/onCall';
import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from './constants';
@@ -7,34 +5,9 @@ import {
ReceiverConfigWithMetadata,
getReceiverDescription,
isAutoGeneratedPolicy,
- isProvisioned,
summarizeEmailAddresses,
} from './utils';
-describe('isProvisioned', () => {
- it('should return true when at least one receiver is provisioned', () => {
- const contactPoint: GrafanaManagedContactPoint = {
- name: 'my-contact-point',
- grafana_managed_receiver_configs: [
- { name: 'email', provenance: 'api', type: 'email', disableResolveMessage: false, settings: {} },
- ],
- };
-
- expect(isProvisioned(contactPoint)).toBe(true);
- });
-
- it('should return false when no receiver was provisioned', () => {
- const contactPoint: GrafanaManagedContactPoint = {
- name: 'my-contact-point',
- grafana_managed_receiver_configs: [
- { name: 'email', provenance: undefined, type: 'email', disableResolveMessage: false, settings: {} },
- ],
- };
-
- expect(isProvisioned(contactPoint)).toBe(false);
- });
-});
-
describe('isAutoGeneratedPolicy', () => {
it('should return false when not enabled', () => {
expect(isAutoGeneratedPolicy({})).toBe(false);
diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts
index 566e1f34763..7288d9785cd 100644
--- a/public/app/features/alerting/unified/components/contact-points/utils.ts
+++ b/public/app/features/alerting/unified/components/contact-points/utils.ts
@@ -22,13 +22,6 @@ import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY, RECEIVER_STATUS_KEY } from
const AUTOGENERATED_RECEIVER_POLICY_MATCHER_KEY = '__grafana_receiver__';
-export function isProvisioned(contactPoint: GrafanaManagedContactPoint) {
- // for some reason the provenance is on the receiver and not the entire contact point
- const provenance = contactPoint.grafana_managed_receiver_configs?.find((receiver) => receiver.provenance)?.provenance;
-
- return Boolean(provenance);
-}
-
// TODO we should really add some type information to these receiver settings...
export function getReceiverDescription(receiver: ReceiverConfigWithMetadata): ReactNode | undefined {
if (!receiver.settings) {
@@ -98,6 +91,7 @@ export interface ReceiverConfigWithMetadata extends GrafanaManagedReceiverConfig
}
export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
+ id: string;
policies?: RouteReference[]; // now is optional as we don't have the data from the read-only endpoint
grafana_managed_receiver_configs: ReceiverConfigWithMetadata[];
}
@@ -134,8 +128,11 @@ export function enhanceContactPointsWithMetadata({
const receivers = extractReceivers(contactPoint);
const statusForReceiver = status.find((status) => status.name === contactPoint.name);
+ const id = 'id' in contactPoint && contactPoint.id ? contactPoint.id : contactPoint.name;
+
return {
...contactPoint,
+ id,
policies:
alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? []),
grafana_managed_receiver_configs: receivers.map((receiver, index) => {
diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx
index 6101e5943a4..377d331b0cf 100644
--- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx
+++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx
@@ -9,8 +9,12 @@ import {
} from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
-import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
-import { getK8sNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
+import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
+import {
+ getK8sNamespace,
+ isK8sEntityProvisioned,
+ shouldUseK8sApi,
+} from 'app/features/alerting/unified/utils/k8s/utils';
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { useAsync } from '../../hooks/useAsync';
@@ -48,7 +52,7 @@ const parseK8sTimeInterval: (item: TimeIntervalV0Alpha1) => MuteTiming = (item)
...spec,
id: spec.name,
metadata,
- provisioned: metadata.annotations?.[PROVENANCE_ANNOTATION] !== PROVENANCE_NONE,
+ provisioned: isK8sEntityProvisioned(item),
};
};
diff --git a/public/app/features/alerting/unified/components/receivers/EditReceiverView.tsx b/public/app/features/alerting/unified/components/receivers/EditReceiverView.tsx
index 636e2944cbb..89bccca70b5 100644
--- a/public/app/features/alerting/unified/components/receivers/EditReceiverView.tsx
+++ b/public/app/features/alerting/unified/components/receivers/EditReceiverView.tsx
@@ -1,5 +1,4 @@
-import { Alert } from '@grafana/ui';
-import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
+import { GrafanaManagedContactPoint, Receiver } from 'app/plugins/datasource/alertmanager/types';
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
@@ -8,41 +7,25 @@ import { CloudReceiverForm } from './form/CloudReceiverForm';
import { GrafanaReceiverForm } from './form/GrafanaReceiverForm';
interface Props {
- receiverName: string;
- config: AlertManagerCortexConfig;
- alertManagerSourceName: string;
+ alertmanagerName: string;
+ contactPoint: GrafanaManagedContactPoint | Receiver;
}
-export const EditReceiverView = ({ config, receiverName, alertManagerSourceName }: Props) => {
+export const EditReceiverView = ({ contactPoint, alertmanagerName }: Props) => {
const [editSupported, editAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint);
- const receiver = config.alertmanager_config.receivers?.find(({ name }) => name === receiverName);
- if (!receiver) {
- return (
-
- Sorry, this receiver does not seem to exist.
-
- );
- }
-
const readOnly = !editSupported || !editAllowed;
- if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME) {
- return (
-
- );
+ if (alertmanagerName === GRAFANA_RULES_SOURCE_NAME) {
+ console.log(contactPoint);
+ return ;
} else {
return (
);
}
diff --git a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx
new file mode 100644
index 00000000000..fa6c20423ba
--- /dev/null
+++ b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx
@@ -0,0 +1,141 @@
+import 'core-js/stable/structured-clone';
+import { Route } from 'react-router';
+import { render, screen } from 'test/test-utils';
+import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector';
+
+import { config } from '@grafana/runtime';
+import { makeGrafanaAlertmanagerConfigUpdateFail } from 'app/features/alerting/unified/mocks/server/configure';
+import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
+import { AccessControlAction } from 'app/types';
+
+import { setupMswServer } from '../../mockApi';
+import { grantUserPermissions } from '../../mocks';
+import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
+
+import NewReceiverView from './NewReceiverView';
+
+setupMswServer();
+
+const renderForm = () =>
+ render(
+
+
+
+
+
+ redirected
+
+ ,
+ {
+ historyOptions: { initialEntries: ['/alerting/notifications/new'] },
+ }
+ );
+
+beforeEach(() => {
+ grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]);
+});
+
+describe('alerting API server enabled', () => {
+ beforeEach(() => {
+ config.featureToggles.alertingApiServer = true;
+ });
+
+ it('can create a receiver', async () => {
+ const { user } = renderForm();
+
+ expect(await ui.inputs.name.find()).toBeEnabled();
+
+ await user.type(await ui.inputs.name.find(), 'my new receiver');
+
+ // enter some email
+ const email = ui.inputs.email.addresses.get();
+ await user.clear(email);
+ await user.type(email, 'tester@grafana.com');
+
+ await user.click(ui.saveContactButton.get());
+
+ expect(await screen.findByText(/redirected/i)).toBeInTheDocument();
+ });
+});
+
+describe('alerting API server disabled', () => {
+ beforeEach(() => {
+ config.featureToggles.alertingApiServer = false;
+ });
+ it('should be able to test and save a receiver', async () => {
+ const capture = captureRequests();
+
+ const { user } = renderForm();
+
+ // wait for loading to be done
+ // type in a name for the new receiver
+ await user.type(await ui.inputs.name.find(), 'my new receiver');
+
+ // enter some email
+ const email = ui.inputs.email.addresses.get();
+ await user.clear(email);
+ await user.type(email, 'tester@grafana.com');
+
+ // try to test the contact point
+ await user.click(await ui.testContactPointButton.find());
+
+ expect(await ui.testContactPointModal.find()).toBeInTheDocument();
+
+ await user.click(ui.customContactPointOption.get());
+
+ // enter custom annotations and labels
+ await user.type(screen.getByPlaceholderText('Enter a description...'), 'Test contact point');
+ await user.type(ui.contactPointLabelKey(0).get(), 'foo');
+ await user.type(ui.contactPointLabelValue(0).get(), 'bar');
+
+ // click test
+ await user.click(ui.testContactPoint.get());
+
+ // we shouldn't be testing implementation details but when the request is successful
+ // it can't seem to assert on the success toast
+ await user.click(ui.saveContactButton.get());
+
+ const requests = await capture;
+ const testRequest = requests.find((r) => r.url.endsWith('/config/api/v1/receivers/test'));
+ const saveRequest = requests.find(
+ (r) => r.url.endsWith('/api/alertmanager/grafana/config/api/v1/alerts') && r.method === 'POST'
+ );
+
+ const testBody = await testRequest?.json();
+ const saveBody = await saveRequest?.json();
+
+ expect([testBody]).toMatchSnapshot();
+ expect([saveBody]).toMatchSnapshot();
+ });
+
+ it('does not redirect when creating contact point and API errors', async () => {
+ makeGrafanaAlertmanagerConfigUpdateFail();
+ const { user } = renderForm();
+
+ await user.type(await ui.inputs.name.find(), 'receiver that should fail');
+ const email = ui.inputs.email.addresses.get();
+ await user.clear(email);
+ await user.type(email, 'tester@grafana.com');
+
+ await user.click(ui.saveContactButton.get());
+
+ expect(screen.queryByText(/redirected/i)).not.toBeInTheDocument();
+ });
+});
+
+const ui = {
+ saveContactButton: byRole('button', { name: /save contact point/i }),
+
+ testContactPointButton: byRole('button', { name: /Test/ }),
+ testContactPointModal: byRole('heading', { name: /test contact point/i }),
+ customContactPointOption: byRole('radio', { name: /custom/i }),
+ contactPointLabelKey: (idx: number) => byTestId(`label-key-${idx}`),
+ contactPointLabelValue: (idx: number) => byTestId(`label-value-${idx}`),
+ testContactPoint: byRole('button', { name: /send test notification/i }),
+ inputs: {
+ name: byPlaceholderText('Name'),
+ email: {
+ addresses: byLabelText(/Addresses/),
+ },
+ },
+};
diff --git a/public/app/features/alerting/unified/components/receivers/NewReceiverView.tsx b/public/app/features/alerting/unified/components/receivers/NewReceiverView.tsx
index 5941336fe35..c84a9492d26 100644
--- a/public/app/features/alerting/unified/components/receivers/NewReceiverView.tsx
+++ b/public/app/features/alerting/unified/components/receivers/NewReceiverView.tsx
@@ -1,19 +1,17 @@
-import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
+import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { CloudReceiverForm } from './form/CloudReceiverForm';
import { GrafanaReceiverForm } from './form/GrafanaReceiverForm';
-interface Props {
- config: AlertManagerCortexConfig;
- alertManagerSourceName: string;
-}
-
-export const NewReceiverView = ({ alertManagerSourceName, config }: Props) => {
- if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME) {
- return ;
+const NewReceiverView = () => {
+ const { selectedAlertmanager } = useAlertmanager();
+ if (selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME) {
+ return ;
} else {
- return ;
+ return ;
}
};
+
+export default NewReceiverView;
diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap
similarity index 96%
rename from public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap
rename to public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap
index db54b874b61..999e5aaf80b 100644
--- a/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap
+++ b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should be able to test and save a receiver 1`] = `
+exports[`alerting API server disabled should be able to test and save a receiver 1`] = `
[
{
"alert": {
@@ -32,7 +32,7 @@ exports[`should be able to test and save a receiver 1`] = `
]
`;
-exports[`should be able to test and save a receiver 2`] = `
+exports[`alerting API server disabled should be able to test and save a receiver 2`] = `
[
{
"alertmanager_config": {
diff --git a/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx
index cfbdafcc8b7..2809617aac3 100644
--- a/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx
+++ b/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx
@@ -1,19 +1,18 @@
import { useMemo } from 'react';
+import { locationService } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
-import { AlertManagerCortexConfig, Receiver } from 'app/plugins/datasource/alertmanager/types';
-import { useDispatch } from 'app/types';
+import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi';
+import {
+ useCreateContactPoint,
+ useUpdateContactPoint,
+} from 'app/features/alerting/unified/components/contact-points/useContactPoints';
+import { Receiver } from 'app/plugins/datasource/alertmanager/types';
-import { alertmanagerApi } from '../../../api/alertmanagerApi';
-import { updateAlertManagerConfigAction } from '../../../state/actions';
import { CloudChannelValues, ReceiverFormValues, CloudChannelMap } from '../../../types/receiver-form';
import { cloudNotifierTypes } from '../../../utils/cloud-alertmanager-notifier-types';
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
-import {
- cloudReceiverToFormValues,
- formValuesToCloudReceiver,
- updateConfigWithReceiver,
-} from '../../../utils/receiver-form';
+import { cloudReceiverToFormValues, formValuesToCloudReceiver } from '../../../utils/receiver-form';
import { CloudCommonChannelSettings } from './CloudCommonChannelSettings';
import { ReceiverForm } from './ReceiverForm';
@@ -21,9 +20,9 @@ import { Notifier } from './notifiers';
interface Props {
alertManagerSourceName: string;
- config: AlertManagerCortexConfig;
- existing?: Receiver;
+ contactPoint?: Receiver;
readOnly?: boolean;
+ editMode?: boolean;
}
const defaultChannelValues: CloudChannelValues = Object.freeze({
@@ -36,39 +35,37 @@ const defaultChannelValues: CloudChannelValues = Object.freeze({
});
const cloudNotifiers = cloudNotifierTypes.map((n) => ({ dto: n }));
+const { useGetAlertmanagerConfigurationQuery } = alertmanagerApi;
+
+export const CloudReceiverForm = ({ contactPoint, alertManagerSourceName, readOnly = false, editMode }: Props) => {
+ const { isLoading, data: config } = useGetAlertmanagerConfigurationQuery(alertManagerSourceName);
-export const CloudReceiverForm = ({ existing, alertManagerSourceName, config, readOnly = false }: Props) => {
- const dispatch = useDispatch();
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
+ const createContactPoint = useCreateContactPoint({ alertmanager: alertManagerSourceName });
+ const updateContactPoint = useUpdateContactPoint({ alertmanager: alertManagerSourceName });
// transform receiver DTO to form values
const [existingValue] = useMemo((): [ReceiverFormValues | undefined, CloudChannelMap] => {
- if (!existing) {
+ if (!contactPoint) {
return [undefined, {}];
}
- return cloudReceiverToFormValues(existing, cloudNotifierTypes);
- }, [existing]);
+ return cloudReceiverToFormValues(contactPoint, cloudNotifierTypes);
+ }, [contactPoint]);
const onSubmit = async (values: ReceiverFormValues) => {
const newReceiver = formValuesToCloudReceiver(values, defaultChannelValues);
- await dispatch(
- updateAlertManagerConfigAction({
- newConfig: updateConfigWithReceiver(config, newReceiver, existing?.name),
- oldConfig: config,
- alertManagerSourceName,
- successMessage: existing ? 'Contact point updated.' : 'Contact point created.',
- redirectPath: '/alerting/notifications',
- })
- ).then(() => {
- dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
- });
+ try {
+ if (editMode) {
+ await updateContactPoint({ contactPoint: newReceiver, originalName: contactPoint!.name });
+ } else {
+ await createContactPoint({ contactPoint: newReceiver });
+ }
+ locationService.push('/alerting/notifications');
+ } catch (error) {
+ // React form validation will handle this for us
+ }
};
- const takenReceiverNames = useMemo(
- () => config.alertmanager_config.receivers?.map(({ name }) => name).filter((name) => name !== existing?.name) ?? [],
- [config, existing]
- );
-
// this basically checks if we can manage the selected alert manager data source, either because it's a Grafana Managed one
// or a Mimir-based AlertManager
const isManageableAlertManagerDataSource =
@@ -82,15 +79,14 @@ export const CloudReceiverForm = ({ existing, alertManagerSourceName, config, re
)}
+ showDefaultRouteWarning={!isLoading && !config?.alertmanager_config.route}
isEditable={isManageableAlertManagerDataSource}
isTestable={isManageableAlertManagerDataSource}
- config={config}
onSubmit={onSubmit}
initialValues={existingValue}
notifiers={cloudNotifiers}
alertManagerSourceName={alertManagerSourceName}
defaultItem={defaultChannelValues}
- takenReceiverNames={takenReceiverNames}
commonSettingsComponent={CloudCommonChannelSettings}
/>
>
diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx
index f4832592872..9cbf672221e 100644
--- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx
+++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx
@@ -1,5 +1,5 @@
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
-import { render, waitFor, userEvent } from 'test/test-utils';
+import { render, waitFor } from 'test/test-utils';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
@@ -8,11 +8,9 @@ import {
setOnCallIntegrations,
} from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
-import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { AlertmanagerConfigBuilder, setupMswServer } from '../../../mockApi';
-import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
import { GrafanaReceiverForm } from './GrafanaReceiverForm';
@@ -35,17 +33,11 @@ const ui = {
};
describe('GrafanaReceiverForm', () => {
- beforeEach(() => {
- clearPluginSettingsCache();
- });
-
describe('OnCall contact point', () => {
it('OnCall contact point should be disabled if OnCall integration is not enabled', async () => {
disablePlugin(SupportedPlugin.OnCall);
- const amConfig = getAmCortexConfig((_) => {});
-
- render();
+ render();
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
@@ -65,11 +57,7 @@ describe('GrafanaReceiverForm', () => {
{ display_name: 'apac-oncall', value: 'apac-oncall', integration_url: 'https://apac.oncall.example.com' },
]);
- const amConfig = getAmCortexConfig((_) => {});
-
- const user = userEvent.setup();
-
- render();
+ const { user } = render();
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
@@ -121,13 +109,7 @@ describe('GrafanaReceiverForm', () => {
)
);
- render(
-
- );
+ render();
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx
index 32e0652827e..ade39241b34 100644
--- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx
+++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx
@@ -1,8 +1,14 @@
import { useMemo, useState } from 'react';
+import { locationService } from '@grafana/runtime';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import {
- AlertManagerCortexConfig,
+ useCreateContactPoint,
+ useUpdateContactPoint,
+} from 'app/features/alerting/unified/components/contact-points/useContactPoints';
+import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
+import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
+import {
GrafanaManagedContactPoint,
GrafanaManagedReceiverConfig,
TestReceiversAlert,
@@ -10,14 +16,12 @@ import {
import { useDispatch } from 'app/types';
import { alertmanagerApi } from '../../../api/alertmanagerApi';
-import { testReceiversAction, updateAlertManagerConfigAction } from '../../../state/actions';
+import { testReceiversAction } from '../../../state/actions';
import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
-import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
import {
formChannelValuesToGrafanaChannelConfig,
formValuesToGrafanaReceiver,
grafanaReceiverToFormValues,
- updateConfigWithReceiver,
} from '../../../utils/receiver-form';
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
import { ReceiverTypes } from '../grafanaAppReceivers/onCall/onCall';
@@ -28,13 +32,6 @@ import { ReceiverForm } from './ReceiverForm';
import { TestContactPointModal } from './TestContactPointModal';
import { Notifier } from './notifiers';
-interface Props {
- alertManagerSourceName: string;
- config: AlertManagerCortexConfig;
- existing?: GrafanaManagedContactPoint;
- readOnly?: boolean;
-}
-
const defaultChannelValues: GrafanaChannelValues = Object.freeze({
__id: '',
secureSettings: {},
@@ -44,20 +41,28 @@ const defaultChannelValues: GrafanaChannelValues = Object.freeze({
type: 'email',
});
-export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config, readOnly = false }: Props) => {
+interface Props {
+ contactPoint?: GrafanaManagedContactPoint;
+ readOnly?: boolean;
+ editMode?: boolean;
+}
+
+const { useGrafanaNotifiersQuery } = alertmanagerApi;
+
+export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }: Props) => {
const dispatch = useDispatch();
+ const createContactPoint = useCreateContactPoint({ alertmanager: GRAFANA_RULES_SOURCE_NAME });
+ const updateContactPoint = useUpdateContactPoint({ alertmanager: GRAFANA_RULES_SOURCE_NAME });
const {
onCallNotifierMeta,
extendOnCallNotifierFeatures,
extendOnCallReceivers,
onCallFormValidators,
- createOnCallIntegrations,
isLoadingOnCallIntegration,
hasOnCallError,
} = useOnCallIntegration();
- const { useGrafanaNotifiersQuery } = alertmanagerApi;
const { data: grafanaNotifiers = [], isLoading: isLoadingNotifiers } = useGrafanaNotifiersQuery();
const [testChannelValues, setTestChannelValues] = useState();
@@ -67,29 +72,30 @@ export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config,
ReceiverFormValues | undefined,
Record,
] => {
- if (!existing || isLoadingNotifiers || isLoadingOnCallIntegration) {
+ if (!contactPoint || isLoadingNotifiers || isLoadingOnCallIntegration) {
return [undefined, {}];
}
- return grafanaReceiverToFormValues(extendOnCallReceivers(existing), grafanaNotifiers);
- }, [existing, isLoadingNotifiers, grafanaNotifiers, extendOnCallReceivers, isLoadingOnCallIntegration]);
+ return grafanaReceiverToFormValues(extendOnCallReceivers(contactPoint), grafanaNotifiers);
+ }, [contactPoint, isLoadingNotifiers, grafanaNotifiers, extendOnCallReceivers, isLoadingOnCallIntegration]);
const onSubmit = async (values: ReceiverFormValues) => {
const newReceiver = formValuesToGrafanaReceiver(values, id2original, defaultChannelValues, grafanaNotifiers);
- const receiverWithOnCall = await createOnCallIntegrations(newReceiver);
-
- const newConfig = updateConfigWithReceiver(config, receiverWithOnCall, existing?.name);
- await dispatch(
- updateAlertManagerConfigAction({
- newConfig: newConfig,
- oldConfig: config,
- alertManagerSourceName: GRAFANA_RULES_SOURCE_NAME,
- successMessage: existing ? 'Contact point updated.' : 'Contact point created',
- redirectPath: '/alerting/notifications',
- })
- ).then(() => {
- dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
- });
+ try {
+ if (editMode) {
+ await updateContactPoint({
+ contactPoint: newReceiver,
+ id: contactPoint!.id,
+ resourceVersion: contactPoint?.metadata?.resourceVersion,
+ originalName: contactPoint?.name,
+ });
+ } else {
+ await createContactPoint({ contactPoint: newReceiver });
+ }
+ locationService.push('/alerting/notifications');
+ } catch (error) {
+ // React form validation will handle this for us
+ }
};
const onTestChannel = (values: GrafanaChannelValues) => {
@@ -102,7 +108,7 @@ export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config,
const chan = formChannelValuesToGrafanaChannelConfig(testChannelValues, defaultChannelValues, 'test', existing);
const payload = {
- alertManagerSourceName,
+ alertManagerSourceName: GRAFANA_RULES_SOURCE_NAME,
receivers: [
{
name: 'test',
@@ -116,17 +122,7 @@ export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config,
}
};
- const takenReceiverNames = useMemo(
- () => config.alertmanager_config.receivers?.map(({ name }) => name).filter((name) => name !== existing?.name) ?? [],
- [config, existing]
- );
-
- // if any receivers in the contact point have a "provenance", the entire contact point should be readOnly
- const hasProvisionedItems = existing
- ? (existing.grafana_managed_receiver_configs ?? []).some((item) => Boolean(item.provenance))
- : false;
-
- const isEditable = !readOnly && !hasProvisionedItems;
+ const isEditable = !readOnly && !contactPoint?.provisioned;
const isTestable = !readOnly;
if (isLoadingNotifiers || isLoadingOnCallIntegration) {
@@ -134,7 +130,7 @@ export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config,
}
const notifiers: Notifier[] = grafanaNotifiers.map((n) => {
- if (n.type === 'oncall') {
+ if (n.type === ReceiverTypes.OnCall) {
return {
dto: extendOnCallNotifierFeatures(n),
meta: onCallNotifierMeta,
@@ -143,7 +139,7 @@ export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config,
return { dto: n };
});
-
+ const disableEditTitle = editMode && shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);
return (
<>
{hasOnCallError && (
@@ -153,19 +149,18 @@ export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config,
)}
- {hasProvisionedItems && }
+ {contactPoint?.provisioned && }
+ disableEditTitle={disableEditTitle}
isEditable={isEditable}
isTestable={isTestable}
- config={config}
onSubmit={onSubmit}
initialValues={existingValue}
onTestChannel={onTestChannel}
notifiers={notifiers}
- alertManagerSourceName={alertManagerSourceName}
+ alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
defaultItem={{ ...defaultChannelValues }}
- takenReceiverNames={takenReceiverNames}
commonSettingsComponent={GrafanaCommonChannelSettings}
customValidators={{ [ReceiverTypes.OnCall]: onCallFormValidators }}
/>
diff --git a/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx
index f726c204426..0d1e6b74f15 100644
--- a/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx
+++ b/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx
@@ -1,14 +1,13 @@
import { css } from '@emotion/css';
-import { useCallback } from 'react';
import * as React from 'react';
-import { FieldErrors, FormProvider, SubmitErrorHandler, useForm, Validate } from 'react-hook-form';
+import { FieldErrors, FormProvider, SubmitErrorHandler, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { isFetchError } from '@grafana/runtime';
import { Alert, Button, Field, Input, LinkButton, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup';
-import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
+import { useValidateContactPoint } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { getMessageFromError } from '../../../../../../core/utils/errors';
import { logError } from '../../../Analytics';
@@ -24,36 +23,45 @@ import { Notifier } from './notifiers';
import { normalizeFormValues } from './util';
interface Props {
- config: AlertManagerCortexConfig;
notifiers: Notifier[];
defaultItem: R;
alertManagerSourceName: string;
onTestChannel?: (channel: R) => void;
onSubmit: (values: ReceiverFormValues) => Promise;
- takenReceiverNames: string[]; // will validate that user entered receiver name is not one of these
commonSettingsComponent: CommonSettingsComponentType;
initialValues?: ReceiverFormValues;
isEditable: boolean;
isTestable?: boolean;
customValidators?: Record['customValidators']>;
+ /**
+ * Should we show a warning that there is no default policy set,
+ * and that contact point being created will be set as the default?
+ */
+ showDefaultRouteWarning?: boolean;
+ /**
+ * Should we disable the title editing? Required when we're using the API server,
+ * as at the time of writing this is not possible
+ */
+ disableEditTitle?: boolean;
}
export function ReceiverForm({
- config,
initialValues,
defaultItem,
notifiers,
alertManagerSourceName,
onSubmit,
onTestChannel,
- takenReceiverNames,
commonSettingsComponent,
isEditable,
isTestable,
customValidators,
-}: Props): JSX.Element {
+ showDefaultRouteWarning,
+ disableEditTitle,
+}: Props) {
const notifyApp = useAppNotification();
const styles = useStyles2(getStyles);
+ const validateContactPointName = useValidateContactPoint({ alertmanager: alertManagerSourceName });
// normalize deprecated and new config values
const normalizedConfig = normalizeFormValues(initialValues);
@@ -84,14 +92,6 @@ export function ReceiverForm({
const { fields, append, remove } = useControlledFieldArray({ name: 'items', formAPI, softDelete: true });
- const validateNameIsAvailable: Validate> = useCallback(
- (name: string) =>
- takenReceiverNames.map((name) => name.trim().toLowerCase()).includes(name.trim().toLowerCase())
- ? 'Another receiver with this name already exists.'
- : true,
- [takenReceiverNames]
- );
-
const submitCallback = async (values: ReceiverFormValues) => {
try {
await onSubmit({
@@ -116,7 +116,7 @@ export function ReceiverForm({
return (
- {!config.alertmanager_config.route && (
+ {showDefaultRouteWarning && (
Because there is no default policy configured yet, this contact point will automatically be set as default.
@@ -127,11 +127,15 @@ export function ReceiverForm({
{
+ const existingValue = initialValues?.name;
+ return validateContactPointName(value, existingValue);
+ },
})}
width={39}
placeholder="Name"
@@ -198,7 +202,7 @@ export function ReceiverForm({
disabled={isSubmitting}
variant="secondary"
data-testid="cancel-button"
- href={makeAMLink('alerting/notifications', alertManagerSourceName)}
+ href={makeAMLink('/alerting/notifications', alertManagerSourceName)}
>
Cancel
diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx
index 28bc7e3bbf3..3a3a4729d48 100644
--- a/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx
+++ b/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx
@@ -35,6 +35,7 @@ export const OptionField: FC = ({
customValidator,
}) => {
const optionPath = `${pathPrefix}${pathSuffix}`;
+ const isSecure = pathSuffix === 'secureSettings.';
if (option.element === 'subform') {
return (
@@ -75,12 +76,13 @@ export const OptionField: FC = ({
readOnly={readOnly}
pathIndex={pathPrefix}
customValidator={customValidator}
+ isSecure={isSecure}
/>
);
};
-const OptionInput: FC = ({
+const OptionInput: FC = ({
option,
invalid,
id,
@@ -88,11 +90,19 @@ const OptionInput: FC = ({
pathIndex = '',
readOnly = false,
customValidator,
+ isSecure,
}) => {
const styles = useStyles2(getStyles);
const { control, register, unregister, getValues, setValue } = useFormContext();
const name = `${pathPrefix}${option.propertyName}`;
+ useEffect(() => {
+ // Remove the value of secure fields so it doesn't show the incorrect value when clearing the field
+ if (isSecure) {
+ setValue(name, null);
+ }
+ }, [isSecure, name, setValue]);
+
// workaround for https://github.com/react-hook-form/react-hook-form/issues/4993#issuecomment-829012506
useEffect(
() => () => {
diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.ts
index 9ea8bad7700..27adfd1f370 100644
--- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.ts
+++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.ts
@@ -142,7 +142,8 @@ export function useOnCallIntegration() {
return receiver;
}
- const onCallIntegrations = receiver.grafana_managed_receiver_configs?.filter((c) => c.type === 'oncall') ?? [];
+ const onCallIntegrations =
+ receiver.grafana_managed_receiver_configs?.filter((c) => c.type === ReceiverTypes.OnCall) ?? [];
const newOnCallIntegrations = onCallIntegrations.filter(
(c) => c.settings[OnCallIntegrationSetting.IntegrationType] === OnCallIntegrationType.NewIntegration
);
diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
index ed59a8e9791..992eba0a1a2 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx
@@ -12,18 +12,21 @@ import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
import {
getCloudRule,
getGrafanaRule,
+ getVanillaPromRule,
grantUserPermissions,
mockDataSource,
mockPluginLinkExtension,
+ mockPromAlertingRule,
} from '../../mocks';
import { grafanaRulerRule } from '../../mocks/grafanaRulerApi';
import { setupDataSources } from '../../testSetup/datasources';
import { Annotation } from '../../utils/constants';
import { DataSourceType } from '../../utils/datasource';
import * as ruleId from '../../utils/rule-id';
+import { stringifyIdentifier } from '../../utils/rule-id';
import { AlertRuleProvider } from './RuleContext';
-import RuleViewer from './RuleViewer';
+import RuleViewer, { ActiveTab } from './RuleViewer';
// metadata and interactive elements
const ELEMENTS = {
@@ -35,6 +38,12 @@ const ELEMENTS = {
evaluationInterval: (interval: string) => byText(`Every ${interval}`),
label: ([key, value]: [string, string]) => byRole('listitem', { name: `${key}: ${value}` }),
},
+ details: {
+ pendingPeriod: byText(/Pending period/i),
+ },
+ tabs: {
+ details: byRole('tab', { name: /Details/i }),
+ },
actions: {
edit: byRole('link', { name: 'Edit' }),
more: {
@@ -259,13 +268,39 @@ describe('RuleViewer', () => {
await waitFor(() => expect(ELEMENTS.actions.more.pluginActions.declareIncident.get()).toBeEnabled());
});
});
+
+ describe('Vanilla Prometheus rule', () => {
+ const mockRule = getVanillaPromRule({
+ name: 'prom test alert',
+ annotations: { [Annotation.summary]: 'prom summary', [Annotation.runbookURL]: 'https://runbook.example.com' },
+ promRule: {
+ ...mockPromAlertingRule(),
+ duration: 900, // 15 minutes
+ },
+ });
+
+ const mockRuleIdentifier = ruleId.fromCombinedRule('prometheus', mockRule);
+
+ it('should render pending period for vanilla Prometheus alert rule', async () => {
+ renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Details);
+
+ expect(screen.getByText('prom test alert')).toBeInTheDocument();
+
+ // One summary is rendered by the Title component, and the other by the DetailsTab component
+ expect(ELEMENTS.metadata.summary(mockRule.annotations[Annotation.summary]).getAll()).toHaveLength(2);
+
+ expect(within(ELEMENTS.details.pendingPeriod.get()).getByText(/15m/i)).toBeInTheDocument();
+ });
+ });
});
-const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier) => {
+const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier, tab: ActiveTab = ActiveTab.Query) => {
+ const path = `/alerting/${identifier.ruleSourceName}/${stringifyIdentifier(identifier)}/view?tab=${tab}`;
render(
-
+ ,
+ { historyOptions: { initialEntries: [path] } }
);
await waitFor(() => expect(ELEMENTS.loading.query()).not.toBeInTheDocument());
diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx
index 5c723326abb..53c74196586 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx
@@ -7,6 +7,7 @@ import { ClipboardButton, Stack, Text, TextLink, useStyles2 } from '@grafana/ui'
import { CombinedRule } from 'app/types/unified-alerting';
import { Annotations } from 'app/types/unified-alerting-dto';
+import { usePendingPeriod } from '../../../hooks/rules/usePendingPeriod';
import { isGrafanaRulerRule, isRecordingRulerRule } from '../../../utils/rules';
import { MetaText } from '../../MetaText';
import { Tokenize } from '../../Tokenize';
@@ -26,6 +27,8 @@ const Details = ({ rule }: DetailsProps) => {
let ruleType: RuleType;
+ const pendingPeriod = usePendingPeriod(rule);
+
if (isGrafanaRulerRule(rule.rulerRule)) {
ruleType = RuleType.GrafanaManagedAlertRule;
} else if (isRecordingRulerRule(rule.rulerRule)) {
@@ -89,10 +92,10 @@ const Details = ({ rule }: DetailsProps) => {
)}
- {!isRecordingRulerRule(rule.rulerRule) && (
+ {pendingPeriod && (
<>
Pending period
- {rule.rulerRule?.for ?? '0s'}
+ {pendingPeriod}
>
)}
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
index 6c8ddb552e1..7c7659a5e32 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
@@ -5,8 +5,9 @@ import { Tooltip, useStyles2 } from '@grafana/ui';
import { Time } from 'app/features/explore/Time';
import { CombinedRule } from 'app/types/unified-alerting';
+import { usePendingPeriod } from '../../hooks/rules/usePendingPeriod';
import { useCleanAnnotations } from '../../utils/annotations';
-import { isGrafanaRecordingRule, isRecordingRulerRule } from '../../utils/rules';
+import { isGrafanaRecordingRule } from '../../utils/rules';
import { isNullDate } from '../../utils/time';
import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField';
@@ -64,16 +65,12 @@ interface EvaluationBehaviorSummaryProps {
}
const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => {
- let forDuration: string | undefined;
const every = rule.group.interval;
const lastEvaluation = rule.promRule?.lastEvaluation;
const lastEvaluationDuration = rule.promRule?.evaluationTime;
const metric = isGrafanaRecordingRule(rule.rulerRule) ? rule.rulerRule?.grafana_alert.record?.metric : undefined;
- // recording rules don't have a for duration
- if (!isRecordingRulerRule(rule.rulerRule)) {
- forDuration = rule.rulerRule?.for ?? '0s';
- }
+ const pendingPeriod = usePendingPeriod(rule);
return (
<>
@@ -88,9 +85,11 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
)}
-
- {forDuration}
-
+ {pendingPeriod && (
+
+ {pendingPeriod}
+
+ )}
{lastEvaluation && !isNullDate(lastEvaluation) && (
diff --git a/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx
new file mode 100644
index 00000000000..6aa79128c34
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx
@@ -0,0 +1,59 @@
+import { render, screen } from 'test/test-utils';
+
+import { setPluginExtensionsHook } from '@grafana/runtime';
+import { RuleListStateView } from 'app/features/alerting/unified/components/rules/RuleListStateView';
+import {
+ mockCombinedRule,
+ mockCombinedRuleGroup,
+ mockCombinedRuleNamespace,
+ mockPromAlertingRule,
+} from 'app/features/alerting/unified/mocks';
+import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
+
+setPluginExtensionsHook(() => ({
+ extensions: [],
+ isLoading: false,
+}));
+
+const namespaces = [
+ mockCombinedRuleNamespace({
+ groups: [
+ mockCombinedRuleGroup('Group with a missing state', [
+ mockCombinedRule({
+ name: 'Rule in firing state',
+ promRule: mockPromAlertingRule({
+ state: PromAlertingRuleState.Firing,
+ }),
+ }),
+ mockCombinedRule({
+ name: 'Rule in pending state',
+ promRule: mockPromAlertingRule({
+ state: PromAlertingRuleState.Pending,
+ }),
+ }),
+ mockCombinedRule({
+ name: 'Rule in inactive state',
+ promRule: mockPromAlertingRule({
+ state: PromAlertingRuleState.Inactive,
+ }),
+ }),
+ mockCombinedRule({
+ name: 'Rule with missing prom state',
+ promRule: mockPromAlertingRule({
+ state: undefined,
+ }),
+ }),
+ ]),
+ ],
+ }),
+];
+
+describe('RuleListStateView', () => {
+ it('renders differing prom rule states correctly and does not crash with missing state', () => {
+ render();
+
+ expect(screen.getByText(/firing \(1\)/i)).toBeInTheDocument();
+ expect(screen.getByText(/pending \(1\)/i)).toBeInTheDocument();
+ expect(screen.getByText(/normal \(1\)/i)).toBeInTheDocument();
+ });
+});
diff --git a/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx b/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx
index 04c16b82a06..d09f998d1a7 100644
--- a/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx
@@ -28,7 +28,10 @@ export const RuleListStateView = ({ namespaces }: Props) => {
namespaces.forEach((namespace) =>
namespace.groups.forEach((group) =>
group.rules.forEach((rule) => {
- if (rule.promRule && isAlertingRule(rule.promRule)) {
+ // We might hit edge cases where there type = alerting, but there is no state.
+ // In this case, we shouldn't try to group these alerts in the state view
+ // Even though we handle this at the API layer, this is a last catch point for any edge cases
+ if (rule.promRule && isAlertingRule(rule.promRule) && rule.promRule.state) {
result[rule.promRule.state].push(rule);
}
})
diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useMoveRuleFromRuleGroup.test.tsx.snap b/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useMoveRuleFromRuleGroup.test.tsx.snap
index 2374935f47a..d5ff767a76f 100644
--- a/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useMoveRuleFromRuleGroup.test.tsx.snap
+++ b/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useMoveRuleFromRuleGroup.test.tsx.snap
@@ -1,5 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Moving a Data source managed rule should move a rule in a namespace to another existing namespace 1`] = `
+[
+ {
+ "body": {
+ "name": "group-1",
+ "rules": [
+ {
+ "alert": "alert1",
+ "annotations": {
+ "summary": "test alert",
+ },
+ "expr": "up = 1",
+ "labels": {
+ "severity": "warning",
+ },
+ },
+ ],
+ },
+ "headers": [
+ [
+ "content-type",
+ "application/json",
+ ],
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "POST",
+ "url": "http://localhost/api/ruler/mimir/api/v1/rules/namespace-2?subtype=mimir",
+ },
+ {
+ "body": "",
+ "headers": [
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "DELETE",
+ "url": "http://localhost/api/ruler/mimir/api/v1/rules/namespace-1/group-1?subtype=mimir",
+ },
+]
+`;
+
exports[`Moving a Data source managed rule should move a rule in an existing group to a new group 1`] = `
[
{
@@ -125,7 +170,7 @@ exports[`Moving a Data source managed rule should move a rule in an existing gro
],
],
"method": "POST",
- "url": "http://localhost/api/ruler/mimir/api/v1/rules/namespace-1?subtype=mimir",
+ "url": "http://localhost/api/ruler/mimir/api/v1/rules/namespace-2?subtype=mimir",
},
{
"body": "",
diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useMoveRuleFromRuleGroup.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/useMoveRuleFromRuleGroup.test.tsx
index 2fda700d4ac..4426b16de92 100644
--- a/public/app/features/alerting/unified/hooks/ruleGroup/useMoveRuleFromRuleGroup.test.tsx
+++ b/public/app/features/alerting/unified/hooks/ruleGroup/useMoveRuleFromRuleGroup.test.tsx
@@ -184,6 +184,45 @@ describe('Moving a Data source managed rule', () => {
expect(serializedRequests).toMatchSnapshot();
});
+ it('should move a rule in a namespace to another existing namespace', async () => {
+ const capture = captureRequests((r) => r.method === 'POST' || r.method === 'DELETE');
+
+ const ruleToMove = group1.rules[0];
+
+ const coreRuleGroup = {
+ dataSourceName: MIMIR_DATASOURCE_UID,
+ groupName: group1.name,
+ };
+
+ const currentRuleGroupID: RuleGroupIdentifier = {
+ ...coreRuleGroup,
+ namespaceName: NAMESPACE_1,
+ };
+
+ const targetRuleGroupID: RuleGroupIdentifier = {
+ ...coreRuleGroup,
+ namespaceName: NAMESPACE_2,
+ };
+
+ const ruleID = fromRulerRuleAndRuleGroupIdentifier(currentRuleGroupID, ruleToMove);
+
+ const { user } = render(
+
+ );
+ await user.click(byRole('button').get());
+
+ expect(await byText(/success/i).find()).toBeInTheDocument();
+
+ const requests = await capture;
+ const serializedRequests = await serializeRequests(requests);
+ expect(serializedRequests).toMatchSnapshot();
+ });
+
it('should fail if the rule group does not exist', async () => {
const groupToUpdate = group1;
const ruleToUpdate = groupToUpdate.rules[0];
diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useUpsertRuleFromRuleGroup.ts b/public/app/features/alerting/unified/hooks/ruleGroup/useUpsertRuleFromRuleGroup.ts
index adc79753748..41a4664c54a 100644
--- a/public/app/features/alerting/unified/hooks/ruleGroup/useUpsertRuleFromRuleGroup.ts
+++ b/public/app/features/alerting/unified/hooks/ruleGroup/useUpsertRuleFromRuleGroup.ts
@@ -114,7 +114,7 @@ export function useMoveRuleToRuleGroup() {
const result = await upsertRuleGroup({
rulerConfig: targetGroupRulerConfig,
- namespace: currentRuleGroup.namespaceName,
+ namespace: targetRuleGroup.namespaceName,
payload: newTargetGroup,
notificationOptions: { successMessage },
}).unwrap();
diff --git a/public/app/features/alerting/unified/hooks/rules/usePendingPeriod.tsx b/public/app/features/alerting/unified/hooks/rules/usePendingPeriod.tsx
new file mode 100644
index 00000000000..d7ebd337f7c
--- /dev/null
+++ b/public/app/features/alerting/unified/hooks/rules/usePendingPeriod.tsx
@@ -0,0 +1,9 @@
+import { useMemo } from 'react';
+
+import { CombinedRule } from 'app/types/unified-alerting';
+
+import { getPendingPeriod } from '../../utils/rules';
+
+export function usePendingPeriod(rule: CombinedRule): string | undefined {
+ return useMemo(() => getPendingPeriod(rule), [rule]);
+}
diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts
index 699252137db..632868a90e0 100644
--- a/public/app/features/alerting/unified/mocks.ts
+++ b/public/app/features/alerting/unified/mocks.ts
@@ -785,6 +785,19 @@ export function getCloudRule(override?: Partial) {
});
}
+export function getVanillaPromRule(override?: Partial>) {
+ return mockCombinedRule({
+ namespace: {
+ groups: [],
+ name: 'Prometheus',
+ rulesSource: mockDataSource(),
+ },
+ promRule: mockPromAlertingRule(),
+ rulerRule: undefined,
+ ...override,
+ });
+}
+
export function mockPluginLinkExtension(extension: Partial): PluginExtensionLink {
return {
type: PluginExtensionTypes.link,
diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts
index 63b976f2a44..db47b049265 100644
--- a/public/app/features/alerting/unified/mocks/server/configure.ts
+++ b/public/app/features/alerting/unified/mocks/server/configure.ts
@@ -4,9 +4,11 @@ import { config } from '@grafana/runtime';
import server, { mockFeatureDiscoveryApi } from 'app/features/alerting/unified/mockApi';
import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks';
import {
+ ALERTMANAGER_UPDATE_ERROR_RESPONSE,
getAlertmanagerConfigHandler,
getGrafanaAlertmanagerConfigHandler,
grafanaAlertingConfigurationStatusHandler,
+ updateGrafanaAlertmanagerConfigHandler,
} from 'app/features/alerting/unified/mocks/server/handlers/alertmanagers';
import { getFolderHandler } from 'app/features/alerting/unified/mocks/server/handlers/folders';
import { listNamespacedTimeIntervalHandler } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
@@ -126,3 +128,8 @@ export const removePlugin = (pluginId: string) => {
export const disablePlugin = (pluginId: SupportedPlugin) => {
server.use(getDisabledPluginHandler(pluginId));
};
+
+/** Make alertmanager config update fail */
+export const makeGrafanaAlertmanagerConfigUpdateFail = () => {
+ server.use(updateGrafanaAlertmanagerConfigHandler(ALERTMANAGER_UPDATE_ERROR_RESPONSE));
+};
diff --git a/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts b/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts
index 2f1f493015e..37f7f8e1c46 100644
--- a/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts
+++ b/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts
@@ -35,7 +35,7 @@ export const getGrafanaAlertmanagerConfigHandler = (config: AlertManagerCortexCo
export const getAlertmanagerConfigHandler = (config: AlertManagerCortexConfig = alertmanagerConfigMock) =>
http.get('/api/alertmanager/:name/config/api/v1/alerts', () => HttpResponse.json(config));
-const alertmanagerUpdateError = HttpResponse.json({ message: 'bad request' }, { status: 400 });
+export const ALERTMANAGER_UPDATE_ERROR_RESPONSE = HttpResponse.json({ message: 'bad request' }, { status: 400 });
/** Perform some basic validation on the config that we expect the backend to also do */
const validateGrafanaAlertmanagerConfig = (config: AlertManagerCortexConfig) => {
@@ -46,27 +46,28 @@ const validateGrafanaAlertmanagerConfig = (config: AlertManagerCortexConfig) =>
const intervalsByName = new Set(intervals.map((interval) => interval.name));
const duplicatedIntervals = intervalsByName.size !== intervals.length;
+ let routesReferencingMissingMuteTimings = false;
+
if (route) {
- const routesReferencingMissingMuteTimings = Boolean(
+ routesReferencingMissingMuteTimings = Boolean(
route.routes?.find((route) => {
return route.mute_time_intervals?.some((name) => !intervalsByName.has(name));
})
);
-
- if (routesReferencingMissingMuteTimings) {
- return alertmanagerUpdateError;
- }
}
- if (duplicatedIntervals) {
- return alertmanagerUpdateError;
+ if (routesReferencingMissingMuteTimings || duplicatedIntervals) {
+ return ALERTMANAGER_UPDATE_ERROR_RESPONSE;
}
return null;
};
-const updateGrafanaAlertmanagerConfigHandler = () =>
+export const updateGrafanaAlertmanagerConfigHandler = (responseOverride?: typeof ALERTMANAGER_UPDATE_ERROR_RESPONSE) =>
http.post('/api/alertmanager/grafana/config/api/v1/alerts', async ({ request }) => {
+ if (responseOverride) {
+ return responseOverride;
+ }
const body: AlertManagerCortexConfig = await request.clone().json();
const potentialError = validateGrafanaAlertmanagerConfig(body);
return potentialError ? potentialError : HttpResponse.json({ message: 'configuration created' });
@@ -95,6 +96,12 @@ const getReceiversHandler = () =>
return HttpResponse.json({ message: 'Not found.' }, { status: 404 });
});
+const testReceiversHandler = () =>
+ http.post('/api/alertmanager/grafana/config/api/v1/receivers/test', () => {
+ // TODO: scaffold out response as needed by tests
+ return HttpResponse.json({});
+ });
+
const getGroupsHandler = () =>
http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/alerts/groups', () =>
// TODO: Scaffold out response with better data as required by tests
@@ -110,6 +117,7 @@ const handlers = [
updateAlertmanagerConfigHandler(),
getGrafanaAlertmanagerTemplatePreview(),
getReceiversHandler(),
+ testReceiversHandler(),
getGroupsHandler(),
];
export default handlers;
diff --git a/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts b/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts
index 157ebddfba0..8983b0171b0 100644
--- a/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts
+++ b/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts
@@ -1,3 +1,4 @@
+import { camelCase } from 'lodash';
import { HttpResponse, http } from 'msw';
import alertmanagerConfig from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
@@ -16,7 +17,11 @@ const mappedReceivers =
return integration.provenance;
})?.provenance || PROVENANCE_NONE;
return {
- metadata: { annotations: { [PROVENANCE_ANNOTATION]: provenance } },
+ metadata: {
+ // This isn't exactly accurate, but its the cleanest way to use the same data for AM config and K8S responses
+ uid: camelCase(contactPoint.name),
+ annotations: { [PROVENANCE_ANNOTATION]: provenance },
+ },
spec: {
title: contactPoint.name,
integrations: contactPoint.grafana_managed_receiver_configs || [],
@@ -34,5 +39,31 @@ const listNamespacedReceiverHandler = () =>
return HttpResponse.json(parsedReceivers);
});
-const handlers = [listNamespacedReceiverHandler()];
+const createNamespacedReceiverHandler = () =>
+ http.post<{ namespace: string }>(
+ `${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers`,
+ async ({ request }) => {
+ const body = await request.json();
+ return HttpResponse.json(body);
+ }
+ );
+
+const deleteNamespacedReceiverHandler = () =>
+ http.delete<{ namespace: string; name: string }>(
+ `${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`,
+ ({ params }) => {
+ const { name } = params;
+ const matchedReceiver = parsedReceivers.items.find((receiver) => receiver.metadata.uid === name);
+ if (matchedReceiver) {
+ return HttpResponse.json(parsedReceivers);
+ }
+ return HttpResponse.json({}, { status: 404 });
+ }
+ );
+
+const handlers = [
+ listNamespacedReceiverHandler(),
+ createNamespacedReceiverHandler(),
+ deleteNamespacedReceiverHandler(),
+];
export default handlers;
diff --git a/public/app/features/alerting/unified/openapi/receiversApi.gen.ts b/public/app/features/alerting/unified/openapi/receiversApi.gen.ts
index 56f2d366d63..defe5c50adb 100644
--- a/public/app/features/alerting/unified/openapi/receiversApi.gen.ts
+++ b/public/app/features/alerting/unified/openapi/receiversApi.gen.ts
@@ -25,6 +25,56 @@ const injectedRtkApi = api
}),
providesTags: ['Receiver'],
}),
+ createNamespacedReceiver: build.mutation({
+ query: (queryArg) => ({
+ url: `/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/receivers`,
+ method: 'POST',
+ body: queryArg.comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver,
+ params: {
+ pretty: queryArg.pretty,
+ dryRun: queryArg.dryRun,
+ fieldManager: queryArg.fieldManager,
+ fieldValidation: queryArg.fieldValidation,
+ },
+ }),
+ invalidatesTags: ['Receiver'],
+ }),
+ readNamespacedReceiver: build.query({
+ query: (queryArg) => ({
+ url: `/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/receivers/${queryArg.name}`,
+ params: { pretty: queryArg.pretty },
+ }),
+ providesTags: ['Receiver'],
+ }),
+ replaceNamespacedReceiver: build.mutation({
+ query: (queryArg) => ({
+ url: `/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/receivers/${queryArg.name}`,
+ method: 'PUT',
+ body: queryArg.comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver,
+ params: {
+ pretty: queryArg.pretty,
+ dryRun: queryArg.dryRun,
+ fieldManager: queryArg.fieldManager,
+ fieldValidation: queryArg.fieldValidation,
+ },
+ }),
+ invalidatesTags: ['Receiver'],
+ }),
+ deleteNamespacedReceiver: build.mutation({
+ query: (queryArg) => ({
+ url: `/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/receivers/${queryArg.name}`,
+ method: 'DELETE',
+ body: queryArg.ioK8SApimachineryPkgApisMetaV1DeleteOptions,
+ params: {
+ pretty: queryArg.pretty,
+ dryRun: queryArg.dryRun,
+ gracePeriodSeconds: queryArg.gracePeriodSeconds,
+ orphanDependents: queryArg.orphanDependents,
+ propagationPolicy: queryArg.propagationPolicy,
+ },
+ }),
+ invalidatesTags: ['Receiver'],
+ }),
}),
overrideExisting: false,
});
@@ -39,7 +89,7 @@ export type ListNamespacedReceiverApiArg = {
/** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */
allowWatchBookmarks?: boolean;
/** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key".
-
+
This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */
continue?: string;
/** A selector to restrict the list of returned objects by their fields. Defaults to everything. */
@@ -47,19 +97,19 @@ export type ListNamespacedReceiverApiArg = {
/** A selector to restrict the list of returned objects by their labels. Defaults to everything. */
labelSelector?: string;
/** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.
-
+
The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */
limit?: number;
/** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.
-
+
Defaults to unset */
resourceVersion?: string;
/** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.
-
+
Defaults to unset */
resourceVersionMatch?: string;
/** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.
-
+
When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan
is interpreted as "data at least as new as the provided `resourceVersion`"
and the bookmark event is send when the state is synced
@@ -69,7 +119,7 @@ export type ListNamespacedReceiverApiArg = {
when request started being processed.
- `resourceVersionMatch` set to any other value or unset
Invalid error is returned.
-
+
Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */
sendInitialEvents?: boolean;
/** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */
@@ -77,6 +127,71 @@ export type ListNamespacedReceiverApiArg = {
/** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */
watch?: boolean;
};
+export type CreateNamespacedReceiverApiResponse = /** status 200 OK */
+ | ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver
+ | /** status 201 Created */ ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver
+ | /** status 202 Accepted */ ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;
+export type CreateNamespacedReceiverApiArg = {
+ /** object name and auth scope, such as for teams and projects */
+ namespace: string;
+ /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
+ pretty?: string;
+ /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
+ dryRun?: string;
+ /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */
+ fieldManager?: string;
+ /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
+ fieldValidation?: string;
+ comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;
+};
+export type ReadNamespacedReceiverApiResponse =
+ /** status 200 OK */ ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;
+export type ReadNamespacedReceiverApiArg = {
+ /** name of the Receiver */
+ name: string;
+ /** object name and auth scope, such as for teams and projects */
+ namespace: string;
+ /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
+ pretty?: string;
+};
+export type ReplaceNamespacedReceiverApiResponse = /** status 200 OK */
+ | ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver
+ | /** status 201 Created */ ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;
+export type ReplaceNamespacedReceiverApiArg = {
+ /** name of the Receiver */
+ name: string;
+ /** object name and auth scope, such as for teams and projects */
+ namespace: string;
+ /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
+ pretty?: string;
+ /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
+ dryRun?: string;
+ /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */
+ fieldManager?: string;
+ /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
+ fieldValidation?: string;
+ comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;
+};
+export type DeleteNamespacedReceiverApiResponse = /** status 200 OK */
+ | IoK8SApimachineryPkgApisMetaV1Status
+ | /** status 202 Accepted */ IoK8SApimachineryPkgApisMetaV1Status;
+export type DeleteNamespacedReceiverApiArg = {
+ /** name of the Receiver */
+ name: string;
+ /** object name and auth scope, such as for teams and projects */
+ namespace: string;
+ /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
+ pretty?: string;
+ /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
+ dryRun?: string;
+ /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */
+ gracePeriodSeconds?: number;
+ /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */
+ orphanDependents?: boolean;
+ /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */
+ propagationPolicy?: string;
+ ioK8SApimachineryPkgApisMetaV1DeleteOptions: IoK8SApimachineryPkgApisMetaV1DeleteOptions;
+};
export type IoK8SApimachineryPkgApisMetaV1Time = string;
export type IoK8SApimachineryPkgApisMetaV1FieldsV1 = object;
export type IoK8SApimachineryPkgApisMetaV1ManagedFieldsEntry = {
@@ -115,21 +230,21 @@ export type IoK8SApimachineryPkgApisMetaV1ObjectMeta = {
[key: string]: string;
};
/** CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.
-
+
Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */
creationTimestamp?: IoK8SApimachineryPkgApisMetaV1Time;
/** Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. */
deletionGracePeriodSeconds?: number;
/** DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.
-
+
Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */
deletionTimestamp?: IoK8SApimachineryPkgApisMetaV1Time;
/** Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list. */
finalizers?: string[];
/** GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.
-
+
If this field is specified and the generated name exists, the server will return a 409.
-
+
Applied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency */
generateName?: string;
/** A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. */
@@ -143,28 +258,29 @@ export type IoK8SApimachineryPkgApisMetaV1ObjectMeta = {
/** Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */
name?: string;
/** Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.
-
+
Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces */
namespace?: string;
/** List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. */
ownerReferences?: IoK8SApimachineryPkgApisMetaV1OwnerReference[];
/** An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.
-
+
Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */
resourceVersion?: string;
/** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */
selfLink?: string;
/** UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.
-
+
Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */
uid?: string;
};
+export type ComGithubGrafanaGrafanaPkgApimachineryApisCommonV0Alpha1Unstructured = {
+ [key: string]: any;
+};
export type ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Integration = {
- SecureFields?: {
- [key: string]: boolean;
- };
+ SecureFields?: ComGithubGrafanaGrafanaPkgApimachineryApisCommonV0Alpha1Unstructured;
disableResolveMessage?: boolean;
- settings: string;
+ settings: ComGithubGrafanaGrafanaPkgApimachineryApisCommonV0Alpha1Unstructured;
type: string;
uid?: string;
};
@@ -198,3 +314,69 @@ export type ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1ReceiverL
kind?: string;
metadata: IoK8SApimachineryPkgApisMetaV1ListMeta;
};
+export type IoK8SApimachineryPkgApisMetaV1StatusCause = {
+ /** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.
+
+ Examples:
+ "name" - the field "name" on the current resource
+ "items[0].name" - the field "name" on the first array entry in "items" */
+ field?: string;
+ /** A human-readable description of the cause of the error. This field may be presented as-is to a reader. */
+ message?: string;
+ /** A machine-readable description of the cause of the error. If this value is empty there is no information available. */
+ reason?: string;
+};
+export type IoK8SApimachineryPkgApisMetaV1StatusDetails = {
+ /** The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. */
+ causes?: IoK8SApimachineryPkgApisMetaV1StatusCause[];
+ /** The group attribute of the resource associated with the status StatusReason. */
+ group?: string;
+ /** The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
+ kind?: string;
+ /** The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described). */
+ name?: string;
+ /** If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action. */
+ retryAfterSeconds?: number;
+ /** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */
+ uid?: string;
+};
+export type IoK8SApimachineryPkgApisMetaV1Status = {
+ /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
+ apiVersion?: string;
+ /** Suggested HTTP return code for this status, 0 if not set. */
+ code?: number;
+ /** Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type. */
+ details?: IoK8SApimachineryPkgApisMetaV1StatusDetails;
+ /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
+ kind?: string;
+ /** A human-readable description of the status of this operation. */
+ message?: string;
+ /** Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
+ metadata?: IoK8SApimachineryPkgApisMetaV1ListMeta;
+ /** A machine-readable description of why this operation is in the "Failure" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. */
+ reason?: string;
+ /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */
+ status?: string;
+};
+export type IoK8SApimachineryPkgApisMetaV1Preconditions = {
+ /** Specifies the target ResourceVersion */
+ resourceVersion?: string;
+ /** Specifies the target UID. */
+ uid?: string;
+};
+export type IoK8SApimachineryPkgApisMetaV1DeleteOptions = {
+ /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
+ apiVersion?: string;
+ /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
+ dryRun?: string[];
+ /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */
+ gracePeriodSeconds?: number;
+ /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
+ kind?: string;
+ /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */
+ orphanDependents?: boolean;
+ /** Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned. */
+ preconditions?: IoK8SApimachineryPkgApisMetaV1Preconditions;
+ /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */
+ propagationPolicy?: string;
+};
diff --git a/public/app/features/alerting/unified/openapi/timeIntervalsApi.gen.ts b/public/app/features/alerting/unified/openapi/timeIntervalsApi.gen.ts
index 42eb90bf6ff..3a01ecf6886 100644
--- a/public/app/features/alerting/unified/openapi/timeIntervalsApi.gen.ts
+++ b/public/app/features/alerting/unified/openapi/timeIntervalsApi.gen.ts
@@ -77,24 +77,6 @@ const injectedRtkApi = api
}),
invalidatesTags: ['TimeInterval'],
}),
- patchNamespacedTimeInterval: build.mutation<
- PatchNamespacedTimeIntervalApiResponse,
- PatchNamespacedTimeIntervalApiArg
- >({
- query: (queryArg) => ({
- url: `/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/timeintervals/${queryArg.name}`,
- method: 'PATCH',
- body: queryArg.ioK8SApimachineryPkgApisMetaV1Patch,
- params: {
- pretty: queryArg.pretty,
- dryRun: queryArg.dryRun,
- fieldManager: queryArg.fieldManager,
- fieldValidation: queryArg.fieldValidation,
- force: queryArg.force,
- },
- }),
- invalidatesTags: ['TimeInterval'],
- }),
}),
overrideExisting: false,
});
@@ -202,26 +184,6 @@ export type DeleteNamespacedTimeIntervalApiArg = {
propagationPolicy?: string;
ioK8SApimachineryPkgApisMetaV1DeleteOptions: IoK8SApimachineryPkgApisMetaV1DeleteOptions;
};
-export type PatchNamespacedTimeIntervalApiResponse = /** status 200 OK */
- | ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval
- | /** status 201 Created */ ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval;
-export type PatchNamespacedTimeIntervalApiArg = {
- /** name of the TimeInterval */
- name: string;
- /** object name and auth scope, such as for teams and projects */
- namespace: string;
- /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
- pretty?: string;
- /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */
- dryRun?: string;
- /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */
- fieldManager?: string;
- /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */
- fieldValidation?: string;
- /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */
- force?: boolean;
- ioK8SApimachineryPkgApisMetaV1Patch: IoK8SApimachineryPkgApisMetaV1Patch;
-};
export type IoK8SApimachineryPkgApisMetaV1Time = string;
export type IoK8SApimachineryPkgApisMetaV1FieldsV1 = object;
export type IoK8SApimachineryPkgApisMetaV1ManagedFieldsEntry = {
@@ -412,4 +374,3 @@ export type IoK8SApimachineryPkgApisMetaV1DeleteOptions = {
/** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */
propagationPolicy?: string;
};
-export type IoK8SApimachineryPkgApisMetaV1Patch = object;
diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts
index 276c43a0bcc..c7dad4ca3b1 100644
--- a/public/app/features/alerting/unified/state/actions.ts
+++ b/public/app/features/alerting/unified/state/actions.ts
@@ -331,7 +331,14 @@ export const updateAlertManagerConfigAction = createAsyncThunk {
const featureToggleEnabled = config.featureToggles.alertingApiServer;
return featureToggleEnabled && alertmanager === GRAFANA_RULES_SOURCE_NAME;
};
+
+type Entity = {
+ metadata: {
+ annotations?: Record;
+ };
+};
+
+/**
+ * Check the metadata of a kubernetes entity and check if has the necessary annotations
+ * that denote it as provisioned
+ */
+export const isK8sEntityProvisioned = (item: Entity) =>
+ item.metadata.annotations?.[PROVENANCE_ANNOTATION] !== PROVENANCE_NONE;
diff --git a/public/app/features/alerting/unified/utils/receiver-form.ts b/public/app/features/alerting/unified/utils/receiver-form.ts
index 672fd3ac801..bc857d79d25 100644
--- a/public/app/features/alerting/unified/utils/receiver-form.ts
+++ b/public/app/features/alerting/unified/utils/receiver-form.ts
@@ -125,38 +125,39 @@ export function formValuesToCloudReceiver(
export function updateConfigWithReceiver(
config: AlertManagerCortexConfig,
receiver: Receiver,
- replaceReceiverName?: string
+ existingReceiverName?: string
): AlertManagerCortexConfig {
- const oldReceivers = config.alertmanager_config.receivers ?? [];
+ const existingReceivers = config.alertmanager_config.receivers ?? [];
+
+ const receiverWasRenamed = existingReceiverName && receiver.name !== existingReceiverName;
// sanity check that name is not duplicated
- if (receiver.name !== replaceReceiverName && !!oldReceivers.find(({ name }) => name === receiver.name)) {
+ if (!existingReceiverName && !!existingReceivers.find(({ name }) => name === receiver.name)) {
throw new Error(`Duplicate receiver name ${receiver.name}`);
}
// sanity check that existing receiver exists
- if (replaceReceiverName && !oldReceivers.find(({ name }) => name === replaceReceiverName)) {
- throw new Error(`Expected receiver ${replaceReceiverName} to exist, but did not find it in the config`);
+ if (existingReceiverName && !existingReceivers.find(({ name }) => name === existingReceiverName)) {
+ throw new Error(`Expected receiver ${existingReceiverName} to exist, but did not find it in the config`);
}
const updated: AlertManagerCortexConfig = {
...config,
alertmanager_config: {
- // @todo rename receiver on routes as necessary
...config.alertmanager_config,
- receivers: replaceReceiverName
- ? oldReceivers.map((existingReceiver) =>
- existingReceiver.name === replaceReceiverName ? receiver : existingReceiver
+ receivers: existingReceiverName
+ ? existingReceivers.map((existingReceiver) =>
+ existingReceiver.name === existingReceiverName ? receiver : existingReceiver
)
- : [...oldReceivers, receiver],
+ : [...existingReceivers, receiver],
},
};
// if receiver was renamed, rename it in routes as well
- if (updated.alertmanager_config.route && replaceReceiverName && receiver.name !== replaceReceiverName) {
+ if (updated.alertmanager_config.route && receiverWasRenamed) {
updated.alertmanager_config.route = renameReceiverInRoute(
updated.alertmanager_config.route,
- replaceReceiverName,
+ existingReceiverName,
receiver.name
);
}
diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts
index fe4aa16b4b4..cc4a7af1af9 100644
--- a/public/app/features/alerting/unified/utils/rules.ts
+++ b/public/app/features/alerting/unified/utils/rules.ts
@@ -44,7 +44,7 @@ import { RULER_NOT_SUPPORTED_MSG } from './constants';
import { getRulesSourceName, isGrafanaRulesSource } from './datasource';
import { GRAFANA_ORIGIN_LABEL } from './labels';
import { AsyncRequestState } from './redux';
-import { safeParsePrometheusDuration } from './time';
+import { formatPrometheusDuration, safeParsePrometheusDuration } from './time';
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
@@ -137,6 +137,26 @@ export function getRuleHealth(health: string): RuleHealth | undefined {
}
}
+export function getPendingPeriod(rule: CombinedRule): string | undefined {
+ if (isRecordingRulerRule(rule.rulerRule) || isRecordingRule(rule.promRule)) {
+ return undefined;
+ }
+
+ // We prefer the for duration from the ruler rule because it is formatted as a duration string
+ // Prometheus duration is in seconds and we need to format it as a duration string
+ // Additionally, due to eventual consistency of the Prometheus endpoint the ruler data might be newer
+ if (isAlertingRulerRule(rule.rulerRule)) {
+ return rule.rulerRule.for;
+ }
+
+ if (isAlertingRule(rule.promRule)) {
+ const durationInMilliseconds = (rule.promRule.duration ?? 0) * 1000;
+ return formatPrometheusDuration(durationInMilliseconds);
+ }
+
+ return undefined;
+}
+
export interface RulePluginOrigin {
pluginId: string;
}
diff --git a/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx b/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx
index 042e126eafd..d186d4a2613 100644
--- a/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx
+++ b/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx
@@ -17,7 +17,7 @@ export const MigrateToServiceAccountsCard = ({ onMigrate, apikeysCount, disabled
const docsLink = (
diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx
index 1b9eeded9e4..7c79f5798cc 100644
--- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx
@@ -4,7 +4,14 @@ import { DataQueryRequest, DataSourceApi, DataSourceInstanceSettings, LoadingSta
import { calculateFieldTransformer } from '@grafana/data/src/transformations/transformers/calculateField';
import { mockTransformationsRegistry } from '@grafana/data/src/utils/tests/mockTransformationsRegistry';
import { config, locationService } from '@grafana/runtime';
-import { LocalValueVariable, SceneGridRow, SceneVariableSet, VizPanel, sceneGraph } from '@grafana/scenes';
+import {
+ CustomVariable,
+ LocalValueVariable,
+ SceneGridRow,
+ SceneVariableSet,
+ VizPanel,
+ sceneGraph,
+} from '@grafana/scenes';
import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { InspectTab } from 'app/features/inspector/types';
@@ -801,6 +808,26 @@ describe('VizPanelManager', () => {
expect(vizPanelManager.state.dsSettings).toEqual(instance1SettingsMock);
});
+ it('Should default to the first variable value if panel is repeated', async () => {
+ const { scene, panel } = setupTest('panel-10');
+
+ scene.setState({
+ $variables: new SceneVariableSet({
+ variables: [
+ new CustomVariable({ name: 'custom', query: 'A,B,C', value: ['A', 'B', 'C'], text: ['A', 'B', 'C'] }),
+ ],
+ }),
+ });
+
+ scene.setState({ editPanel: buildPanelEditScene(panel) });
+
+ const vizPanelManager = scene.state.editPanel!.state.vizManager;
+ vizPanelManager.activate();
+
+ const variable = sceneGraph.lookupVariable('custom', vizPanelManager);
+ expect(variable?.getValue()).toBe('A');
+ });
+
describe('Given a panel inside repeated row', () => {
it('Should include row variable scope', () => {
const { panel } = setupTest('panel-9');
diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx
index 3e8721302ca..6878fd5e569 100644
--- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx
@@ -15,6 +15,8 @@ import {
import { config, getDataSourceSrv, locationService } from '@grafana/runtime';
import {
DeepPartial,
+ LocalValueVariable,
+ MultiValueVariable,
PanelBuilders,
SceneComponentProps,
SceneDataTransformer,
@@ -23,8 +25,10 @@ import {
SceneObjectState,
SceneObjectStateChangedEvent,
SceneQueryRunner,
+ SceneVariableSet,
SceneVariables,
VizPanel,
+ sceneGraph,
} from '@grafana/scenes';
import { DataQuery, DataTransformerConfig, Panel } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
@@ -41,7 +45,7 @@ import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
-import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
+import { getDashboardSceneFor, getMultiVariableValues, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
export interface VizPanelManagerState extends SceneObjectState {
panel: VizPanel;
@@ -97,6 +101,28 @@ export class VizPanelManager extends SceneObjectBase {
variables = gridItem.parent.state.$variables.clone();
}
+ if (repeatOptions.repeat) {
+ const variable = sceneGraph.lookupVariable(repeatOptions.repeat, gridItem);
+
+ if (variable instanceof MultiValueVariable && variable.state.value.length) {
+ const { values, texts } = getMultiVariableValues(variable);
+
+ const varWithDefaultValue = new LocalValueVariable({
+ name: variable.state.name,
+ value: values[0],
+ text: String(texts[0]),
+ });
+
+ if (!variables) {
+ variables = new SceneVariableSet({
+ variables: [varWithDefaultValue],
+ });
+ } else {
+ variables.setState({ variables: [varWithDefaultValue] });
+ }
+ }
+ }
+
return new VizPanelManager({
$variables: variables,
panel: sourcePanel.clone(),
diff --git a/public/app/features/dashboard-scene/panel-edit/testfiles/testDashboard.ts b/public/app/features/dashboard-scene/panel-edit/testfiles/testDashboard.ts
index a439bb2541d..f596a026b25 100644
--- a/public/app/features/dashboard-scene/panel-edit/testfiles/testDashboard.ts
+++ b/public/app/features/dashboard-scene/panel-edit/testfiles/testDashboard.ts
@@ -91,6 +91,102 @@ export const panelWithQueriesOnly = {
type: 'timeseries',
};
+export const repeatedPanel = {
+ datasource: {
+ type: 'grafana-testdata-datasource',
+ uid: 'gdev-testdata',
+ },
+ repeat: 'custom',
+ repeatDirection: 'h',
+ maxPerRow: 4,
+ fieldConfig: {
+ defaults: {
+ color: {
+ mode: 'palette-classic',
+ },
+ custom: {
+ axisBorderShow: false,
+ axisCenteredZero: false,
+ axisColorMode: 'text',
+ axisLabel: '',
+ axisPlacement: 'auto',
+ barAlignment: 0,
+ drawStyle: 'line',
+ fillOpacity: 0,
+ gradientMode: 'none',
+ hideFrom: {
+ legend: false,
+ tooltip: false,
+ viz: false,
+ },
+ insertNulls: false,
+ lineInterpolation: 'linear',
+ lineWidth: 1,
+ pointSize: 5,
+ scaleDistribution: {
+ type: 'linear',
+ },
+ showPoints: 'auto',
+ spanNulls: false,
+ stacking: {
+ group: 'A',
+ mode: 'none',
+ },
+ thresholdsStyle: {
+ mode: 'off',
+ },
+ },
+ mappings: [],
+ thresholds: {
+ mode: 'absolute',
+ steps: [
+ {
+ color: 'green',
+ value: null,
+ },
+ {
+ color: 'red',
+ value: 80,
+ },
+ ],
+ },
+ },
+ overrides: [],
+ },
+ gridPos: {
+ h: 8,
+ w: 12,
+ x: 0,
+ y: 0,
+ },
+ id: 1,
+ options: {
+ legend: {
+ calcs: [],
+ displayMode: 'list',
+ placement: 'bottom',
+ showLegend: true,
+ },
+ tooltip: {
+ mode: 'single',
+ sort: 'none',
+ },
+ },
+ targets: [
+ {
+ datasource: {
+ type: 'grafana-testdata-datasource',
+ uid: 'gdev-testdata',
+ },
+ refId: 'A',
+ scenarioId: 'random_walk',
+ seriesCount: 1,
+ },
+ ],
+ title: 'Panel with just queries',
+ type: 'timeseries',
+};
+
export const panelWithTransformations = {
datasource: {
type: 'grafana-testdata-datasource',
@@ -636,6 +732,7 @@ export const testDashboard = {
panelWithQueriesAndMixedDatasource,
row,
rowChild,
+ repeatedPanel,
],
refresh: '',
schemaVersion: 39,
diff --git a/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx b/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx
index e56fe084dcb..b09427095b3 100644
--- a/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx
@@ -1,3 +1,4 @@
+import { VariableRefresh } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils } from '@grafana/runtime';
import { SceneGridLayout, VizPanel } from '@grafana/scenes';
@@ -11,6 +12,12 @@ setPluginImportUtils({
getPanelPluginFromCache: (id: string) => undefined,
});
+const mockGetQueryRunnerFor = jest.fn();
+jest.mock('../utils/utils', () => ({
+ ...jest.requireActual('../utils/utils'),
+ getQueryRunnerFor: jest.fn().mockImplementation(() => mockGetQueryRunnerFor()),
+}));
+
describe('PanelRepeaterGridItem', () => {
it('Given scene with variable with 2 values', async () => {
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 });
@@ -40,6 +47,32 @@ describe('PanelRepeaterGridItem', () => {
expect(repeater.state.repeatedPanels?.length).toBe(5);
});
+ it('Should update panels on refresh if variables load on time range change', async () => {
+ const { scene, repeater } = buildPanelRepeaterScene({
+ variableQueryTime: 0,
+ variableRefresh: VariableRefresh.onTimeRangeChanged,
+ });
+
+ const notifyPanelsSpy = jest.spyOn(repeater, 'notifyRepeatedPanelsWaitingForVariables');
+
+ activateFullSceneTree(scene);
+
+ expect(repeater.state.repeatedPanels?.length).toBe(5);
+
+ expect(notifyPanelsSpy).toHaveBeenCalledTimes(0);
+
+ scene.state.$timeRange?.onRefresh();
+
+ //make sure notifier is called
+ expect(notifyPanelsSpy).toHaveBeenCalledTimes(1);
+
+ //make sure getQueryRunner is called for each repeated panel
+ expect(mockGetQueryRunnerFor).toHaveBeenCalledTimes(5);
+
+ notifyPanelsSpy.mockRestore();
+ mockGetQueryRunnerFor.mockClear();
+ });
+
it('Should display a panel when there are no options', async () => {
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 1, numberOfOptions: 0 });
diff --git a/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx
index d6d66259527..670a57b9f98 100644
--- a/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx
@@ -7,7 +7,6 @@ import { config } from '@grafana/runtime';
import {
VizPanel,
SceneObjectBase,
- VariableDependencyConfig,
SceneGridLayout,
SceneVariableSet,
SceneComponentProps,
@@ -20,10 +19,12 @@ import {
VizPanelMenu,
VizPanelState,
VariableValueSingle,
+ SceneVariable,
+ SceneVariableDependencyConfigLike,
} from '@grafana/scenes';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
-import { getMultiVariableValues } from '../utils/utils';
+import { getMultiVariableValues, getQueryRunnerFor } from '../utils/utils';
import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer';
import { LibraryVizPanel } from './LibraryVizPanel';
@@ -45,10 +46,7 @@ export class DashboardGridItem extends SceneObjectBase i
private _libPanelSubscription: Unsubscribable | undefined;
private _prevRepeatValues?: VariableValueSingle[];
- protected _variableDependency = new VariableDependencyConfig(this, {
- variableNames: this.state.variableName ? [this.state.variableName] : [],
- onVariableUpdateCompleted: this._onVariableUpdateCompleted.bind(this),
- });
+ protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
public constructor(state: DashboardGridItemState) {
super(state);
@@ -59,7 +57,7 @@ export class DashboardGridItem extends SceneObjectBase i
private _activationHandler() {
if (this.state.variableName) {
this._subs.add(this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState)));
- this._performRepeat();
+ this.performRepeat();
}
// Subscriptions that handles body updates, i.e. VizPanel -> LibraryVizPanel, AddLibPanelWidget -> LibraryVizPanel
@@ -92,23 +90,16 @@ export class DashboardGridItem extends SceneObjectBase i
this._libPanelSubscription = panel.subscribeToState((newState) => {
if (newState._loadedPanel?.model.repeat) {
- this._variableDependency.setVariableNames([newState._loadedPanel.model.repeat]);
this.setState({
variableName: newState._loadedPanel.model.repeat,
repeatDirection: newState._loadedPanel.model.repeatDirection,
maxPerRow: newState._loadedPanel.model.maxPerRow,
});
- this._performRepeat();
+ this.performRepeat();
}
});
}
- private _onVariableUpdateCompleted(): void {
- if (this.state.variableName) {
- this._performRepeat();
- }
- }
-
/**
* Uses the current repeat item count to calculate the user intended desired itemHeight
*/
@@ -134,11 +125,12 @@ export class DashboardGridItem extends SceneObjectBase i
}
}
- private _performRepeat() {
+ public performRepeat() {
if (this.state.body instanceof AddLibraryPanelDrawer) {
return;
}
- if (!this.state.variableName || this._variableDependency.hasDependencyInLoadingState()) {
+
+ if (!this.state.variableName || sceneGraph.hasVariableDependencyInLoadingState(this)) {
return;
}
@@ -160,6 +152,9 @@ export class DashboardGridItem extends SceneObjectBase i
const { values, texts } = getMultiVariableValues(variable);
if (isEqual(this._prevRepeatValues, values)) {
+ // In some cases, like for variables that depend on time range, the panel query runners are waiting for the top level variable to complete
+ // So even when there was no change in the variable value (like in this case) we need to notify the query runners that the variable has completed it's update
+ this.notifyRepeatedPanelsWaitingForVariables(variable);
return;
}
@@ -227,6 +222,15 @@ export class DashboardGridItem extends SceneObjectBase i
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
}
+ public notifyRepeatedPanelsWaitingForVariables(variable: SceneVariable) {
+ for (const panel of this.state.repeatedPanels ?? []) {
+ const queryRunner = getQueryRunnerFor(panel);
+ if (queryRunner) {
+ queryRunner.variableDependency?.variableUpdateCompleted(variable, false);
+ }
+ }
+ }
+
public getMaxPerRow(): number {
return this.state.maxPerRow ?? 4;
}
@@ -278,6 +282,34 @@ export class DashboardGridItem extends SceneObjectBase i
};
}
+export class DashboardGridItemVariableDependencyHandler implements SceneVariableDependencyConfigLike {
+ constructor(private _gridItem: DashboardGridItem) {}
+
+ getNames(): Set {
+ if (this._gridItem.state.variableName) {
+ return new Set([this._gridItem.state.variableName]);
+ }
+
+ return new Set();
+ }
+
+ hasDependencyOn(name: string): boolean {
+ return this._gridItem.state.variableName === name;
+ }
+
+ variableUpdateCompleted(variable: SceneVariable, hasChanged: boolean): void {
+ if (this._gridItem.state.variableName === variable.state.name) {
+ /**
+ * We do not really care if the variable has changed or not as we do an equality check in performRepeat
+ * And this function needs to be called even when variable valued id not change as performRepeat calls
+ * notifyRepeatedPanelsWaitingForVariables which is needed to notify panels waiting for variable to complete (even when the value did not change)
+ * This is for scenarios where the variable used for repeating is depending on time range.
+ */
+ this._gridItem.performRepeat();
+ }
+ }
+}
+
function useLayoutStyle(direction: RepeatDirection, itemCount: number, maxPerRow: number, itemHeight: number) {
return useMemo(() => {
const theme = config.theme2;
diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts
index c1a7172044f..276c82e9d5c 100644
--- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts
+++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts
@@ -1,10 +1,11 @@
import { AppEvents } from '@grafana/data';
-import { SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
+import { SceneGridLayout, SceneGridRow, SceneQueryRunner, VizPanel } from '@grafana/scenes';
import appEvents from 'app/core/app_events';
import { KioskMode } from 'app/types';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene';
+import { RowRepeaterBehavior } from './RowRepeaterBehavior';
import { DashboardRepeatsProcessedEvent } from './types';
describe('DashboardSceneUrlSync', () => {
@@ -108,6 +109,35 @@ describe('DashboardSceneUrlSync', () => {
scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene }));
expect(scene.state.viewPanelScene?.getUrlKey()).toBe('panel-1-clone-1');
});
+
+ it('should subscribe and update view panel if panel is in a repeated row', () => {
+ const scene = buildTestScene();
+
+ // fake adding row panel
+ const layout = scene.state.body as SceneGridLayout;
+ layout.setState({
+ children: [
+ new SceneGridRow({
+ $behaviors: [new RowRepeaterBehavior({ variableName: 'test' })],
+ children: [
+ new VizPanel({
+ title: 'Panel A',
+ key: 'panel-1',
+ pluginId: 'table',
+ }),
+ ],
+ }),
+ ],
+ });
+
+ scene.urlSync?.updateFromUrl({ viewPanel: 'panel-1' });
+
+ expect(scene.state.viewPanelScene?.getUrlKey()).toBeUndefined();
+
+ // Verify it subscribes to DashboardRepeatsProcessedEvent
+ scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene }));
+ expect(scene.state.viewPanelScene?.getUrlKey()).toBe('panel-1');
+ });
});
function buildTestScene() {
diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts
index 16189017918..a169d05a60f 100644
--- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts
+++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts
@@ -18,7 +18,13 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { createDashboardEditViewFor } from '../settings/utils';
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
import { ShareModal } from '../sharing/ShareModal';
-import { findVizPanelByKey, getDashboardSceneFor, getLibraryPanel, isPanelClone } from '../utils/utils';
+import {
+ findVizPanelByKey,
+ getDashboardSceneFor,
+ getLibraryPanel,
+ isPanelClone,
+ isWithinUnactivatedRepeatRow,
+} from '../utils/utils';
import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
@@ -121,6 +127,11 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
return;
}
+ if (isWithinUnactivatedRepeatRow(panel)) {
+ this._handleViewRepeatClone(values.viewPanel);
+ return;
+ }
+
update.viewPanelScene = new ViewPanelScene({ panelRef: panel.getRef() });
} else if (viewPanelScene && values.viewPanel === null) {
update.viewPanelScene = undefined;
diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx
index 19878903d7d..bddb841cc14 100644
--- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx
+++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx
@@ -67,6 +67,42 @@ describe('RowRepeaterBehavior', () => {
expect(gridItem.state.body?.state.key).toBe('canvas-1-clone-B1');
});
+ it('Should update all rows when a panel is added to a clone', async () => {
+ const originalRow = grid.state.children[1] as SceneGridRow;
+ const clone1 = grid.state.children[2] as SceneGridRow;
+ const clone2 = grid.state.children[3] as SceneGridRow;
+
+ expect(originalRow.state.children.length).toBe(1);
+ expect(clone1.state.children.length).toBe(1);
+ expect(clone2.state.children.length).toBe(1);
+
+ clone1.setState({
+ children: [
+ ...clone1.state.children,
+ new SceneGridItem({
+ x: 0,
+ y: 16,
+ width: 24,
+ height: 5,
+ key: 'griditem-4',
+ body: new SceneCanvasText({
+ text: 'new panel',
+ }),
+ }),
+ ],
+ });
+
+ grid.forceRender();
+
+ // repeater has run so there are new clone row objects
+ const newClone1 = grid.state.children[2] as SceneGridRow;
+ const newClone2 = grid.state.children[3] as SceneGridRow;
+
+ expect(originalRow.state.children.length).toBe(2);
+ expect(newClone1.state.children.length).toBe(2);
+ expect(newClone2.state.children.length).toBe(2);
+ });
+
it('Should push row at the bottom down', () => {
// Should push row at the bottom down
const rowAtTheBottom = grid.state.children[6] as SceneGridRow;
diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts
index 12d11116678..663315e5d04 100644
--- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts
+++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts
@@ -46,6 +46,48 @@ export class RowRepeaterBehavior extends SceneObjectBase {
+ const repeatedRows = layout.state.children.filter(
+ (child) => child instanceof SceneGridRow && child.state.key?.includes(filterKey)
+ );
+
+ // go through cloned rows, search for panels that are not clones
+ for (const row of repeatedRows) {
+ if (!(row instanceof SceneGridRow)) {
+ continue;
+ }
+
+ // if no differences in row children compared to original, then no new panel added to clone
+ if (row.state.children.length === originalRow.state.children.length) {
+ continue;
+ }
+
+ //if there are differences, find the new panel, move it to the original and perform re peat
+ const gridItem = row.state.children.find((gridItem) => !gridItem.state.key?.includes('clone'));
+ if (gridItem) {
+ const newGridItem = gridItem.clone();
+ row.setState({ children: row.state.children.filter((item) => item !== gridItem) });
+
+ // if we are moving a panel from the origin row to a clone row, we just return
+ // this means we are modifying the origin row, retriggering the repeat and losing that panel
+ if (originalRow.state.children.find((item) => item.state.key === newGridItem.state.key)) {
+ return;
+ }
+
+ originalRow.setState({ children: [...originalRow.state.children, newGridItem] });
+ this.performRepeat(true);
+ }
+ }
+ });
+
+ return () => {
+ sub.unsubscribe();
+ };
}
private _getRow(): SceneGridRow {
@@ -66,7 +108,7 @@ export class RowRepeaterBehavior extends SceneObjectBase 0) {
ensureUniqueKeys(itemClone, localValue);
+ //disallow clones to be dragged around or out of the row
if (itemClone instanceof DashboardGridItem) {
+ itemClone.setState({
+ isDraggable: false,
+ });
+
itemClone.state.body.setState({
menu: new VizPanelMenu({
$behaviors: [repeatPanelMenuBehavior],
diff --git a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts
index dbbf225c129..00150b02a0a 100644
--- a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts
+++ b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts
@@ -1,4 +1,5 @@
import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data';
+import { config, getDataSourceSrv } from '@grafana/runtime';
import { AdHocFiltersVariable, dataLayers, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { AdHocFilterItem, PanelContext } from '@grafana/ui';
@@ -160,7 +161,9 @@ export function getAdHocFilterVariableFor(scene: DashboardScene, ds: DataSourceR
const newVariable = new AdHocFiltersVariable({
name: 'Filters',
datasource: ds,
+ supportsMultiValueOperators: Boolean(getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators),
useQueriesAsFilterForOptions: true,
+ layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined,
});
// Add it to the scene
diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
index 9b02fcc2f6a..042d2cd2ce7 100644
--- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
@@ -136,6 +136,8 @@ jest.mock('@grafana/runtime', () => ({
toDataQuery: (q: StandardVariableQuery) => q,
},
}),
+ // mock getInstanceSettings()
+ getInstanceSettings: jest.fn(),
}),
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
return runRequestMock(ds, request);
diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx
index 105f747a867..d464023a07a 100644
--- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx
+++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx
@@ -1,7 +1,18 @@
import { map, of } from 'rxjs';
+import { MockDataSourceApi } from 'test/mocks/datasource_srv';
-import { AnnotationQuery, DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data';
+import {
+ AnnotationQuery,
+ DataQueryRequest,
+ DataSourceApi,
+ DataSourceInstanceSettings,
+ DataSourcePluginMeta,
+ getDataSourceUID,
+ LoadingState,
+ PanelData,
+} from '@grafana/data';
import { SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes';
+import { DataSourceRef } from '@grafana/schema';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
@@ -29,11 +40,49 @@ const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request:
);
});
+const noAnnotationsDsInstanceSettings = {
+ name: 'noAnnotationsDs',
+ uid: 'noAnnotationsDs',
+ meta: {
+ annotations: false,
+ } as DataSourcePluginMeta,
+ readOnly: false,
+ type: 'noAnnotations',
+} as DataSourceInstanceSettings;
+
+const grafanaDsInstanceSettings = {
+ name: 'Grafana',
+ uid: '-- Grafana --',
+ meta: {
+ annotations: true,
+ } as DataSourcePluginMeta,
+ readOnly: false,
+ type: 'grafana',
+} as DataSourceInstanceSettings;
+
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => {
return {
- getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
+ //return default datasource when no ref is provided
+ getInstanceSettings: (ref: DataSourceRef | string | null) => {
+ if (!ref) {
+ return new MockDataSourceApi(noAnnotationsDsInstanceSettings);
+ }
+ if (getDataSourceUID(ref) === '-- Grafana --') {
+ return new MockDataSourceApi(grafanaDsInstanceSettings);
+ }
+ return jest.fn().mockResolvedValue({ uid: 'ds1' });
+ },
+ get: (ref: DataSourceRef) => {
+ if (getDataSourceUID(ref) === 'noAnnotationsDs') {
+ return Promise.resolve(new MockDataSourceApi(noAnnotationsDsInstanceSettings));
+ }
+ if (getDataSourceUID(ref) === '-- Grafana --') {
+ return Promise.resolve(new MockDataSourceApi(grafanaDsInstanceSettings));
+ }
+ return Promise.resolve(new MockDataSourceApi('ds1'));
+ },
};
},
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
@@ -52,17 +101,23 @@ describe('AnnotationsEditView', () => {
beforeEach(async () => {
const result = await buildTestScene();
annotationsView = result.annotationsView;
+ jest.spyOn(console, 'error').mockImplementation();
});
it('should return the correct urlKey', () => {
expect(annotationsView.getUrlKey()).toBe('annotations');
});
+ it('should return undefined when datasource does not support annotations', () => {
+ const ds = annotationsView.getDataSourceRefForAnnotation();
+ expect(ds).toBe(undefined);
+ expect(console.error).toHaveBeenCalledWith('Default datasource does not support annotations');
+ });
+
it('should add a new annotation and group it with the other annotations', () => {
const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard());
expect(dataLayers?.state.annotationLayers.length).toBe(1);
-
annotationsView.onNew();
expect(dataLayers?.state.annotationLayers.length).toBe(2);
diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx
index 40d836c7148..72510de5130 100644
--- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx
+++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx
@@ -1,4 +1,4 @@
-import { AnnotationQuery, NavModel, NavModelItem, PageLayoutType, getDataSourceRef } from '@grafana/data';
+import { AnnotationQuery, getDataSourceRef, NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, VizPanel, dataLayers } from '@grafana/scenes';
import { Page } from 'app/core/components/Page/Page';
@@ -51,11 +51,23 @@ export class AnnotationsEditView extends SceneObjectBase {
+ public getDataSourceRefForAnnotation = () => {
+ // get current default datasource ref from instance settings
+ // null is passed to get the default datasource
+ const defaultInstanceDS = getDataSourceSrv().getInstanceSettings(null);
+ // check for an annotation flag in the plugin json to see if it supports annotations
+ if (!defaultInstanceDS || !defaultInstanceDS.meta.annotations) {
+ console.error('Default datasource does not support annotations');
+ return undefined;
+ }
+ return getDataSourceRef(defaultInstanceDS);
+ };
+
+ public onNew = async () => {
const newAnnotationQuery: AnnotationQuery = {
name: newAnnotationName,
enable: true,
- datasource: getDataSourceRef(getDataSourceSrv().getInstanceSettings(null)!),
+ datasource: this.getDataSourceRefForAnnotation(),
iconColor: 'red',
};
@@ -69,7 +81,6 @@ export class AnnotationsEditView extends SceneObjectBase
+ {!ds?.meta.annotations && (
+
+
+ The selected data source does not support annotations. Please select a different data source.
+
+
+ )}
b instanceof RowRepeaterBehavior));
+ const hasVariables = !!(row.state.$variables && row.state.$variables.state.variables.length > 0);
+
+ return hasBehavior && !hasVariables;
+}
diff --git a/public/app/features/dashboard-scene/utils/variables.test.ts b/public/app/features/dashboard-scene/utils/variables.test.ts
index cc97261c95b..0f3616b2f75 100644
--- a/public/app/features/dashboard-scene/utils/variables.test.ts
+++ b/public/app/features/dashboard-scene/utils/variables.test.ts
@@ -26,6 +26,14 @@ import { NEW_LINK } from '../settings/links/utils';
import { createSceneVariableFromVariableModel, createVariablesForSnapshot } from './variables';
+// mock getDataSourceSrv.getInstanceSettings()
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ getDataSourceSrv: () => ({
+ getInstanceSettings: jest.fn(),
+ }),
+}));
+
describe('when creating variables objects', () => {
it('should migrate custom variable', () => {
const variable: CustomVariableModel = {
@@ -425,6 +433,7 @@ describe('when creating variables objects', () => {
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
applyMode: 'auto',
useQueriesAsFilterForOptions: true,
+ supportsMultiValueOperators: false,
});
});
@@ -508,6 +517,7 @@ describe('when creating variables objects', () => {
},
],
useQueriesAsFilterForOptions: true,
+ supportsMultiValueOperators: false,
});
});
diff --git a/public/app/features/dashboard-scene/utils/variables.ts b/public/app/features/dashboard-scene/utils/variables.ts
index 3eefdc84fc3..7421ce979fa 100644
--- a/public/app/features/dashboard-scene/utils/variables.ts
+++ b/public/app/features/dashboard-scene/utils/variables.ts
@@ -1,5 +1,5 @@
import { TypedVariableModel } from '@grafana/data';
-import { config } from '@grafana/runtime';
+import { config, getDataSourceSrv } from '@grafana/runtime';
import {
AdHocFiltersVariable,
ConstantVariable,
@@ -56,6 +56,10 @@ export function createVariablesForSnapshot(oldModel: DashboardModel) {
baseFilters: v.baseFilters ?? [],
defaultKeys: v.defaultKeys,
useQueriesAsFilterForOptions: true,
+ layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined,
+ supportsMultiValueOperators: Boolean(
+ getDataSourceSrv().getInstanceSettings(v.datasource)?.meta.multiValueFilterOperators
+ ),
});
}
// for other variable types we are using the SnapshotVariable
@@ -133,6 +137,10 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
baseFilters: variable.baseFilters ?? [],
defaultKeys: variable.defaultKeys,
useQueriesAsFilterForOptions: true,
+ layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined,
+ supportsMultiValueOperators: Boolean(
+ getDataSourceSrv().getInstanceSettings(variable.datasource)?.meta.multiValueFilterOperators
+ ),
});
}
if (variable.type === 'custom') {
diff --git a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx
index 645d74a6ec4..6349e5ccd18 100644
--- a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx
+++ b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx
@@ -24,9 +24,11 @@ import {
Select,
useStyles2,
Stack,
+ Alert,
} from '@grafana/ui';
import { ColorValueEditor } from 'app/core/components/OptionsUI/color';
import config from 'app/core/config';
+import { Trans } from 'app/core/internationalization';
import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor';
import { AngularEditorLoader } from 'app/features/dashboard-scene/settings/annotations/AngularEditorLoader';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
@@ -189,6 +191,13 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => {
+ {!ds?.meta.annotations && (
+
+
+ The selected data source does not support annotations. Please select a different data source.
+
+
+ )}
diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx
index 4e178bfe8a6..7a1f01125f0 100644
--- a/public/app/features/dashboard/containers/DashboardPage.test.tsx
+++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx
@@ -1,7 +1,6 @@
import { screen, waitFor } from '@testing-library/react';
import { KBarProvider } from 'kbar';
import { Component } from 'react';
-import { match } from 'react-router-dom';
import { useEffectOnce } from 'react-use';
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { render } from 'test/test-utils';
@@ -101,7 +100,7 @@ function setup(propOverrides?: Partial) {
const props: Props = {
...getRouteComponentProps({
- match: { params: { slug: 'my-dash', uid: '11' } } as unknown as match,
+ match: { params: { slug: 'my-dash', uid: '11' }, isExact: false, path: '', url: '' },
route: { routeName: DashboardRoutes.Normal } as RouteDescriptor,
}),
navIndex: {
@@ -271,7 +270,7 @@ describe('DashboardPage', () => {
const { rerender } = setup();
rerender({ dashboard: getTestDashboard() });
rerender({
- match: { params: { uid: 'new-uid' } } as unknown as match,
+ match: { params: { uid: 'new-uid' }, isExact: false, path: '', url: '' },
dashboard: getTestDashboard({ title: 'Another dashboard' }),
});
await waitFor(() => {
diff --git a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx
index 97932973433..29753f6275a 100644
--- a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx
+++ b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx
@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import { Component } from 'react';
-import { match } from 'react-router-dom';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { Dashboard } from '@grafana/schema';
@@ -75,7 +74,10 @@ function soloPanelPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
...getRouteComponentProps({
match: {
params: { slug: 'my-dash', uid: '11' },
- } as unknown as match,
+ isExact: false,
+ path: '',
+ url: '',
+ },
queryParams: {
panelId: '1',
},
diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx
index 69fd9176a8e..a204d4f4278 100644
--- a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx
+++ b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx
@@ -20,12 +20,15 @@ export const getStyles = (theme: GrafanaTheme2) => {
gap: '4px',
marginBottom: '10px',
}),
+ feedbackContainer: css({
+ color: theme.colors.text.link,
+ }),
feedback: css({
- margin: '6px 6px 6px 0',
- color: theme.colors.text.secondary,
+ margin: '6px',
+ color: theme.colors.text.link,
fontSize: theme.typography.bodySmall.fontSize,
'&:hover': {
- color: theme.colors.text.link,
+ textDecoration: 'underline',
},
}),
};
@@ -63,15 +66,18 @@ export default function TracePageActions(props: TracePageActionsProps) {
return (