mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'main' into drclau/unistor/replace-authenticators-3
This commit is contained in:
commit
884c4a8c24
@ -1283,14 +1283,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/core/navigation/testRoutes.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "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 <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/receivers/EditReceiverView.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/receivers/EditTemplateView.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
@ -4790,11 +4779,7 @@ exports[`better eslint`] = {
|
||||
"public/app/features/playlist/PlaylistTableRows.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "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 <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/playlist/StartModal.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "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"],
|
||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
634
.github/commands.json
vendored
634
.github/commands.json
vendored
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
2
.github/issue-opened.json
vendored
2
.github/issue-opened.json
vendored
@ -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:"
|
||||
}
|
||||
]
|
75
.github/workflows/auto-triager.yml
vendored
Normal file
75
.github/workflows/auto-triager.yml
vendored
Normal file
@ -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 }}
|
@ -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.
|
||||
|
@ -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/<GRAFANA_VERSION>/administration/service-accounts/migrate-api-keys/)
|
||||
- [Dashboards and folders]({{< relref "../../../dashboards/" >}})
|
||||
- [Data sources]({{< relref "../../../datasources/" >}})
|
||||
- [Explore]({{< relref "../../../explore/" >}})
|
||||
|
@ -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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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:<userId>` 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:*`<br>`datasources:uid:*` | Read alerts and silences in data sources that support alerting. |
|
||||
| `alert.instances.external:write` | `datasources:*`<br>`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:*`<br>`datasources:uid:*` | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. |
|
||||
| `alert.notifications.external:write` | `datasources:*`<br>`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:*`<br>`datasources:uid:*` | Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki) |
|
||||
| `alert.rules.external:write` | `datasources:*`<br>`datasources:uid:*` | Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). |
|
||||
| `alert.rules:create` | `folders:*`<br>`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:*`<br>`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:*`<br>`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:*`<br>`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:*`<br>`folders:uid:*` | Create rule-specific silences in a folder and its subfolders. |
|
||||
| `alert.silences:read` | `folders:*`<br>`folders:uid:*` | Read all general silences and rule-specific silences in a folder and its subfolders. |
|
||||
| `alert.silences:write` | `folders:*`<br>`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:*`<br>`annotations:type:*` | Create annotations. |
|
||||
| `annotations:delete` | `annotations:*`<br>`annotations:type:*` | Delete annotations. |
|
||||
| `annotations:read` | `annotations:*`<br>`annotations:type:*` | Read annotations and annotation tags. |
|
||||
| `annotations:write` | `annotations:*`<br>`annotations:type:*` | Update annotations. |
|
||||
| `apikeys:create` | n/a | Create API keys. |
|
||||
| `apikeys:read` | `apikeys:*`<br>`apikeys:id:*` | Read API keys. |
|
||||
| `apikeys:delete` | `apikeys:*`<br>`apikeys:id:*` | Delete API keys. |
|
||||
| `dashboards:create` | `folders:*`<br>`folders:uid:*` | Create dashboards in one or more folders and their subfolders. |
|
||||
| `dashboards:delete` | `dashboards:*`<br>`dashboards:uid:*`<br>`folders:*`<br>`folders:uid:*` | Delete one or more dashboards. |
|
||||
| `dashboards.insights:read` | n/a | Read dashboard insights data and see presence indicators. |
|
||||
| `dashboards.permissions:read` | `dashboards:*`<br>`dashboards:uid:*`<br>`folders:*`<br>`folders:uid:*` | Read permissions for one or more dashboards. |
|
||||
| `dashboards.permissions:write` | `dashboards:*`<br>`dashboards:uid:*`<br>`folders:*`<br>`folders:uid:*` | Update permissions for one or more dashboards. |
|
||||
| `dashboards:read` | `dashboards:*`<br>`dashboards:uid:*`<br>`folders:*`<br>`folders:uid:*` | Read one or more dashboards. |
|
||||
| `dashboards:write` | `dashboards:*`<br>`dashboards:uid:*`<br>`folders:*`<br>`folders:uid:*` | Update one or more dashboards. |
|
||||
| `dashboards.public:write` | `dashboards:*`<br>`dashboards:uid:*` | Write public dashboard configuration. |
|
||||
| `datasources.caching:read` | `datasources:*`<br>`datasources:uid:*` | Read data source query caching settings. |
|
||||
| `datasources.caching:write` | `datasources:*`<br>`datasources:uid:*` | Update data source query caching settings. |
|
||||
| `datasources:create` | n/a | Create data sources. |
|
||||
| `datasources:delete` | `datasources:*`<br>`datasources:uid:*` | Delete data sources. |
|
||||
| `datasources:explore` | n/a | Enable access to the **Explore** tab. |
|
||||
| `datasources.id:read` | `datasources:*`<br>`datasources:uid:*` | Read data source IDs. |
|
||||
| `datasources.insights:read` | n/a | Read data sources insights data. |
|
||||
| `datasources.permissions:read` | `datasources:*`<br>`datasources:uid:*` | List data source permissions. |
|
||||
| `datasources.permissions:write` | `datasources:*`<br>`datasources:uid:*` | Update data source permissions. |
|
||||
| `datasources:query` | `datasources:*`<br>`datasources:uid:*` | Query data sources. |
|
||||
| `datasources:read` | `datasources:*`<br>`datasources:uid:*` | List data sources. |
|
||||
| `datasources:write` | `datasources:*`<br>`datasources:uid:*` | Update data sources. |
|
||||
| `featuremgmt.read` | n/a | Read feature toggles. |
|
||||
| `featuremgmt.write` | n/a | Write feature toggles. |
|
||||
| `folders.permissions:read` | `folders:*`<br>`folders:uid:*` | Read permissions for one or more folders and their subfolders. |
|
||||
| `folders.permissions:write` | `folders:*`<br>`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:*`<br>`folders:uid:*` | Delete one or more folders and their subfolders. |
|
||||
| `folders:read` | `folders:*`<br>`folders:uid:*` | Read one or more folders and their subfolders. |
|
||||
| `folders:write` | `folders:*`<br>`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:*` <br> `folders:uid:*` | Create a library panel in one or more folders and their subfolders. |
|
||||
| `library.panels:read` | `folders:*` <br> `folders:uid:*` <br> `library.panels:*` <br> `library.panels:uid:*` | Read one or more library panels. |
|
||||
| `library.panels:write` | `folders:*` <br> `folders:uid:*` <br> `library.panels:*` <br> `library.panels:uid:*` | Update one or more library panels. |
|
||||
| `library.panels:delete` | `folders:*` <br> `folders:uid:*` <br> `library.panels:*` <br> `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:*` <br> `users:id:*` | Update the organization role (`None`, `Viewer`, `Editor`, or `Admin`) of a user. |
|
||||
| `org.users:add` | `users:*` <br> `users:id:*` | Add a user to an organization or invite a new user to an organization. |
|
||||
| `org.users:read` | `users:*` <br> `users:id:*` | Get user profiles within an organization. |
|
||||
| `org.users:remove` | `users:*` <br> `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:*` <br> `plugins:id:*` | Access one or more application plugins (still enforcing the organization role) |
|
||||
| `plugins:install` | n/a | Install and uninstall plugins. |
|
||||
| `plugins:write` | `plugins:*` <br> `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:*` <br> `reports:id:*` | Update reports. |
|
||||
| `reports.settings:read` | n/a | Read report settings. |
|
||||
| `reports.settings:write` | n/a | Update report settings. |
|
||||
| `reports:delete` | `reports:*` <br> `reports:id:*` | Delete reports. |
|
||||
| `reports:read` | `reports:*` <br> `reports:id:*` | List all available reports or get a specific report. |
|
||||
| `reports:send` | `reports:*` <br> `reports:id:*` | Send a report email. |
|
||||
| `roles:delete` | `permissions:type:delegate` | Delete a custom role. |
|
||||
| `roles:read` | `roles:*` <br> `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:*` <br> `serviceaccounts:id:*` | Delete Grafana service accounts. |
|
||||
| `serviceaccounts:read` | `serviceaccounts:*` <br> `serviceaccounts:id:*` | Read Grafana service accounts. |
|
||||
| `serviceaccounts.permissions:write` | `serviceaccounts:*` <br> `serviceaccounts:id:*` | Update Grafana service account permissions to control who can do what with the service account. |
|
||||
| `serviceaccounts.permissions:read` | `serviceaccounts:*` <br> `serviceaccounts:id:*` | Read Grafana service account permissions to see who can do what with the service account. |
|
||||
| `settings:read` | `settings:*`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Read the [Grafana configuration settings]({{< relref "../../../../setup-grafana/configure-grafana/" >}}) |
|
||||
| `settings:write` | `settings:*`<br>`settings:auth.saml:*`<br>`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:*`<br>`teams:id:*` | Read members and Team Sync setup for teams. |
|
||||
| `teams.permissions:write` | `teams:*`<br>`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:*`<br>`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:*`<br>`teams:id:*` | Delete one or more teams. |
|
||||
| `teams:read` | `teams:*`<br>`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:*`<br>`teams:id:*` | Update one or more teams and team preferences. |
|
||||
| `users.authtoken:read` | `global.users:*` <br> `global.users:id:*` | List authentication tokens that are assigned to a user. |
|
||||
| `users.authtoken:write` | `global.users:*` <br> `global.users:id:*` | Update authentication tokens that are assigned to a user. |
|
||||
| `users.password:write` | `global.users:*` <br> `global.users:id:*` | Update a user’s password. |
|
||||
| `users.permissions:read` | `users:*` | List permissions of a user. |
|
||||
| `users.permissions:write` | `global.users:*` <br> `global.users:id:*` | Update a user’s organization-level permissions. |
|
||||
| `users.quotas:read` | `global.users:*` <br> `global.users:id:*` | List a user’s quotas. |
|
||||
| `users.quotas:write` | `global.users:*` <br> `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:*` <br> `global.users:id:*` | Delete a user. |
|
||||
| `users:disable` | `global.users:*` <br> `global.users:id:*` | Disable a user. |
|
||||
| `users:enable` | `global.users:*` <br> `global.users:id:*` | Enable a user. |
|
||||
| `users:logout` | `global.users:*` <br> `global.users:id:*` | Sign out a user. |
|
||||
| `users:read` | `global.users:*` | Read or search user profiles. |
|
||||
| `users:write` | `global.users:*` <br> `global.users:id:*` | Update a user’s profile. |
|
||||
<!-- prettier-ignore-start -->
|
||||
| Action | Applicable scopes | Description |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `alert.instances.external:read` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Read alerts and silences in data sources that support alerting. |
|
||||
| `alert.instances.external:write` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | 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` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. |
|
||||
| `alert.notifications.external:write` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | 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` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki) |
|
||||
| `alert.rules.external:write` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). |
|
||||
| `alert.rules:create` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | 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` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | 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` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | 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` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | 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` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Create rule-specific silences in a folder and its subfolders. |
|
||||
| `alert.silences:read` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Read all general silences and rule-specific silences in a folder and its subfolders. |
|
||||
| `alert.silences:write` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | 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` | <ul><li>`annotations:*`</li><li>`annotations:type:*`</li></ul> | Create annotations. |
|
||||
| `annotations:delete` | <ul><li>`annotations:*`</li><li>`annotations:type:*`</li></ul> | Delete annotations. |
|
||||
| `annotations:read` | <ul><li>`annotations:*`</li><li>`annotations:type:*`</li></ul> | Read annotations and annotation tags. |
|
||||
| `annotations:write` | <ul><li>`annotations:*`</li><li>`annotations:type:*`</li></ul> | Update annotations. |
|
||||
| `apikeys:create` | None | Create API keys. |
|
||||
| `apikeys:read` | <ul><li>`apikeys:*`</li><li>`apikeys:id:*`</li></ul> | Read API keys. |
|
||||
| `apikeys:delete` | <ul><li>`apikeys:*`</li><li>`apikeys:id:*`</li></ul> | Delete API keys. |
|
||||
| `dashboards:create` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Create dashboards in one or more folders and their subfolders. |
|
||||
| `dashboards:delete` | <ul><li>`dashboards:*`</li><li>`dashboards:uid:*`</li><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Delete one or more dashboards. |
|
||||
| `dashboards.insights:read` | None | Read dashboard insights data and see presence indicators. |
|
||||
| `dashboards.permissions:read` | <ul><li>`dashboards:*`</li><li>`dashboards:uid:*`</li><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Read permissions for one or more dashboards. |
|
||||
| `dashboards.permissions:write` | <ul><li>`dashboards:*`</li><li>`dashboards:uid:*`</li><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Update permissions for one or more dashboards. |
|
||||
| `dashboards:read` | <ul><li>`dashboards:*`</li><li>`dashboards:uid:*`</li><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Read one or more dashboards. |
|
||||
| `dashboards:write` | <ul><li>`dashboards:*`</li><li>`dashboards:uid:*`</li><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Update one or more dashboards. |
|
||||
| `dashboards.public:write` | <ul><li>`dashboards:*`</li><li>`dashboards:uid:*`</li></ul> | Write public dashboard configuration. |
|
||||
| `datasources.caching:read` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Read data source query caching settings. |
|
||||
| `datasources.caching:write` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Update data source query caching settings. |
|
||||
| `datasources:create` | None | Create data sources. |
|
||||
| `datasources:delete` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Delete data sources. |
|
||||
| `datasources:explore` | None | Enable access to the **Explore** tab. |
|
||||
| `datasources.id:read` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Read data source IDs. |
|
||||
| `datasources.insights:read` | None | Read data sources insights data. |
|
||||
| `datasources.permissions:read` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | List data source permissions. |
|
||||
| `datasources.permissions:write` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Update data source permissions. |
|
||||
| `datasources:query` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Query data sources. |
|
||||
| `datasources:read` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | List data sources. |
|
||||
| `datasources:write` | <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | Update data sources. |
|
||||
| `featuremgmt.read` | None | Read feature toggles. |
|
||||
| `featuremgmt.write` | None | Write feature toggles. |
|
||||
| `folders.permissions:read` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Read permissions for one or more folders and their subfolders. |
|
||||
| `folders.permissions:write` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Update permissions for one or more folders and their subfolders. |
|
||||
| `folders:create` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li><li>`folders:uid:general`</li></ul> | 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` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Delete one or more folders and their subfolders. |
|
||||
| `folders:read` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Read one or more folders and their subfolders. |
|
||||
| `folders:write` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | 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` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Create a library panel in one or more folders and their subfolders. |
|
||||
| `library.panels:read` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li><li>`library.panels:*`</li><li>`library.panels:uid:*`</li></ul> | Read one or more library panels. |
|
||||
| `library.panels:write` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li><li>`library.panels:*`</li><li>`library.panels:uid:*`</li></ul> | Update one or more library panels. |
|
||||
| `library.panels:delete` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li><li>`library.panels:*`</li><li>`library.panels:uid:*`</li></ul> | 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` | <ul><li>`users:*`</li><li>`users:id:*`</li></ul> | Update the organization role (`None`, `Viewer`, `Editor`, or `Admin`) of a user. |
|
||||
| `org.users:add` | <ul><li>`users:*`</li><li>`users:id:*`</li></ul> | Add a user to an organization or invite a new user to an organization. |
|
||||
| `org.users:read` | <ul><li>`users:*`</li><li>`users:id:*`</li></ul> | Get user profiles within an organization. |
|
||||
| `org.users:remove` | <ul><li>`users:*`</li><li>`users:id:*`</li></ul> | 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` | <ul><li>`plugins:*`</li><li>`plugins:id:*`</li></ul> | Access one or more application plugins (still enforcing the organization role) |
|
||||
| `plugins:install` | None | Install and uninstall plugins. |
|
||||
| `plugins:write` | <ul><li>`plugins:*`</li><li>`plugins:id:*`</li></ul> | 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` | <ul><li>`reports:*`</li><li>`reports:id:*`</li></ul> | Update reports. |
|
||||
| `reports.settings:read` | None | Read report settings. |
|
||||
| `reports.settings:write` | None | Update report settings. |
|
||||
| `reports:delete` | <ul><li>`reports:*`</li><li>`reports:id:*`</li></ul> | Delete reports. |
|
||||
| `reports:read` | <ul><li>`reports:*`</li><li>`reports:id:*`</li></ul> | List all available reports or get a specific report. |
|
||||
| `reports:send` | <ul><li>`reports:*`</li><li>`reports:id:*`</li></ul> | Send a report email. |
|
||||
| `roles:delete` | <ul><li>`permissions:type:delegate`</li><ul> | Delete a custom role. |
|
||||
| `roles:read` | <ul><li>`roles:*`</li><li>`roles:uid:*`</li></ul> | List roles and read a specific role with its permissions. |
|
||||
| `roles:write` | <ul><li>`permissions:type:delegate`</li><ul> | Create or update a custom role. |
|
||||
| `roles:write` | <ul><li>`permissions:type:escalate`</li><ul> | Reset basic roles to their default permissions. |
|
||||
| `server.stats:read` | None | Read Grafana instance statistics. |
|
||||
| `server.usagestats.report:read` | None | View usage statistics report. |
|
||||
| `serviceaccounts:write` | <ul><li>`serviceaccounts:*`</li><ul> | Create Grafana service accounts. |
|
||||
| `serviceaccounts:create` | None | Update Grafana service accounts. |
|
||||
| `serviceaccounts:delete` | <ul><li>`serviceaccounts:*`</li><li>`serviceaccounts:id:*`</li></ul> | Delete Grafana service accounts. |
|
||||
| `serviceaccounts:read` | <ul><li>`serviceaccounts:*`</li><li>`serviceaccounts:id:*`</li></ul> | Read Grafana service accounts. |
|
||||
| `serviceaccounts.permissions:write` | <ul><li>`serviceaccounts:*`</li><li>`serviceaccounts:id:*`</li></ul> | Update Grafana service account permissions to control who can do what with the service account. |
|
||||
| `serviceaccounts.permissions:read` | <ul><li>`serviceaccounts:*`</li><li>`serviceaccounts:id:*`</li></ul> | Read Grafana service account permissions to see who can do what with the service account. |
|
||||
| `settings:read` | <ul><li>`settings:*`</li><li>`settings:auth.saml:*`</li><li>`settings:auth.saml:enabled`</li></ul> (property level) | Read the [Grafana configuration settings](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/) |
|
||||
| `settings:write` | <ul><li>`settings:*`</li><li>`settings:auth.saml:*`</li><li>`settings:auth.saml:enabled`</li></ul> (property level) | Update any Grafana configuration settings that can be [updated at runtime](/docs/grafana/<GRAFANA_VERSION>/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` | <ul><li>`services:accesscontrol`</li><ul> | Get access-control enabled status. |
|
||||
| `teams.permissions:read` | <ul><li>`teams:*`</li><li>`teams:id:*`</li></ul> | Read members and Team Sync setup for teams. |
|
||||
| `teams.permissions:write` | <ul><li>`teams:*`</li><li>`teams:id:*`</li></ul> | Add, remove and update members and manage Team Sync setup for teams. |
|
||||
| `teams.roles:add` | <ul><li>`permissions:type:delegate`</li><ul> | Assign a role to a team. |
|
||||
| `teams.roles:read` | <ul><li>`teams:*`</li><li>`teams:id:*`</li></ul> | List roles assigned directly to a team. |
|
||||
| `teams.roles:remove` | <ul><li>`permissions:type:delegate`</li><ul> | Unassign a role from a team. |
|
||||
| `teams:create` | None | Create teams. |
|
||||
| `teams:delete` | <ul><li>`teams:*`</li><li>`teams:id:*`</li></ul> | Delete one or more teams. |
|
||||
| `teams:read` | <ul><li>`teams:*`</li><li>`teams:id:*`</li></ul> | 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` | <ul><li>`teams:*`</li><li>`teams:id:*`</li></ul> | Update one or more teams and team preferences. |
|
||||
| `users.authtoken:read` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | List authentication tokens that are assigned to a user. |
|
||||
| `users.authtoken:write` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Update authentication tokens that are assigned to a user. |
|
||||
| `users.password:write` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Update a user’s password. |
|
||||
| `users.permissions:read` | <ul><li>`users:*`</li><ul> | List permissions of a user. |
|
||||
| `users.permissions:write` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Update a user’s organization-level permissions. |
|
||||
| `users.quotas:read` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | List a user’s quotas. |
|
||||
| `users.quotas:write` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Update a user’s quotas. |
|
||||
| `users.roles:add` | <ul><li>`permissions:type:delegate`</li><ul> | Assign a role to a user or a service account. |
|
||||
| `users.roles:read` | <ul><li>`users:*`</li><ul> | List roles assigned directly to a user or a service account. |
|
||||
| `users.roles:remove` | <ul><li>`permissions:type:delegate`</li><ul> | Unassign a role from a user or a service account. |
|
||||
| `users:create` | None | Create a user. |
|
||||
| `users:delete` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Delete a user. |
|
||||
| `users:disable` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Disable a user. |
|
||||
| `users:enable` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Enable a user. |
|
||||
| `users:logout` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Sign out a user. |
|
||||
| `users:read` | <ul><li>`global.users:*`</li><ul> | Read or search user profiles. |
|
||||
| `users:write` | <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | Update a user’s profile. |
|
||||
{ .no-spacing-list }
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
### 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:*`<br>`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:*`<br>`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:*`<br>`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:*`<br>`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:*`<br>`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:*` <br> `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:*` <br> `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:*` <br> `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:*` <br> `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:*` <br> `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:*` <br> `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:*` <br> `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:*` <br> `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:*` <br> `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. |
|
||||
<!-- prettier-ignore-start -->
|
||||
| Scopes | Descriptions |
|
||||
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <ul><li>`annotations:*`</li><li>`annotations:type:*`</li></ul> | 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. |
|
||||
| <ul><li>`apikeys:*`</li><li>`apikeys:id:*`</li></ul> | 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`. |
|
||||
| <ul><li>`dashboards:*`</li><li>`dashboards:uid:*`</li></ul> | 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`. |
|
||||
| <ul><li>`datasources:*`</li><li>`datasources:uid:*`</li></ul> | 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`. |
|
||||
| <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | 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. |
|
||||
| <ul><li>`global.users:*`</li><li>`global.users:id:*`</li></ul> | 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`. |
|
||||
| <ul><li>`library.panels:*`</li><li>`library.panels:uid:*`</li></ul> | 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`. |
|
||||
| <ul><li>`orgs:*`</li><li>`orgs:id:*`</li></ul> | 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`. |
|
||||
| <ul><li>`permissions:type:delegate`</li><ul> | 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. |
|
||||
| <ul><li>`permissions:type:escalate`</li><ul> | 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. |
|
||||
| <ul><li>`plugins:*`</li><li>`plugins:id:*`</li></ul> | Restrict an action to a set of plugins. For example, `plugins:id:grafana-oncall-app` matches Grafana OnCall plugin, and `plugins:*` matches all plugins. |
|
||||
| <ul><li>`provisioners:*`</li><ul> | 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/" >}}). |
|
||||
| <ul><li>`reports:*`</li><li>`reports:id:*`</li></ul> | 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`. |
|
||||
| <ul><li>`roles:*`</li><li>`roles:uid:*`</li></ul> | 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`. |
|
||||
| <ul><li>`services:accesscontrol`</li><ul> | Restrict an action to target only the role-based access control service. You can use this in conjunction with the `status:accesscontrol` actions. |
|
||||
| <ul><li>`serviceaccounts:*`</li><li>`serviceaccounts:id:*`</li></ul> | 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`. |
|
||||
| <ul><li>`settings:*`</li><ul> | 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. |
|
||||
| <ul><li>`teams:*`</li><li>`teams:id:*`</li></ul> | 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`. |
|
||||
| <ul><li>`users:*`</li><li>`users:id:*`</li></ul> | 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`. |
|
||||
| <ul><li>None</li><ul> | 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 }
|
||||
<!-- prettier-ignore-end -->
|
||||
|
@ -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 <br>`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`<br>`dashboards:read` | Read all folders and dashboards. |
|
||||
| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and <br>`folders:read`<br>`folders:write`<br>`folders:create`<br>`folders:delete`<br>`folders.permissions:read`<br>`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 <br>`folders:read`<br>`folders:write`<br>`folders:create`<br>`folders:delete`<br>`folders.permissions:read`<br>`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 <br>`folders.permissions:write` | Read and update all folder permissions. |
|
||||
| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`<br>`ldap.status:read` | Read the LDAP configuration and LDAP status information. |
|
||||
|
@ -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/<GRAFANA_VERSION>/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:
|
@ -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/<GRAFANA_VERSION>/administration/api-keys/
|
||||
- ../about-api-keys/ # /docs/grafana/<GRAFANA_VERSION>/administration/about-api-keys/
|
||||
- ../create-api-key/ # /docs/grafana/<GRAFANA_VERSION>/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.
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/)
|
||||
|
||||
|
@ -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:**
|
||||
|
||||
|
@ -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/" >}})
|
||||
|
@ -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/<GRAFANA_VERSION>/administration/service-accounts/migrate-api-keys/).
|
||||
|
||||
`POST /api/auth/keys`
|
||||
|
||||
|
@ -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:<destination folder UID>` |
|
||||
| Action | Scope |
|
||||
| ---------------- | ----------------------------------------------------- |
|
||||
| `folders:create` | `folders:uid:<destination folder UID>`<br>`folders:*` |
|
||||
|
||||
If moving the folder under root:
|
||||
| Action | Scope |
|
||||
| -------------- | ------------- |
|
||||
| `folders:create` | `folders:*` |
|
||||
| `folders:create` | `folders:uid:general`<br>`folders:*` |
|
||||
|
||||
JSON body schema:
|
||||
|
||||
|
@ -13,7 +13,7 @@ labels:
|
||||
products:
|
||||
- enterprise
|
||||
- oss
|
||||
title: 'HTTP Preferences API '
|
||||
title: 'Preferences API'
|
||||
---
|
||||
|
||||
# User and Org Preferences API
|
||||
|
@ -13,7 +13,7 @@ labels:
|
||||
products:
|
||||
- enterprise
|
||||
- oss
|
||||
title: 'HTTP Snapshot API '
|
||||
title: 'Snapshot API'
|
||||
---
|
||||
|
||||
# Snapshot API
|
||||
|
@ -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).",
|
||||
|
@ -50,6 +50,10 @@ Once you’ve created a [dashboard](https://grafana.com/docs/grafana/<GRAFANA_VE
|
||||
|
||||
Use the following options to refine your alert list visualization.
|
||||
|
||||
### View mode
|
||||
|
||||
Choose between **List** to display alerts in a detailed list format with comprehensive information, or **Stat** to show alerts as a summarized single-value statistic.
|
||||
|
||||
### Group mode
|
||||
|
||||
Choose between **Default grouping** to show alert instances grouped by their alert rule, or **Custom grouping** to show alert instances grouped by a custom set of labels.
|
||||
@ -103,8 +107,8 @@ Filter alerts by the selected folder. Only alerts from dashboards in this folder
|
||||
|
||||
Choose which alert states to display in this visualization.
|
||||
|
||||
- Alerting / Firing
|
||||
- Pending
|
||||
- No Data
|
||||
- Normal
|
||||
- Error
|
||||
- **Alerting / Firing -** Shows alerts that are currently active and triggering an alert condition.
|
||||
- **Pending -** Shows alerts that are in a transitional state, waiting for conditions to be met before triggering.
|
||||
- **No Data -** Shows alerts where the data source is not returning any data, which could indicate an issue with data collection.
|
||||
- **Normal -** Shows alerts that are in a normal or resolved state, where no alert condition is currently met.
|
||||
- **Error -** Shows alerts where an error has occurred, typically related to an issue in the alerting process.
|
||||
|
@ -30,11 +30,26 @@ refs:
|
||||
|
||||
# Canvas
|
||||
|
||||
Canvases combine the power of Grafana with the flexibility of custom elements. Canvases are extensible form-built visualizations that allow you to explicitly place elements within static and dynamic layouts. This empowers you to design custom visualizations and overlay data in ways that aren't possible with standard Grafana panels, all within Grafana's UI. If you've used popular UI and web design tools, then designing canvases will feel very familiar.
|
||||
Canvases combine the power of Grafana with the flexibility of custom elements.
|
||||
They are extensible visualizations that allow you to add and arrange elements wherever you want within unstructured static and dynamic layouts.
|
||||
This lets you design custom visualizations and overlay data in ways that aren't possible with standard Grafana visualizations, all within the Grafana UI.
|
||||
|
||||
> 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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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="<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="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Tooltip options
|
||||
|
||||
{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="<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="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Data links
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Value mappings
|
||||
|
||||
{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
@ -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="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Legend options
|
||||
|
||||
{{< docs/shared lookup="visualizations/legend-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
@ -131,6 +131,10 @@ Controls the opacity of state regions.
|
||||
|
||||
{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Standard options
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Data links
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
@ -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 |
|
||||
|
@ -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.
|
||||
|
@ -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/<GRAFANA_VERSION>/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:
|
||||
|
@ -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/<GRAFANA_VERSION>/administration/service-accounts/migrate-api-keys/).
|
||||
|
||||
## Configure secret scanning
|
||||
|
||||
|
@ -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
|
||||
<!-- INTERACTIVE page intro.md START -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
{{< 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 >}}
|
||||
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
{{< 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 >}}
|
||||
|
||||
<!-- INTERACTIVE page intro.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step1.md START -->
|
||||
|
||||
## 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)
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
## 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:
|
||||
<!-- INTERACTIVE ignore END-->
|
||||
|
||||
```
|
||||
sum by (message)(count_over_time({filename="/var/log/web_requests.log"} != `status=200` | pattern `<_> <message> duration<_>` [10m]))
|
||||
### Grafana OSS users
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
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/)
|
||||
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
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:
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
**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.
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
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.
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
- 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
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
[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.
|
||||
<!-- INTERACTIVE page step1.md END -->
|
||||
|
||||
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:
|
||||
<!-- INTERACTIVE page step2.md START -->
|
||||
|
||||
```
|
||||
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 `<_> <message> 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
|
||||
<!-- INTERACTIVE page step2.md END -->
|
||||
|
||||
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.
|
||||
<!-- INTERACTIVE page step3.md START -->
|
||||
|
||||
### 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**
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
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.
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
{{< 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.
|
||||
|
||||
<!-- INTERACTIVE page step3.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step4.md START -->
|
||||
|
||||
## 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 "<_> <message> 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**
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
```
|
||||
{{% 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
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
```
|
||||
{{< 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.
|
||||
|
||||
<!-- INTERACTIVE page step4.md END -->
|
||||
|
||||
requests_per_second = 2
|
||||
failure_rate = 0.05
|
||||
get_post_ratio = 0.9
|
||||
get_average_duration_ms = 500
|
||||
post_average_duration_ms = 2000
|
||||
<!-- INTERACTIVE page step5.md START -->
|
||||
|
||||
## 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" >}}
|
||||
|
||||
```
|
||||
<!-- INTERACTIVE page step5.md END -->
|
||||
|
||||
1. Execute the log-generating python script.
|
||||
<!-- INTERACTIVE page finish.md START -->
|
||||
|
||||
In a terminal window on linux-based systems run the command:
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
```
|
||||
{{< 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
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
```
|
||||
{{< 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.
|
||||
<!-- INTERACTIVE page finish.md END -->
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
<!-- INTERACTIVE page intro.md START -->
|
||||
|
||||
## 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
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
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 >}}
|
||||
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
<!-- INTERACTIVE page intro.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step1.md START -->
|
||||
|
||||
## 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.
|
||||
|
||||
<!-- INTERACTIVE page step1.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step2.md START -->
|
||||
|
||||
## 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.
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
1. Open a new tab.
|
||||
1. Browse to [localhost:3000](http://localhost:3000).
|
||||
1. Browse to [http://localhost:3000](http://localhost:3000).
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
{{< docs/ignore >}}
|
||||
|
||||
1. Browse to [http://localhost:3000](http://localhost:3000).
|
||||
{{< /docs/ignore >}}
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
{{< 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 >}}
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
{{< 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.
|
||||
|
||||
<!-- INTERACTIVE page step2.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step3.md START -->
|
||||
|
||||
## 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)`.
|
||||
|
||||
<!-- INTERACTIVE page step3.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step4.md START -->
|
||||
|
||||
## 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.
|
||||
|
||||
<!-- INTERACTIVE page step4.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step5.md START -->
|
||||
|
||||
## 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.
|
||||
|
||||
<!-- INTERACTIVE page step5.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step6.md START -->
|
||||
|
||||
## 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" >}}
|
||||
|
||||
<!-- INTERACTIVE page step6.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step7.md START -->
|
||||
|
||||
## 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
|
||||
<!-- INTERACTIVE page step7.md END -->
|
||||
|
||||
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.
|
||||
<!-- INTERACTIVE page step8.md START -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
{{< 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 >}}
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
{{< 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
|
||||
<!-- INTERACTIVE page step8.md END -->
|
||||
|
||||
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.
|
||||
<!-- INTERACTIVE page step9.md START -->
|
||||
|
||||
### 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
|
||||
<!-- INTERACTIVE page step9.md END -->
|
||||
|
||||
<!-- INTERACTIVE page step10.md START -->
|
||||
|
||||
### 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.
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
{{< 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
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
{{< youtube id="ClLp-iSoaSY" >}}
|
||||
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
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" >}}
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
@ -405,17 +504,27 @@ Check out our [advanced alerting tutorial](http://grafana.com/tutorials/alerting
|
||||
|
||||
{{< /docs/ignore >}}
|
||||
|
||||
<!-- INTERACTIVE page step10.md END -->
|
||||
|
||||
<!-- INTERACTIVE page finish.md START -->
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- INTERACTIVE ignore START -->
|
||||
|
||||
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)
|
||||
<!-- INTERACTIVE ignore END -->
|
||||
|
||||
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/)
|
||||
|
||||
<!-- INTERACTIVE page finish.md END -->
|
||||
|
@ -19,51 +19,55 @@ weight: 1000
|
||||
|
||||
{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="<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.
|
||||
|
@ -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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/administration/service-accounts/migrate-api-keys/).
|
||||
|
@ -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 (
|
||||
<PluginPage>
|
||||
<div data-testid={testIds.addedLinksPage.container}>
|
||||
{isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
links.map(({ id, title, path, onClick }) => (
|
||||
<a href={path} title={title} key={id} onClick={onClick}>
|
||||
{title}
|
||||
</a>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<Stack direction={'column'} gap={4} data-testid={testIds.addedLinksPage.container}>
|
||||
<section data-testid={testIds.addedLinksPage.section1}>
|
||||
<h3>Link extensions defined with addLink and retrieved using usePluginLinks</h3>
|
||||
<ActionButton extensions={links} />
|
||||
</section>
|
||||
</Stack>
|
||||
</PluginPage>
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { LINKS_EXTENSION_POINT_ID } from '../../pages/AddedLinks';
|
||||
import { testIds } from '../../testIds';
|
||||
|
||||
import { App } from './components/App';
|
||||
import pluginJson from './plugin.json';
|
||||
|
||||
export const plugin = new AppPlugin<{}>()
|
||||
.setRootPage(App)
|
||||
@ -24,5 +23,11 @@ export const plugin = new AppPlugin<{}>()
|
||||
title: 'Basic link',
|
||||
description: '...',
|
||||
targets: [LINKS_EXTENSION_POINT_ID],
|
||||
path: `/a/${pluginJson.id}/`,
|
||||
path: '/a/grafana-extensionexample1-app/',
|
||||
})
|
||||
.addLink({
|
||||
title: 'Go to A',
|
||||
description: 'Navigating to pluging A',
|
||||
targets: [LINKS_EXTENSION_POINT_ID],
|
||||
path: '/a/grafana-extensionexample1-app/',
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AppPlugin } from '@grafana/data';
|
||||
|
||||
import { LINKS_EXTENSION_POINT_ID } from '../../pages/AddedLinks';
|
||||
import { testIds } from '../../testIds';
|
||||
|
||||
import { App } from './components/App';
|
||||
@ -30,4 +31,15 @@ export const plugin = new AppPlugin<{}>()
|
||||
component: ({ name }: { name: string }) => (
|
||||
<div data-testid={testIds.appB.reusableAddedComponent}>Hello {name}!</div>
|
||||
),
|
||||
})
|
||||
.addLink({
|
||||
title: 'Open from B',
|
||||
description: 'Open a modal from plugin B',
|
||||
targets: [LINKS_EXTENSION_POINT_ID],
|
||||
onClick: (_, { openModal }) => {
|
||||
openModal({
|
||||
title: 'Modal from app B',
|
||||
body: () => <div data-testid={testIds.appB.modal}>From plugin B</div>,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -36,5 +36,6 @@ export const testIds = {
|
||||
},
|
||||
addedLinksPage: {
|
||||
container: 'data-testid pg-added-links-container',
|
||||
section1: 'use-plugin-links',
|
||||
},
|
||||
};
|
||||
|
@ -3,8 +3,28 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
import pluginJson from '../plugin.json';
|
||||
import { testIds } from '../testIds';
|
||||
|
||||
test('path link', async ({ page }) => {
|
||||
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
|
||||
await page.goto(`/a/${pluginJson.id}/added-links`);
|
||||
await page.getByTestId(testIds.addedLinksPage.container).getByText('Basic link').click();
|
||||
await expect(page.getByTestId(testIds.appA.container)).toHaveText('Hello Grafana!');
|
||||
const section = await page.getByTestId(testIds.addedLinksPage.section1);
|
||||
await section.getByTestId(testIds.actions.button).click();
|
||||
await page.getByTestId(testIds.container).getByText('Go to A').click();
|
||||
await page.getByTestId(testIds.modal.open).click();
|
||||
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should extend main app with link extension from app B', async ({ page }) => {
|
||||
await page.goto(`/a/${pluginJson.id}/added-links`);
|
||||
const section = await page.getByTestId(testIds.addedLinksPage.section1);
|
||||
await section.getByTestId(testIds.actions.button).click();
|
||||
await page.getByTestId(testIds.container).getByText('Open from B').click();
|
||||
await expect(page.getByTestId(testIds.appB.modal)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should extend main app with basic link extension from app A', async ({ page }) => {
|
||||
await page.goto(`/a/${pluginJson.id}/added-links`);
|
||||
const section = await page.getByTestId(testIds.addedLinksPage.section1);
|
||||
await section.getByTestId(testIds.actions.button).click();
|
||||
await page.getByTestId(testIds.container).getByText('Basic link').click();
|
||||
await page.getByTestId(testIds.modal.open).click();
|
||||
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
|
||||
});
|
||||
|
10
package.json
10
package.json
@ -71,7 +71,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.2",
|
||||
"@babel/preset-env": "7.25.4",
|
||||
"@babel/runtime": "7.25.4",
|
||||
"@babel/runtime": "7.25.6",
|
||||
"@betterer/betterer": "5.4.0",
|
||||
"@betterer/cli": "5.4.0",
|
||||
"@betterer/eslint": "5.4.0",
|
||||
@ -122,7 +122,7 @@
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/logfmt": "^1.2.3",
|
||||
"@types/lucene": "^2",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/node-forge": "^1",
|
||||
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
@ -175,7 +175,7 @@
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "28.8.0",
|
||||
"eslint-plugin-jest": "28.8.1",
|
||||
"eslint-plugin-jest-dom": "^5.4.0",
|
||||
"eslint-plugin-jsdoc": "48.11.0",
|
||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
||||
@ -268,7 +268,7 @@
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/saga-icons": "workspace:*",
|
||||
"@grafana/scenes": "^5.10.1",
|
||||
"@grafana/scenes": "^5.11.1",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
@ -403,7 +403,7 @@
|
||||
"systemjs-cjs-extra": "0.2.1",
|
||||
"tether-drop": "https://github.com/torkelo/drop",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tslib": "2.6.3",
|
||||
"tslib": "2.7.0",
|
||||
"tween-functions": "^1.2.0",
|
||||
"type-fest": "^4.18.2",
|
||||
"uplot": "1.6.30",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"rxjs": "7.8.1",
|
||||
"string-hash": "^1.1.3",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tslib": "2.6.3",
|
||||
"tslib": "2.7.0",
|
||||
"uplot": "1.6.30",
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
@ -66,7 +66,7 @@
|
||||
"@types/dompurify": "^3.0.0",
|
||||
"@types/history": "4.7.11",
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/papaparse": "5.3.14",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.2.25",
|
||||
|
@ -555,8 +555,9 @@ export {
|
||||
type PluginExtensionDataSourceConfigContext,
|
||||
type PluginExtensionCommandPaletteContext,
|
||||
type PluginExtensionOpenModalOptions,
|
||||
type PluginExposedComponentConfig,
|
||||
type PluginAddedComponentConfig,
|
||||
type PluginExtensionExposedComponentConfig,
|
||||
type PluginExtensionAddedComponentConfig,
|
||||
type PluginExtensionAddedLinkConfig,
|
||||
} from './types/pluginExtensions';
|
||||
export {
|
||||
type ScopeDashboardBindingSpec,
|
||||
|
@ -5,11 +5,10 @@ import { NavModel } from './navModel';
|
||||
import { PluginMeta, GrafanaPlugin, PluginIncludeType } from './plugin';
|
||||
import {
|
||||
type PluginExtensionLinkConfig,
|
||||
PluginExtensionTypes,
|
||||
PluginExtensionComponentConfig,
|
||||
PluginExposedComponentConfig,
|
||||
PluginExtensionConfig,
|
||||
PluginAddedComponentConfig,
|
||||
PluginExtensionExposedComponentConfig,
|
||||
PluginExtensionAddedComponentConfig,
|
||||
PluginExtensionAddedLinkConfig,
|
||||
} from './pluginExtensions';
|
||||
|
||||
/**
|
||||
@ -58,9 +57,9 @@ export interface AppPluginMeta<T extends KeyValue = KeyValue> extends PluginMeta
|
||||
}
|
||||
|
||||
export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppPluginMeta<T>> {
|
||||
private _exposedComponentConfigs: PluginExposedComponentConfig[] = [];
|
||||
private _addedComponentConfigs: PluginAddedComponentConfig[] = [];
|
||||
private _extensionConfigs: PluginExtensionConfig[] = [];
|
||||
private _exposedComponentConfigs: PluginExtensionExposedComponentConfig[] = [];
|
||||
private _addedComponentConfigs: PluginExtensionAddedComponentConfig[] = [];
|
||||
private _addedLinkConfigs: PluginExtensionAddedLinkConfig[] = [];
|
||||
|
||||
// Content under: /a/${plugin-id}/*
|
||||
root?: ComponentType<AppRootProps<T>>;
|
||||
@ -110,38 +109,24 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
||||
return this._addedComponentConfigs;
|
||||
}
|
||||
|
||||
get extensionConfigs() {
|
||||
return this._extensionConfigs;
|
||||
get addedLinkConfigs() {
|
||||
return this._addedLinkConfigs;
|
||||
}
|
||||
|
||||
addLink<Context extends object>(
|
||||
extensionConfig: { targets: string | string[] } & Omit<
|
||||
PluginExtensionLinkConfig<Context>,
|
||||
'type' | 'extensionPointId'
|
||||
>
|
||||
) {
|
||||
const { targets, ...extension } = extensionConfig;
|
||||
const targetsArray = Array.isArray(targets) ? targets : [targets];
|
||||
|
||||
targetsArray.forEach((target) => {
|
||||
this._extensionConfigs.push({
|
||||
...extension,
|
||||
extensionPointId: target,
|
||||
type: PluginExtensionTypes.link,
|
||||
} as PluginExtensionLinkConfig);
|
||||
});
|
||||
addLink<Context extends object>(linkConfig: PluginExtensionAddedLinkConfig<Context>) {
|
||||
this._addedLinkConfigs.push(linkConfig as PluginExtensionAddedLinkConfig);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addComponent<Props = {}>(addedComponentConfig: PluginAddedComponentConfig<Props>) {
|
||||
this._addedComponentConfigs.push(addedComponentConfig as PluginAddedComponentConfig);
|
||||
addComponent<Props = {}>(addedComponentConfig: PluginExtensionAddedComponentConfig<Props>) {
|
||||
this._addedComponentConfigs.push(addedComponentConfig as PluginExtensionAddedComponentConfig);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
exposeComponent<Props = {}>(componentConfig: PluginExposedComponentConfig<Props>) {
|
||||
this._exposedComponentConfigs.push(componentConfig as PluginExposedComponentConfig);
|
||||
exposeComponent<Props = {}>(componentConfig: PluginExtensionExposedComponentConfig<Props>) {
|
||||
this._exposedComponentConfigs.push(componentConfig as PluginExtensionExposedComponentConfig);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -142,6 +142,7 @@ export interface DataSourcePluginMeta<T extends KeyValue = {}> extends PluginMet
|
||||
unlicensed?: boolean;
|
||||
backend?: boolean;
|
||||
isBackend?: boolean;
|
||||
multiValueFilterOperators?: boolean;
|
||||
}
|
||||
|
||||
interface PluginMetaQueryOptions {
|
||||
|
@ -202,6 +202,7 @@ export interface FeatureToggles {
|
||||
backgroundPluginInstaller?: boolean;
|
||||
dataplaneAggregator?: boolean;
|
||||
adhocFilterOneOf?: boolean;
|
||||
newFiltersUI?: boolean;
|
||||
lokiSendDashboardPanelNames?: boolean;
|
||||
singleTopNav?: boolean;
|
||||
exploreLogsShardSplitting?: boolean;
|
||||
|
@ -137,6 +137,7 @@ export enum FieldConfigProperty {
|
||||
Unit = 'unit',
|
||||
Min = 'min',
|
||||
Max = 'max',
|
||||
FieldMinMax = 'fieldMinMax',
|
||||
Decimals = 'decimals',
|
||||
DisplayName = 'displayName',
|
||||
NoValue = 'noValue',
|
||||
|
@ -23,7 +23,6 @@ type PluginExtensionBase = {
|
||||
description: string;
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
export type PluginExtensionLink = PluginExtensionBase & {
|
||||
type: PluginExtensionTypes.link;
|
||||
path?: string;
|
||||
@ -41,61 +40,20 @@ export type PluginExtension = PluginExtensionLink | PluginExtensionComponent;
|
||||
|
||||
// Objects used for registering extensions (in app plugins)
|
||||
// --------------------------------------------------------
|
||||
export type PluginExtensionLinkConfig<Context extends object = object> = {
|
||||
type: PluginExtensionTypes.link;
|
||||
|
||||
type PluginExtensionConfigBase = {
|
||||
/**
|
||||
* The title of the link extension
|
||||
*/
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
// A URL path that will be used as the href for the rendered link extension
|
||||
// (It is optional, because in some cases the action will be handled by the `onClick` handler instead of navigating to a new page)
|
||||
path?: string;
|
||||
|
||||
// A function that will be called when the link is clicked
|
||||
// (It is called with the original event object)
|
||||
onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
|
||||
/**
|
||||
* The unique identifier of the Extension Point
|
||||
* (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
||||
* A short description
|
||||
*/
|
||||
extensionPointId: string;
|
||||
|
||||
// (Optional) A function that can be used to configure the extension dynamically based on the extension point's context
|
||||
configure?: (context?: Readonly<Context>) =>
|
||||
| Partial<{
|
||||
title: string;
|
||||
description: string;
|
||||
path: string;
|
||||
onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
icon: IconName;
|
||||
category: string;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
// (Optional) A icon that can be displayed in the ui for the extension option.
|
||||
icon?: IconName;
|
||||
|
||||
// (Optional) A category to be used when grouping the options in the ui
|
||||
category?: string;
|
||||
};
|
||||
|
||||
export type PluginExtensionComponentConfig<Props = {}> = {
|
||||
type: PluginExtensionTypes.component;
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
// The React component that will be rendered as the extension
|
||||
// (This component receives contextual information as props when it is rendered. You can just return `null` from the component to hide it.)
|
||||
component: React.ComponentType<Props>;
|
||||
|
||||
/**
|
||||
* The unique identifier of the Extension Point
|
||||
* (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
||||
*/
|
||||
extensionPointId: string;
|
||||
};
|
||||
|
||||
export type PluginAddedComponentConfig<Props = {}> = {
|
||||
export type PluginExtensionAddedComponentConfig<Props = {}> = PluginExtensionConfigBase & {
|
||||
/**
|
||||
* The target extension points where the component will be added
|
||||
*/
|
||||
@ -117,23 +75,54 @@ export type PluginAddedComponentConfig<Props = {}> = {
|
||||
component: React.ComponentType<Props>;
|
||||
};
|
||||
|
||||
export type PluginExposedComponentConfig<Props = {}> = {
|
||||
export type PluginAddedLinksConfigureFunc<Context extends object> = (context?: Readonly<Context>) =>
|
||||
| Partial<{
|
||||
title: string;
|
||||
description: string;
|
||||
path: string;
|
||||
onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
icon: IconName;
|
||||
category: string;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
export type PluginExtensionAddedLinkConfig<Context extends object = object> = PluginExtensionConfigBase & {
|
||||
/**
|
||||
* The target extension points where the link will be added
|
||||
*/
|
||||
targets: string | string[];
|
||||
|
||||
/** A URL path that will be used as the href for the rendered link extension
|
||||
* (It is optional, because in some cases the action will be handled by the `onClick` handler instead of navigating to a new page)
|
||||
*/
|
||||
path?: string;
|
||||
|
||||
/** A URL path that will be used as the href for the rendered link extension
|
||||
* (It is optional, because in some cases the action will be handled by the `onClick` handler instead of navigating to a new page)
|
||||
* path?: string;
|
||||
*
|
||||
* A function that will be called when the link is clicked
|
||||
* (It is called with the original event object)
|
||||
*/
|
||||
onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
|
||||
// (Optional) A function that can be used to configure the extension dynamically based on the extension point's context
|
||||
configure?: PluginAddedLinksConfigureFunc<Context>;
|
||||
|
||||
// (Optional) A icon that can be displayed in the ui for the extension option.
|
||||
icon?: IconName;
|
||||
|
||||
// (Optional) A category to be used when grouping the options in the ui
|
||||
category?: string;
|
||||
};
|
||||
|
||||
export type PluginExtensionExposedComponentConfig<Props = {}> = PluginExtensionConfigBase & {
|
||||
/**
|
||||
* The unique identifier of the component
|
||||
* Shoud be in the format of `<pluginId>/<componentName>/<componentVersion>`. e.g. `myorg-todo-app/todo-list/v1`
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The title of the component
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* A short description of the component
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* The React component that will be exposed to other plugins
|
||||
*/
|
||||
@ -212,3 +201,61 @@ type Dashboard = {
|
||||
title: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
// deprecated types
|
||||
|
||||
/** @deprecated - use PluginAddedComponentConfig instead */
|
||||
export type PluginExtensionLinkConfig<Context extends object = object> = {
|
||||
type: PluginExtensionTypes.link;
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
// A URL path that will be used as the href for the rendered link extension
|
||||
// (It is optional, because in some cases the action will be handled by the `onClick` handler instead of navigating to a new page)
|
||||
path?: string;
|
||||
|
||||
// A function that will be called when the link is clicked
|
||||
// (It is called with the original event object)
|
||||
onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
|
||||
/**
|
||||
* The unique identifier of the Extension Point
|
||||
* (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
||||
*/
|
||||
extensionPointId: string;
|
||||
|
||||
// (Optional) A function that can be used to configure the extension dynamically based on the extension point's context
|
||||
configure?: (context?: Readonly<Context>) =>
|
||||
| Partial<{
|
||||
title: string;
|
||||
description: string;
|
||||
path: string;
|
||||
onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
icon: IconName;
|
||||
category: string;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
// (Optional) A icon that can be displayed in the ui for the extension option.
|
||||
icon?: IconName;
|
||||
|
||||
// (Optional) A category to be used when grouping the options in the ui
|
||||
category?: string;
|
||||
};
|
||||
|
||||
/** @deprecated - use PluginAddedLinkConfig instead */
|
||||
export type PluginExtensionComponentConfig<Props = {}> = {
|
||||
type: PluginExtensionTypes.component;
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
// The React component that will be rendered as the extension
|
||||
// (This component receives contextual information as props when it is rendered. You can just return `null` from the component to hide it.)
|
||||
component: React.ComponentType<Props>;
|
||||
|
||||
/**
|
||||
* The unique identifier of the Extension Point
|
||||
* (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
||||
*/
|
||||
extensionPointId: string;
|
||||
};
|
||||
|
@ -53,6 +53,7 @@ export interface AdHocVariableFilter {
|
||||
key: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
values?: string[];
|
||||
/** @deprecated */
|
||||
condition?: string;
|
||||
}
|
||||
|
@ -287,19 +287,19 @@ describe('clock', () => {
|
||||
describe('size greater than or equal 1 hour', () => {
|
||||
it('default', () => {
|
||||
const str = toClock(7199999);
|
||||
expect(formattedValueToString(str)).toBe('01h:59m:59s:999ms');
|
||||
expect(formattedValueToString(str)).toBe('1h:59m:59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = toClock(7199999, 0);
|
||||
expect(formattedValueToString(str)).toBe('01h');
|
||||
expect(formattedValueToString(str)).toBe('1h');
|
||||
});
|
||||
it('decimals equals 1', () => {
|
||||
const str = toClock(7199999, 1);
|
||||
expect(formattedValueToString(str)).toBe('01h:59m');
|
||||
expect(formattedValueToString(str)).toBe('1h:59m');
|
||||
});
|
||||
it('decimals equals 2', () => {
|
||||
const str = toClock(7199999, 2);
|
||||
expect(formattedValueToString(str)).toBe('01h:59m:59s');
|
||||
expect(formattedValueToString(str)).toBe('1h:59m:59s');
|
||||
});
|
||||
});
|
||||
describe('size greater than or equal 1 day', () => {
|
||||
@ -320,6 +320,24 @@ describe('clock', () => {
|
||||
expect(formattedValueToString(str)).toBe('24h:59m:59s');
|
||||
});
|
||||
});
|
||||
describe('size greater than or equal 100 hours', () => {
|
||||
it('default', () => {
|
||||
const str = toClock(363599999);
|
||||
expect(formattedValueToString(str)).toBe('100h:59m:59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = toClock(363599999, 0);
|
||||
expect(formattedValueToString(str)).toBe('100h');
|
||||
});
|
||||
it('decimals equals 1', () => {
|
||||
const str = toClock(363599999, 1);
|
||||
expect(formattedValueToString(str)).toBe('100h:59m');
|
||||
});
|
||||
it('decimals equals 2', () => {
|
||||
const str = toClock(363599999, 2);
|
||||
expect(formattedValueToString(str)).toBe('100h:59m:59s');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('to nanoseconds', () => {
|
||||
|
@ -275,7 +275,7 @@ export function toClock(size: number, decimals?: DecimalCount): FormattedValue {
|
||||
|
||||
let format = 'mm\\m:ss\\s:SSS\\m\\s';
|
||||
|
||||
const hours = `${('0' + Math.floor(duration(size, 'milliseconds').asHours())).slice(-2)}h`;
|
||||
const hours = `${Math.floor(duration(size, 'milliseconds').asHours())}h`;
|
||||
|
||||
if (decimals === 0) {
|
||||
format = '';
|
||||
|
@ -40,7 +40,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"esbuild": "0.20.2",
|
||||
"rimraf": "5.0.7",
|
||||
"rollup": "2.79.1",
|
||||
@ -50,7 +50,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/tsconfig": "^2.0.0",
|
||||
"tslib": "2.6.3",
|
||||
"tslib": "2.7.0",
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/types": "^6.0.0",
|
||||
"eslint": "8.57.0",
|
||||
"tslib": "2.6.3"
|
||||
"tslib": "2.7.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
@ -53,7 +53,7 @@
|
||||
"react-use": "17.5.1",
|
||||
"react-virtualized-auto-sizer": "1.0.24",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tslib": "2.6.3"
|
||||
"tslib": "2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.2",
|
||||
@ -68,7 +68,7 @@
|
||||
"@types/d3": "^7",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
|
@ -45,7 +45,7 @@
|
||||
"@svgr/plugin-prettier": "^8.1.0",
|
||||
"@svgr/plugin-svgo": "^8.1.0",
|
||||
"@types/babel__core": "^7",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"esbuild": "0.20.2",
|
||||
|
@ -27,7 +27,7 @@
|
||||
"react-select": "5.8.0",
|
||||
"react-use": "17.5.1",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.6.3"
|
||||
"tslib": "2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/tsconfig": "^2.0.0",
|
||||
@ -36,7 +36,7 @@
|
||||
"@testing-library/react": "15.0.2",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/systemjs": "6.13.5",
|
||||
"@types/testing-library__jest-dom": "5.14.9",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"version": "11.3.0-pre",
|
||||
"dependencies": {
|
||||
"tslib": "2.6.3"
|
||||
"tslib": "2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/tsconfig": "^2.0.0",
|
||||
|
@ -70,7 +70,7 @@
|
||||
"react-window": "1.8.10",
|
||||
"rxjs": "7.8.1",
|
||||
"semver": "7.6.3",
|
||||
"tslib": "2.6.3",
|
||||
"tslib": "2.7.0",
|
||||
"uuid": "9.0.1",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
},
|
||||
@ -92,7 +92,7 @@
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jquery": "3.5.30",
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/prismjs": "1.26.4",
|
||||
"@types/react": "18.3.3",
|
||||
@ -110,7 +110,7 @@
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "28.8.0",
|
||||
"eslint-plugin-jest": "28.8.1",
|
||||
"eslint-plugin-jsdoc": "48.11.0",
|
||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
||||
"eslint-plugin-lodash": "7.4.0",
|
||||
|
@ -603,7 +603,7 @@ export class PrometheusDatasource
|
||||
return this.languageProvider.getLabelKeys().map((k) => ({ value: k, text: k }));
|
||||
}
|
||||
|
||||
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({
|
||||
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map(remapOneOf).map((f) => ({
|
||||
label: f.key,
|
||||
value: f.value,
|
||||
op: f.operator,
|
||||
@ -620,7 +620,7 @@ export class PrometheusDatasource
|
||||
|
||||
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
|
||||
async getTagValues(options: DataSourceGetTagValuesOptions<PromQuery>) {
|
||||
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({
|
||||
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map(remapOneOf).map((f) => ({
|
||||
label: f.key,
|
||||
value: f.value,
|
||||
op: f.operator,
|
||||
@ -822,7 +822,7 @@ export class PrometheusDatasource
|
||||
return [];
|
||||
}
|
||||
|
||||
return filters.map((f) => ({
|
||||
return filters.map(remapOneOf).map((f) => ({
|
||||
...f,
|
||||
value: this.templateSrv.replace(f.value, {}, this.interpolateQueryExpr),
|
||||
operator: scopeFilterOperatorMap[f.operator],
|
||||
@ -834,7 +834,7 @@ export class PrometheusDatasource
|
||||
return expr;
|
||||
}
|
||||
|
||||
const finalQuery = filters.reduce((acc, filter) => {
|
||||
const finalQuery = filters.map(remapOneOf).reduce((acc, filter) => {
|
||||
const { key, operator } = filter;
|
||||
let { value } = filter;
|
||||
if (operator === '=~' || operator === '!~') {
|
||||
@ -1001,3 +1001,19 @@ export function prometheusRegularEscape<T>(value: T) {
|
||||
export function prometheusSpecialRegexEscape<T>(value: T) {
|
||||
return typeof value === 'string' ? value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&') : value;
|
||||
}
|
||||
|
||||
export function remapOneOf(filter: AdHocVariableFilter) {
|
||||
let { operator, value, values } = filter;
|
||||
if (operator === '=|') {
|
||||
operator = '=~';
|
||||
value = values?.map(prometheusRegularEscape).join('|') ?? '';
|
||||
} else if (operator === '!=|') {
|
||||
operator = '!~';
|
||||
value = values?.map(prometheusRegularEscape).join('|') ?? '';
|
||||
}
|
||||
return {
|
||||
...filter,
|
||||
operator,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
@ -45,7 +45,7 @@
|
||||
"history": "4.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.6.3"
|
||||
"tslib": "2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/tsconfig": "^2.0.0",
|
||||
|
@ -26,11 +26,11 @@ export {
|
||||
usePluginExtensions,
|
||||
usePluginLinkExtensions,
|
||||
usePluginComponentExtensions,
|
||||
usePluginLinks,
|
||||
} from './pluginExtensions/usePluginExtensions';
|
||||
|
||||
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
||||
export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents';
|
||||
export { setPluginLinksHook, usePluginLinks } from './pluginExtensions/usePluginLinks';
|
||||
|
||||
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
||||
export { setCurrentUser } from './user';
|
||||
|
@ -40,6 +40,17 @@ export type UsePluginComponentsResult<Props = {}> = {
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export type UsePluginLinksOptions = {
|
||||
extensionPointId: string;
|
||||
context?: object | Record<string | symbol, unknown>;
|
||||
limitPerPlugin?: number;
|
||||
};
|
||||
|
||||
export type UsePluginLinksResult = {
|
||||
isLoading: boolean;
|
||||
links: PluginExtensionLink[];
|
||||
};
|
||||
|
||||
let singleton: GetPluginExtensions | undefined;
|
||||
|
||||
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
||||
|
@ -25,20 +25,6 @@ export function usePluginExtensions(options: GetPluginExtensionsOptions): UsePlu
|
||||
return singleton(options);
|
||||
}
|
||||
|
||||
export function usePluginLinks(options: GetPluginExtensionsOptions): {
|
||||
links: PluginExtensionLink[];
|
||||
isLoading: boolean;
|
||||
} {
|
||||
const { extensions, isLoading } = usePluginExtensions(options);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
links: extensions.filter(isPluginExtensionLink),
|
||||
isLoading,
|
||||
};
|
||||
}, [extensions, isLoading]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use usePluginLinks() instead.
|
||||
*/
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { UsePluginLinksOptions, UsePluginLinksResult } from './getPluginExtensions';
|
||||
|
||||
export type UsePluginLinks = (options: UsePluginLinksOptions) => UsePluginLinksResult;
|
||||
|
||||
let singleton: UsePluginLinks | undefined;
|
||||
|
||||
export function setPluginLinksHook(hook: UsePluginLinks): void {
|
||||
// We allow overriding the registry in tests
|
||||
if (singleton && process.env.NODE_ENV !== 'test') {
|
||||
throw new Error('setPluginLinksHook() function should only be called once, when Grafana is starting.');
|
||||
}
|
||||
singleton = hook;
|
||||
}
|
||||
|
||||
export function usePluginLinks(options: UsePluginLinksOptions): UsePluginLinksResult {
|
||||
if (!singleton) {
|
||||
throw new Error('setPluginLinksHook(options) can only be used after the Grafana instance has started.');
|
||||
}
|
||||
return singleton(options);
|
||||
}
|
@ -48,6 +48,6 @@
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "2.6.3"
|
||||
"tslib": "2.7.0"
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@
|
||||
"react-virtualized-auto-sizer": "1.0.24",
|
||||
"rxjs": "7.8.1",
|
||||
"sql-formatter-plus": "^1.3.6",
|
||||
"tslib": "2.6.3",
|
||||
"tslib": "2.7.0",
|
||||
"uuid": "9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -42,7 +42,7 @@
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.4",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Input } from '@grafana/ui/src/components/Input/Input';
|
||||
import { Input } from '@grafana/ui';
|
||||
|
||||
type NumberInputProps = {
|
||||
value: number;
|
||||
|
@ -109,7 +109,7 @@
|
||||
"slate-plain-serializer": "0.7.13",
|
||||
"slate-react": "0.22.10",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tslib": "2.6.3",
|
||||
"tslib": "2.7.0",
|
||||
"uplot": "1.6.30",
|
||||
"uuid": "9.0.1"
|
||||
},
|
||||
@ -145,7 +145,7 @@
|
||||
"@types/is-hotkey": "0.1.10",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/mock-raf": "1.0.6",
|
||||
"@types/node": "20.16.2",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/prismjs": "1.26.4",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-color": "3.0.12",
|
||||
|
@ -97,9 +97,9 @@ const ManyOptionsStory: StoryFn<PropsAndCustomArgs> = ({ numberOfOptions, ...arg
|
||||
loading={isLoading}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
setValue(val?.value || null);
|
||||
action('onChange')(val);
|
||||
onChange={(opt) => {
|
||||
setValue(opt?.value || null);
|
||||
action('onChange')(opt);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -79,7 +79,7 @@ describe('Combobox', () => {
|
||||
});
|
||||
|
||||
it('clears selected value', async () => {
|
||||
render(<Combobox options={options} value={options[1].value} onChange={onChangeHandler} />);
|
||||
render(<Combobox options={options} value={options[1].value} onChange={onChangeHandler} isClearable />);
|
||||
|
||||
expect(screen.queryByDisplayValue('Option 2')).toBeInTheDocument();
|
||||
const input = screen.getByRole('combobox');
|
||||
|
@ -23,10 +23,12 @@ interface ComboboxProps
|
||||
onChange: (val: Option | null) => void;
|
||||
value: Value | null;
|
||||
options: Option[];
|
||||
isClearable?: boolean;
|
||||
createCustomValue?: boolean;
|
||||
}
|
||||
|
||||
function itemToString(item: Option | null) {
|
||||
return item?.label ?? '';
|
||||
return item?.label ?? item?.value?.toString() ?? '';
|
||||
}
|
||||
|
||||
function itemFilter(inputValue: string) {
|
||||
@ -51,13 +53,44 @@ const INDEX_WIDTH_CALCULATION = 100;
|
||||
// A multiplier guesstimate times the amount of characters. If any padding or image support etc. is added this will need to be updated.
|
||||
const WIDTH_MULTIPLIER = 7.3;
|
||||
|
||||
export const Combobox = ({ options, onChange, value, id, ...restProps }: ComboboxProps) => {
|
||||
export const Combobox = ({
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
isClearable = false,
|
||||
createCustomValue = false,
|
||||
id,
|
||||
...restProps
|
||||
}: ComboboxProps) => {
|
||||
const [items, setItems] = useState(options);
|
||||
const selectedItemIndex = useMemo(
|
||||
() => options.findIndex((option) => option.value === value) || null,
|
||||
[options, value]
|
||||
);
|
||||
const selectedItem = selectedItemIndex ? options[selectedItemIndex] : null;
|
||||
|
||||
const selectedItemIndex = useMemo(() => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = options.findIndex((option) => option.value === value);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return index;
|
||||
}, [options, value]);
|
||||
|
||||
const selectedItem = useMemo(() => {
|
||||
if (selectedItemIndex) {
|
||||
return options[selectedItemIndex];
|
||||
}
|
||||
|
||||
// Custom value
|
||||
if (value !== null) {
|
||||
return {
|
||||
label: value.toString(),
|
||||
value,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [selectedItemIndex, options, value]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const floatingRef = useRef<HTMLDivElement>(null);
|
||||
@ -82,18 +115,34 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
isOpen,
|
||||
highlightedIndex,
|
||||
setInputValue,
|
||||
selectItem,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
selectItem,
|
||||
} = useCombobox({
|
||||
inputId: id,
|
||||
items,
|
||||
itemToString,
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem, inputValue }) => {
|
||||
onChange(selectedItem);
|
||||
},
|
||||
defaultHighlightedIndex: selectedItemIndex ?? undefined,
|
||||
scrollIntoView: () => {},
|
||||
onInputValueChange: ({ inputValue }) => {
|
||||
setItems(options.filter(itemFilter(inputValue)));
|
||||
const filteredItems = options.filter(itemFilter(inputValue));
|
||||
if (createCustomValue && inputValue && filteredItems.findIndex((opt) => opt.label === inputValue) === -1) {
|
||||
setItems([
|
||||
...filteredItems,
|
||||
{
|
||||
label: inputValue,
|
||||
value: inputValue,
|
||||
description: t('combobox.custom-value.create', 'Create custom value'),
|
||||
},
|
||||
]);
|
||||
return;
|
||||
} else {
|
||||
setItems(filteredItems);
|
||||
}
|
||||
},
|
||||
onIsOpenChange: ({ isOpen }) => {
|
||||
// Default to displaying all values when opening
|
||||
@ -102,9 +151,6 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
return;
|
||||
}
|
||||
},
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
onChange(selectedItem);
|
||||
},
|
||||
onHighlightedIndexChange: ({ highlightedIndex, type }) => {
|
||||
if (type !== useCombobox.stateChangeTypes.MenuMouseLeave) {
|
||||
rowVirtualizer.scrollToIndex(highlightedIndex);
|
||||
@ -113,8 +159,8 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
});
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
setInputValue(selectedItem?.label ?? '');
|
||||
}, [selectedItem, setInputValue]);
|
||||
setInputValue(selectedItem?.label ?? value?.toString() ?? '');
|
||||
}, [selectedItem, setInputValue, value]);
|
||||
|
||||
// the order of middleware is important!
|
||||
const middleware = [
|
||||
@ -147,7 +193,7 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
<Input
|
||||
suffix={
|
||||
<>
|
||||
{!!value && value === selectedItem?.value && (
|
||||
{!!value && value === selectedItem?.value && isClearable && (
|
||||
<Icon
|
||||
name="times"
|
||||
className={styles.clear}
|
||||
@ -202,7 +248,7 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
return (
|
||||
<li
|
||||
key={items[virtualRow.index].value}
|
||||
key={items[virtualRow.index].value + items[virtualRow.index].label}
|
||||
data-index={virtualRow.index}
|
||||
className={cx(
|
||||
styles.option,
|
||||
|
@ -28,10 +28,11 @@ export function TableCellInspector({ value, onDismiss, mode }: TableCellInspecto
|
||||
if (trimmedValue[0] === '{' || trimmedValue[0] === '[' || mode === 'code') {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
displayValue = JSON.stringify(value, null, '');
|
||||
} catch {}
|
||||
}
|
||||
} else {
|
||||
displayValue = JSON.stringify(value, null, ' ');
|
||||
displayValue = JSON.stringify(value, null, '');
|
||||
}
|
||||
let text = displayValue;
|
||||
|
||||
|
@ -18,15 +18,75 @@ export function getFormElementStyles(theme: GrafanaTheme2) {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
|
||||
textarea: {
|
||||
// Placeholder text gets special styles because when browsers invalidate entire lines if it doesn't understand a selector
|
||||
'input, textarea': {
|
||||
'&::placeholder': {
|
||||
color: theme.colors.text.disabled,
|
||||
},
|
||||
},
|
||||
|
||||
// not a big fan of number fields
|
||||
'input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button': {
|
||||
WebkitAppearance: 'none',
|
||||
margin: 0,
|
||||
},
|
||||
'input[type="number"]': {
|
||||
MozAppearance: 'textfield',
|
||||
},
|
||||
|
||||
// Set the height of select and file controls to match text inputs
|
||||
'select, input[type="file"]': {
|
||||
height:
|
||||
theme.components.height
|
||||
.md /* In IE7, the height of the select element cannot be changed by height, only font-size */,
|
||||
lineHeight: theme.components.height.md,
|
||||
},
|
||||
|
||||
// Make select elements obey height by applying a border
|
||||
select: {
|
||||
width: '220px', // default input width + 10px of padding that doesn't get applied
|
||||
border: `1px solid ${theme.components.input.borderColor}`,
|
||||
backgroundColor: theme.components.input.background, // Chrome on Linux and Mobile Safari need background-color
|
||||
},
|
||||
|
||||
'select[multiple], select[size], textarea': {
|
||||
height: 'auto',
|
||||
},
|
||||
|
||||
// Focus for select, file, radio, and checkbox
|
||||
'select:focus, input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus': {
|
||||
// WebKit
|
||||
outline: '5px auto -webkit-focus-ring-color',
|
||||
outlineOffset: '-2px',
|
||||
},
|
||||
|
||||
// Reset width of input images, buttons, radios, checkboxes
|
||||
"input[type='file'], input[type='image'], input[type='submit'], input[type='reset'], input[type='button'], input[type='radio'], input[type='checkbox']":
|
||||
{
|
||||
width: 'auto', // Override of generic input selector
|
||||
},
|
||||
|
||||
// Disabled and read-only inputs
|
||||
'input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly]': {
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: theme.colors.action.disabledBackground,
|
||||
},
|
||||
|
||||
// Explicitly reset the colors here
|
||||
'input[type="radio"][disabled], input[type="checkbox"][disabled], input[type="radio"][readonly], input[type="checkbox"][readonly]':
|
||||
{
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
|
||||
'input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill, textarea:-webkit-autofill, textarea:-webkit-autofill:hover, textarea:-webkit-autofill:focus, select:-webkit-autofill, select:-webkit-autofill:hover, select:-webkit-autofill:focus':
|
||||
{
|
||||
WebkitBoxShadow: `0 0 0px 1000px ${theme.components.input.background} inset !important`,
|
||||
WebkitTextFillColor: theme.components.input.text,
|
||||
boxShadow: `0 0 0px 1000px ${theme.components.input.background} inset`,
|
||||
border: `1px solid ${theme.components.input.background}`,
|
||||
},
|
||||
|
||||
'.gf-form': {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
@ -367,5 +427,8 @@ export function getFormElementStyles(theme: GrafanaTheme2) {
|
||||
marginBottom: theme.spacing(3),
|
||||
borderTop: `3px solid ${theme.colors.success.main}`,
|
||||
},
|
||||
'.input-small': {
|
||||
width: '90px',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
// Deprecated: true.
|
||||
//
|
||||
// Deprecated. Please use GET /api/serviceaccounts and GET /api/serviceaccounts/{id}/tokens instead
|
||||
// see https://grafana.com/docs/grafana/next/administration/api-keys/#migrate-api-keys-to-grafana-service-accounts-using-the-api.
|
||||
// see https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.
|
||||
//
|
||||
// Responses:
|
||||
// 200: getAPIkeyResponse
|
||||
@ -71,7 +71,7 @@ func (hs *HTTPServer) GetAPIKeys(c *contextmodel.ReqContext) response.Response {
|
||||
// Delete API key.
|
||||
//
|
||||
// Deletes an API key.
|
||||
// Deprecated. See: https://grafana.com/docs/grafana/next/administration/api-keys/#migrate-api-keys-to-grafana-service-accounts-using-the-api.
|
||||
// Deprecated. See: https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.
|
||||
//
|
||||
// Deprecated: true
|
||||
// Responses:
|
||||
@ -110,7 +110,7 @@ func (hs *HTTPServer) DeleteAPIKey(c *contextmodel.ReqContext) response.Response
|
||||
// Deprecated: true
|
||||
// Deprecated. Please use POST /api/serviceaccounts and POST /api/serviceaccounts/{id}/tokens
|
||||
//
|
||||
// see: https://grafana.com/docs/grafana/next/administration/api-keys/#migrate-api-keys-to-grafana-service-accounts-using-the-api.
|
||||
// see: https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.
|
||||
//
|
||||
// Responses:
|
||||
// 301: statusMovedPermanently
|
||||
|
@ -449,11 +449,12 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
|
||||
dsDTO.Preload = plugin.Preload
|
||||
dsDTO.Module = plugin.Module
|
||||
dsDTO.PluginMeta = &plugins.PluginMetaDTO{
|
||||
JSONData: plugin.JSONData,
|
||||
Signature: plugin.Signature,
|
||||
Module: plugin.Module,
|
||||
BaseURL: plugin.BaseURL,
|
||||
Angular: plugin.Angular,
|
||||
JSONData: plugin.JSONData,
|
||||
Signature: plugin.Signature,
|
||||
Module: plugin.Module,
|
||||
BaseURL: plugin.BaseURL,
|
||||
Angular: plugin.Angular,
|
||||
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
||||
}
|
||||
|
||||
if ds.JsonData == nil {
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1"
|
||||
playlistalpha1 "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
internalplaylist "github.com/grafana/grafana/pkg/registry/apis/playlist"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
|
||||
@ -27,6 +27,7 @@ import (
|
||||
func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) {
|
||||
// Register the actual handlers
|
||||
apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) {
|
||||
// TODO: remove kubernetesPlaylists feature flag
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesPlaylists) {
|
||||
// Use k8s client to implement legacy API
|
||||
handler := newPlaylistK8sHandler(hs)
|
||||
@ -330,7 +331,7 @@ type playlistK8sHandler struct {
|
||||
|
||||
func newPlaylistK8sHandler(hs *HTTPServer) *playlistK8sHandler {
|
||||
return &playlistK8sHandler{
|
||||
gvr: v0alpha1.PlaylistResourceInfo.GroupVersionResource(),
|
||||
gvr: playlistalpha1.PlaylistResourceInfo.GroupVersionResource(),
|
||||
namespacer: request.GetNamespaceMapper(hs.Cfg),
|
||||
clientConfigProvider: hs.clientConfigProvider,
|
||||
}
|
||||
|
@ -161,6 +161,7 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) {
|
||||
schema.GroupVersion{Group: GROUP, Version: version},
|
||||
&User{},
|
||||
&UserList{},
|
||||
&UserTeamList{},
|
||||
&ServiceAccount{},
|
||||
&ServiceAccountList{},
|
||||
&Team{},
|
||||
|
@ -13,7 +13,7 @@ type Team struct {
|
||||
}
|
||||
|
||||
type TeamSpec struct {
|
||||
Title string `json:"name,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -25,3 +25,17 @@ type UserList struct {
|
||||
|
||||
Items []User `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type UserTeamList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []UserTeam `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type UserTeam struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
TeamRef TeamRef `json:"teamRef,omitempty"`
|
||||
Permission TeamPermission `json:"permission,omitempty"`
|
||||
}
|
||||
|
@ -533,3 +533,51 @@ func (in *UserSpec) DeepCopy() *UserSpec {
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UserTeam) DeepCopyInto(out *UserTeam) {
|
||||
*out = *in
|
||||
out.TeamRef = in.TeamRef
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserTeam.
|
||||
func (in *UserTeam) DeepCopy() *UserTeam {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UserTeam)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UserTeamList) DeepCopyInto(out *UserTeamList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]UserTeam, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserTeamList.
|
||||
func (in *UserTeamList) DeepCopy() *UserTeamList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UserTeamList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *UserTeamList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.User": schema_pkg_apis_identity_v0alpha1_User(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserList": schema_pkg_apis_identity_v0alpha1_UserList(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserSpec": schema_pkg_apis_identity_v0alpha1_UserSpec(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserTeam": schema_pkg_apis_identity_v0alpha1_UserTeam(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserTeamList": schema_pkg_apis_identity_v0alpha1_UserTeamList(ref),
|
||||
}
|
||||
}
|
||||
|
||||
@ -763,7 +765,7 @@ func schema_pkg_apis_identity_v0alpha1_TeamSpec(ref common.ReferenceCallback) co
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"name": {
|
||||
"title": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
@ -936,3 +938,84 @@ func schema_pkg_apis_identity_v0alpha1_UserSpec(ref common.ReferenceCallback) co
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_identity_v0alpha1_UserTeam(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"title": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"teamRef": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef"),
|
||||
},
|
||||
},
|
||||
"permission": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Possible enum values:\n - `\"admin\"`\n - `\"member\"`",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
Enum: []interface{}{"admin", "member"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_identity_v0alpha1_UserTeamList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "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",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "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",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserTeam"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserTeam", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,TeamBindingSpec,Subjects
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,IdentityDisplay,IdentityType
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,IdentityDisplay,InternalID
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,TeamSpec,Title
|
||||
|
@ -171,13 +171,11 @@ type Signature struct {
|
||||
|
||||
type PluginMetaDTO struct {
|
||||
JSONData
|
||||
|
||||
Signature SignatureStatus `json:"signature"`
|
||||
|
||||
Module string `json:"module"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
|
||||
Angular AngularMeta `json:"angular"`
|
||||
Signature SignatureStatus `json:"signature"`
|
||||
Module string `json:"module"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Angular AngularMeta `json:"angular"`
|
||||
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
||||
}
|
||||
|
||||
type DataSourceDTO struct {
|
||||
|
@ -112,18 +112,19 @@ type JSONData struct {
|
||||
AutoEnabled bool `json:"autoEnabled"`
|
||||
|
||||
// Datasource settings
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
Explore bool `json:"explore"`
|
||||
Table bool `json:"tables"`
|
||||
Logs bool `json:"logs"`
|
||||
Tracing bool `json:"tracing"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
Mixed bool `json:"mixed,omitempty"`
|
||||
Streaming bool `json:"streaming"`
|
||||
SDK bool `json:"sdk,omitempty"`
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
Explore bool `json:"explore"`
|
||||
Table bool `json:"tables"`
|
||||
Logs bool `json:"logs"`
|
||||
Tracing bool `json:"tracing"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
Mixed bool `json:"mixed,omitempty"`
|
||||
Streaming bool `json:"streaming"`
|
||||
SDK bool `json:"sdk,omitempty"`
|
||||
MultiValueFilterOperators bool `json:"multiValueFilterOperators,omitempty"`
|
||||
|
||||
// Backend (Datasource + Renderer + SecretsManager)
|
||||
Executable string `json:"executable,omitempty"`
|
||||
|
@ -2,6 +2,9 @@ package common
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
identityv0 "github.com/grafana/grafana/pkg/apis/identity/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
)
|
||||
|
||||
// OptonalFormatInt formats num as a string. If num is less or equal than 0
|
||||
@ -12,3 +15,11 @@ func OptionalFormatInt(num int64) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func MapTeamPermission(p team.PermissionType) identityv0.TeamPermission {
|
||||
if p == team.PermissionTypeAdmin {
|
||||
return identityv0.TeamPermissionAdmin
|
||||
} else {
|
||||
return identityv0.TeamPermissionMember
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
)
|
||||
|
||||
@ -16,12 +15,11 @@ type LegacyIdentityStore interface {
|
||||
ListDisplay(ctx context.Context, ns claims.NamespaceInfo, query ListDisplayQuery) (*ListUserResult, error)
|
||||
|
||||
ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error)
|
||||
ListUserTeams(ctx context.Context, ns claims.NamespaceInfo, query ListUserTeamsQuery) (*ListUserTeamsResult, error)
|
||||
|
||||
ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error)
|
||||
ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error)
|
||||
ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error)
|
||||
|
||||
GetUserTeams(ctx context.Context, ns claims.NamespaceInfo, uid string) ([]team.Team, error)
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -48,6 +48,12 @@ func TestIdentityQueries(t *testing.T) {
|
||||
return &v
|
||||
}
|
||||
|
||||
listUserTeams := func(q *ListUserTeamsQuery) sqltemplate.SQLTemplate {
|
||||
v := newListUserTeams(nodb, q)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
|
||||
RootDir: "testdata",
|
||||
Templates: map[*template.Template][]mocks.TemplateTestCase{
|
||||
@ -168,6 +174,24 @@ func TestIdentityQueries(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
sqlQueryUserTeamsTemplate: {
|
||||
{
|
||||
Name: "team_1_members_page_1",
|
||||
Data: listUserTeams(&ListUserTeamsQuery{
|
||||
UserUID: "user-1",
|
||||
OrgID: 1,
|
||||
Pagination: common.Pagination{Limit: 1},
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "team_1_members_page_2",
|
||||
Data: listUserTeams(&ListUserTeamsQuery{
|
||||
UserUID: "user-1",
|
||||
OrgID: 1,
|
||||
Pagination: common.Pagination{Limit: 1, Continue: 2},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -328,8 +328,3 @@ func scanMember(rows *sql.Rows) (TeamMember, error) {
|
||||
err := rows.Scan(&m.ID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.UserID, &m.Name, &m.Email, &m.Username, &m.External, &m.Created, &m.Updated, &m.Permission)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// GetUserTeams implements LegacyIdentityStore.
|
||||
func (s *legacySQLStore) GetUserTeams(ctx context.Context, ns claims.NamespaceInfo, uid string) ([]team.Team, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
8
pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_1.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_1.sql
vendored
Executable file
@ -0,0 +1,8 @@
|
||||
SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission
|
||||
FROM `grafana`.`user` u
|
||||
INNER JOIN `grafana`.`team_member` tm on u.id = tm.user_id
|
||||
INNER JOIN `grafana`.`team`t on tm.team_id = t.id
|
||||
WHERE u.uid = 'user-1'
|
||||
AND t.org_id = 1
|
||||
ORDER BY t.id ASC
|
||||
LIMIT 1;
|
9
pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_2.sql
vendored
Executable file
9
pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_2.sql
vendored
Executable file
@ -0,0 +1,9 @@
|
||||
SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission
|
||||
FROM `grafana`.`user` u
|
||||
INNER JOIN `grafana`.`team_member` tm on u.id = tm.user_id
|
||||
INNER JOIN `grafana`.`team`t on tm.team_id = t.id
|
||||
WHERE u.uid = 'user-1'
|
||||
AND t.org_id = 1
|
||||
AND t.id >= 2
|
||||
ORDER BY t.id ASC
|
||||
LIMIT 1;
|
8
pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_1.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_1.sql
vendored
Executable file
@ -0,0 +1,8 @@
|
||||
SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission
|
||||
FROM "grafana"."user" u
|
||||
INNER JOIN "grafana"."team_member" tm on u.id = tm.user_id
|
||||
INNER JOIN "grafana"."team"t on tm.team_id = t.id
|
||||
WHERE u.uid = 'user-1'
|
||||
AND t.org_id = 1
|
||||
ORDER BY t.id ASC
|
||||
LIMIT 1;
|
9
pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_2.sql
vendored
Executable file
9
pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_2.sql
vendored
Executable file
@ -0,0 +1,9 @@
|
||||
SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission
|
||||
FROM "grafana"."user" u
|
||||
INNER JOIN "grafana"."team_member" tm on u.id = tm.user_id
|
||||
INNER JOIN "grafana"."team"t on tm.team_id = t.id
|
||||
WHERE u.uid = 'user-1'
|
||||
AND t.org_id = 1
|
||||
AND t.id >= 2
|
||||
ORDER BY t.id ASC
|
||||
LIMIT 1;
|
8
pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_1.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_1.sql
vendored
Executable file
@ -0,0 +1,8 @@
|
||||
SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission
|
||||
FROM "grafana"."user" u
|
||||
INNER JOIN "grafana"."team_member" tm on u.id = tm.user_id
|
||||
INNER JOIN "grafana"."team"t on tm.team_id = t.id
|
||||
WHERE u.uid = 'user-1'
|
||||
AND t.org_id = 1
|
||||
ORDER BY t.id ASC
|
||||
LIMIT 1;
|
9
pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_2.sql
vendored
Executable file
9
pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_2.sql
vendored
Executable file
@ -0,0 +1,9 @@
|
||||
SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission
|
||||
FROM "grafana"."user" u
|
||||
INNER JOIN "grafana"."team_member" tm on u.id = tm.user_id
|
||||
INNER JOIN "grafana"."team"t on tm.team_id = t.id
|
||||
WHERE u.uid = 'user-1'
|
||||
AND t.org_id = 1
|
||||
AND t.id >= 2
|
||||
ORDER BY t.id ASC
|
||||
LIMIT 1;
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/identity/common"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
@ -109,3 +110,95 @@ func (s *legacySQLStore) queryUsers(ctx context.Context, sql *legacysql.LegacyDa
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
type ListUserTeamsQuery struct {
|
||||
UserUID string
|
||||
OrgID int64
|
||||
Pagination common.Pagination
|
||||
}
|
||||
|
||||
type ListUserTeamsResult struct {
|
||||
Continue int64
|
||||
Items []UserTeam
|
||||
}
|
||||
|
||||
type UserTeam struct {
|
||||
ID int64
|
||||
UID string
|
||||
Name string
|
||||
Permission team.PermissionType
|
||||
}
|
||||
|
||||
var sqlQueryUserTeamsTemplate = mustTemplate("user_teams_query.sql")
|
||||
|
||||
func newListUserTeams(sql *legacysql.LegacyDatabaseHelper, q *ListUserTeamsQuery) listUserTeamsQuery {
|
||||
return listUserTeamsQuery{
|
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||
UserTable: sql.Table("user"),
|
||||
TeamTable: sql.Table("team"),
|
||||
TeamMemberTable: sql.Table("team_member"),
|
||||
Query: q,
|
||||
}
|
||||
}
|
||||
|
||||
type listUserTeamsQuery struct {
|
||||
sqltemplate.SQLTemplate
|
||||
Query *ListUserTeamsQuery
|
||||
UserTable string
|
||||
TeamTable string
|
||||
TeamMemberTable string
|
||||
}
|
||||
|
||||
func (r listUserTeamsQuery) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *legacySQLStore) ListUserTeams(ctx context.Context, ns claims.NamespaceInfo, query ListUserTeamsQuery) (*ListUserTeamsResult, error) {
|
||||
query.Pagination.Limit += 1
|
||||
query.OrgID = ns.OrgID
|
||||
if query.OrgID == 0 {
|
||||
return nil, fmt.Errorf("expected non zero org id")
|
||||
}
|
||||
|
||||
sql, err := s.sql(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := newListUserTeams(sql, &query)
|
||||
q, err := sqltemplate.Execute(sqlQueryUserTeamsTemplate, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeamsTemplate.Name(), err)
|
||||
}
|
||||
|
||||
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
|
||||
defer func() {
|
||||
if rows != nil {
|
||||
_ = rows.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &ListUserTeamsResult{}
|
||||
var lastID int64
|
||||
for rows.Next() {
|
||||
t := UserTeam{}
|
||||
err := rows.Scan(&t.ID, &t.UID, &t.Name, &t.Permission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lastID = t.ID
|
||||
res.Items = append(res.Items, t)
|
||||
if len(res.Items) > int(query.Pagination.Limit)-1 {
|
||||
res.Continue = lastID
|
||||
res.Items = res.Items[0 : len(res.Items)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
11
pkg/registry/apis/identity/legacy/user_teams_query.sql
Normal file
11
pkg/registry/apis/identity/legacy/user_teams_query.sql
Normal file
@ -0,0 +1,11 @@
|
||||
SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission
|
||||
FROM {{ .Ident .UserTable }} u
|
||||
INNER JOIN {{ .Ident .TeamMemberTable }} tm on u.id = tm.user_id
|
||||
INNER JOIN {{ .Ident .TeamTable }}t on tm.team_id = t.id
|
||||
WHERE u.uid = {{ .Arg .Query.UserUID }}
|
||||
AND t.org_id = {{ .Arg .Query.OrgID }}
|
||||
{{- if .Query.Pagination.Continue }}
|
||||
AND t.id >= {{ .Arg .Query.Pagination.Continue }}
|
||||
{{- end }}
|
||||
ORDER BY t.id ASC
|
||||
LIMIT {{ .Arg .Query.Pagination.Limit }};
|
@ -89,7 +89,7 @@ func (b *IdentityAPIBuilder) GetAPIGroupInfo(
|
||||
|
||||
userResource := identityv0.UserResourceInfo
|
||||
storage[userResource.StoragePath()] = user.NewLegacyStore(b.Store)
|
||||
storage[userResource.StoragePath("teams")] = team.NewLegacyUserTeamsStore(b.Store)
|
||||
storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.Store)
|
||||
|
||||
serviceaccountResource := identityv0.ServiceAccountResourceInfo
|
||||
storage[serviceaccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.Store)
|
||||
@ -100,7 +100,7 @@ func (b *IdentityAPIBuilder) GetAPIGroupInfo(
|
||||
}
|
||||
|
||||
// The display endpoint -- NOTE, this uses a rewrite hack to allow requests without a name parameter
|
||||
storage["display"] = user.NewLegacyDisplayStore(b.Store)
|
||||
storage["display"] = user.NewLegacyDisplayREST(b.Store)
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap[identityv0.VERSION] = storage
|
||||
return &apiGroupInfo, nil
|
||||
|
@ -62,8 +62,6 @@ func (s *LegacyTeamMemberREST) Connect(ctx context.Context, name string, options
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = common.PaginationFromListQuery(r.URL.Query())
|
||||
|
||||
res, err := s.store.ListTeamMembers(ctx, ns, legacy.ListTeamMembersQuery{
|
||||
UID: name,
|
||||
Pagination: common.PaginationFromListQuery(r.URL.Query()),
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/identity/common"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/identity/legacy"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -130,28 +129,3 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO
|
||||
}
|
||||
return nil, resource.NewNotFound(name)
|
||||
}
|
||||
|
||||
func asTeam(team *team.Team, ns string) (*identityv0.Team, error) {
|
||||
item := &identityv0.Team{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: team.UID,
|
||||
Namespace: ns,
|
||||
CreationTimestamp: metav1.NewTime(team.Created),
|
||||
ResourceVersion: strconv.FormatInt(team.Updated.UnixMilli(), 10),
|
||||
},
|
||||
Spec: identityv0.TeamSpec{
|
||||
Title: team.Name,
|
||||
Email: team.Email,
|
||||
},
|
||||
}
|
||||
meta, err := utils.MetaAccessor(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta.SetUpdatedTimestamp(&team.Updated)
|
||||
meta.SetOriginInfo(&utils.ResourceOriginInfo{
|
||||
Name: "SQL",
|
||||
Path: strconv.FormatInt(team.ID, 10),
|
||||
})
|
||||
return item, nil
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ func mapToSubjects(members []legacy.TeamMember) []identityv0.TeamSubject {
|
||||
for _, m := range members {
|
||||
out = append(out, identityv0.TeamSubject{
|
||||
Name: m.MemberID(),
|
||||
Permission: mapPermisson(m.Permission),
|
||||
Permission: common.MapTeamPermission(m.Permission),
|
||||
})
|
||||
}
|
||||
return out
|
||||
|
@ -1,88 +0,0 @@
|
||||
package team
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
identityv0 "github.com/grafana/grafana/pkg/apis/identity/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/identity/legacy"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Storage = (*LegacyUserTeamsStore)(nil)
|
||||
_ rest.SingularNameProvider = (*LegacyUserTeamsStore)(nil)
|
||||
_ rest.Connecter = (*LegacyUserTeamsStore)(nil)
|
||||
_ rest.Scoper = (*LegacyUserTeamsStore)(nil)
|
||||
_ rest.StorageMetadata = (*LegacyUserTeamsStore)(nil)
|
||||
)
|
||||
|
||||
func NewLegacyUserTeamsStore(store legacy.LegacyIdentityStore) *LegacyUserTeamsStore {
|
||||
return &LegacyUserTeamsStore{
|
||||
logger: log.New("user teams"),
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
type LegacyUserTeamsStore struct {
|
||||
logger log.Logger
|
||||
store legacy.LegacyIdentityStore
|
||||
}
|
||||
|
||||
func (r *LegacyUserTeamsStore) New() runtime.Object {
|
||||
return &identityv0.TeamList{}
|
||||
}
|
||||
|
||||
func (r *LegacyUserTeamsStore) Destroy() {}
|
||||
|
||||
func (r *LegacyUserTeamsStore) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *LegacyUserTeamsStore) GetSingularName() string {
|
||||
return "TeamList"
|
||||
}
|
||||
|
||||
func (r *LegacyUserTeamsStore) ProducesMIMETypes(verb string) []string {
|
||||
return []string{"application/json"}
|
||||
}
|
||||
|
||||
func (r *LegacyUserTeamsStore) ProducesObject(verb string) interface{} {
|
||||
return &identityv0.TeamList{}
|
||||
}
|
||||
|
||||
func (r *LegacyUserTeamsStore) ConnectMethods() []string {
|
||||
return []string{"GET"}
|
||||
}
|
||||
|
||||
func (r *LegacyUserTeamsStore) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, "" // true means you can use the trailing path as a variable
|
||||
}
|
||||
|
||||
func (r *LegacyUserTeamsStore) Connect(ctx context.Context, name string, _ runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
teams, err := r.store.GetUserTeams(ctx, ns, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
||||
list := &identityv0.TeamList{}
|
||||
for _, team := range teams {
|
||||
t, err := asTeam(&team, ns.Value)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
list.Items = append(list.Items, *t)
|
||||
}
|
||||
responder.Object(200, list)
|
||||
}), nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user