Compare commits

...

86 Commits

Author SHA1 Message Date
Grot (@grafanabot)
e9cb2a313e [v9.3.x] Fix XSS in runbook URL (#684)
Fix XSS in runbook URL (#681)

(cherry picked from commit db1548c1491c2f5b522e3c0ceb1832b914a4b2f0)

Co-authored-by: George Robinson <george.robinson@grafana.com>
2022-11-29 18:03:52 +02:00
Grot (@grafanabot)
08d483e0bd [v9.3.x] Docs: Improve docs for images in notifications (#59402)
Docs: Improve docs for images in notifications (#59033)

(cherry picked from commit 0af3515e95)

Co-authored-by: George Robinson <george.robinson@grafana.com>
2022-11-28 09:23:11 -05:00
Grot (@grafanabot)
05948043e8 [v9.3.x] Docs: Improve Labels and annotations docs (#59401)
Docs: Improve Labels and annotations docs (#59325)

This commit makes a number of changes to the docs for Labels and
annotations.

 1. It changes the order in which Labels and annotations are mentioned
    from Annotations and labels to Labels and annotations as this is
    the order shown in the UI when creating and editing alert rules.
    It is also the order in the Prometheus documentation.

 2. It changes most of the documentation explaining what labels and
    annotations are and the differences between them. It also adds
    some paragraphs on Custom Labels.

(cherry picked from commit e7316ff13f)

Co-authored-by: George Robinson <george.robinson@grafana.com>
2022-11-28 09:21:41 -05:00
Grot (@grafanabot)
3513179722 [v9.3.x] BarChart: fix hover overlay for hz stacked (#59396)
BarChart: fix hover overlay for hz stacked (#59359)

(cherry picked from commit 13d5ad2ce2)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2022-11-28 09:02:33 -05:00
Erik Sundell
c652135724 [v9.3.x] CloudWatch: Cross-account querying support (#59389)
* add cross-account querying support

* fix tests
2022-11-28 14:21:34 +01:00
Grot (@grafanabot)
426fab32eb [v9.3.x] SSE: Make sure to forward headers, user and cookies/OAuth token (#59390)
SSE: Make sure to forward headers, user and cookies/OAuth token (#58897)

Fixes #58793 and Fixes https://github.com/grafana/azure-data-explorer-datasource/issues/513

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
(cherry picked from commit 5623b5afaf)

Co-authored-by: Kyle Brandt <kyle@grafana.com>
2022-11-28 08:11:52 -05:00
Gilles De Mey
0e8f0d4b4a Alerting: fix "no permissions" alert flashing (#59300) (#59374) 2022-11-28 12:31:38 +01:00
Grot (@grafanabot)
7ab181383b [v9.3.x] OptionsUI: SliderValueEditor does not get auto focused on slider change (#59368)
OptionsUI: SliderValueEditor does not get auto focused on slider change (#59209)

(cherry picked from commit 45d3125919)

Co-authored-by: Laura Fernández <laura.fernandez@grafana.com>
2022-11-28 05:20:18 -05:00
Grot (@grafanabot)
e0cf9e4331 [v9.3.x] Breadcrumbs: Remove semi-bold and change current/last breadcrumb text color (#58913)
Breadcrumbs: Remove semi-bold and change current/last breadcrumb text color (#58875)

(cherry picked from commit c14cbfc65d)

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
2022-11-28 04:40:54 -05:00
Timur Olzhabayev
9b5269dc4f Feat: Adding an optional browser config option to e2e test runner (#59321)
Adding an optional browser config option to e2e test runner

(cherry picked from commit 9e2d37462b)
2022-11-28 08:12:22 +01:00
Grot (@grafanabot)
2e36890b56 [v9.3.x] Access Control: Clear user's permission cache after resource creation (#59307)
Access Control: Clear user's permission cache after resource creation (#59101)

* refresh user's permission cache after resource creation

* clear the cache instead of reloading the permissions

* don't error if can't clear cache

* fix tests

* fix tests again

(cherry picked from commit a8bae3f0b0)

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
2022-11-24 09:52:46 -05:00
Grot (@grafanabot)
d147f3d366 [v9.3.x] Navigation: Support rbac for new dashboard, import dashboard and new folder (#59305)
Navigation: Support rbac for new dashboard, import dashboard and new folder (#59303)

RBAC: Support rbac for new dashboard, import dashboard and new folder
page

(cherry picked from commit a53f57cc43)

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
2022-11-24 09:46:36 -05:00
Grot (@grafanabot)
c0ddd089ef [v9.3.x] TraceView: Fix broken rendering when scrolling in Dashboard panel in Firefox (#59281)
TraceView: Fix broken rendering when scrolling in Dashboard panel in Firefox (#56642)

(cherry picked from commit a2f1d2e102)

Co-authored-by: zdg-github <52441803+zdg-github@users.noreply.github.com>
2022-11-24 14:31:56 +01:00
Grot (@grafanabot)
556bec41c9 [v9.3.x] Logs: Fix misalignment of LogRows (#59290)
Logs: Fix misalignment of LogRows (#59279)

* add default display flex

* changed to `text-align: left`

(cherry picked from commit 400ada1ad0)

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>
2022-11-24 07:33:41 -05:00
Grot (@grafanabot)
b95b9f0c47 [v9.3.x] PostgreSQL: Fix missing CA field from configuration (#59286)
PostgreSQL: Fix missing CA field from configuration (#59280)

* PostgreSQL: Fix missing CA field from configuration

(cherry picked from commit be73418d00)

Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com>
2022-11-24 13:33:24 +01:00
Grot (@grafanabot)
4e2446d3e3 [v9.3.x] Azure Monitor: Fix empty/errored responses for Logs variables (#59278)
Azure Monitor: Fix empty/errored responses for Logs variables (#59240)

(cherry picked from commit 276b54fe9d)

Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com>
2022-11-24 06:02:51 -05:00
Grot (@grafanabot)
2cbaab0e4c [v9.3.x] User: Optimize signed in user cache management (#59199)
User: Optimize signed in user cache management (#59090)

* only access the cache if a user ID is set

* ignore all negative values

(cherry picked from commit d7a652ff7f)

Co-authored-by: Jo <joao.guerreiro@grafana.com>
2022-11-24 04:15:43 -05:00
Grot (@grafanabot)
85383e9b43 [v9.3.x] Heatmap: Fix blurry text & rendering (#59262)
Heatmap: Fix blurry text & rendering (#59260)

(cherry picked from commit 6f00bc5674)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2022-11-23 23:15:19 -05:00
Grot (@grafanabot)
d895cad92a [v9.3.x] Explore: Sub-tables support for Table component (#59250)
Explore: Sub-tables support for Table component (#58682)

* First commit with working version of sub-tables using subData array

* Update TableContainer and query result to support a dataframe array for the table result

* Fix border issue by moving the subtable to above the cells in the DOM

* Allow header to be configurable using custom options.

* Update TablePanel to support sub-tables

* Fix main row links

* Added tests

* Fix TablePanel correctly splitting frames and sub-frames by using refId

(cherry picked from commit 183b279274)

Co-authored-by: Andre Pereira <adrapereira@gmail.com>
2022-11-23 18:30:45 +00:00
Grot (@grafanabot)
000cfe1dfc [v9.3.x] Internationalization: Enable internationalization by default (#59226)
Internationalization: Enable internationalization by default (#59204)

* Enable internationalization feature flag by default

* Change i18n feature to beta

* Set i18n feature flag to stable

* update features

(cherry picked from commit ba0ac08465)

Co-authored-by: Josh Hunt <joshhunt@users.noreply.github.com>
2022-11-23 12:17:09 -05:00
Grot (@grafanabot)
c4756c394b [v9.3.x] Azure Monitor Logs: Avoid warning when the response is empty (#59231)
Azure Monitor Logs: Avoid warning when the response is empty (#59211)

(cherry picked from commit 9d88e14f01)

Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com>
2022-11-23 10:31:54 -05:00
Grot (@grafanabot)
f5d7e0ed92 [v9.3.x] Tempo: Send the correct start time when making a TraceQL query (#59219)
(cherry picked from commit 6d94fa6aa5)
Co-authored-by: Hamas Shafiq <hamas.shafiq@grafana.com>
2022-11-23 14:28:59 +00:00
Grot (@grafanabot)
e20a296176 [v9.3.x] I18n: Crowdin sync (#59212)
I18n: Crowdin sync (#59210)

* New translations grafana.json (French)

* New translations grafana.json (Spanish)

* New translations grafana.json (German)

* New translations grafana.json (Chinese Simplified)

(cherry picked from commit 343f65fffb)

Co-authored-by: Grafana I18n Bot <110095610+grafana-i18n-bot@users.noreply.github.com>
2022-11-23 14:17:34 +00:00
Grot (@grafanabot)
14878bcf99 [v9.3.x] I18n: Translate Dashboard Starred notification (#59208)
I18n: Translate Dashboard Starred notification (#59138)

(cherry picked from commit f180bb46f8)

Co-authored-by: Josh Hunt <joshhunt@users.noreply.github.com>
2022-11-23 07:28:04 -05:00
Grot (@grafanabot)
f1efd350e6 [v9.3.x] Navigation: always show GetStartedWithPlugin even if there's install control wa… (#59206)
Navigation: always show `GetStartedWithPlugin` even if there's install control wa… (#59127)

always show `GetStartedWithPlugin` even if there's install control warnings

(cherry picked from commit c2f31c2685)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-23 11:49:45 +00:00
Grot (@grafanabot)
66ceacd98f [v9.3.x] Solo Panel: Configurable timezone (#59202)
Solo Panel: Configurable timezone (#59153)

* Allow to set timezone in query

* Use optional timestamp

* Update timeZone

(cherry picked from commit a0334a92f5)

Co-authored-by: Selene <selenepinillos@gmail.com>
2022-11-23 12:42:21 +01:00
Grot (@grafanabot)
7a543052ee [v9.3.x] Fix #58598 X-ID-Token header missing on Loki Datasource (#59193)
Fix #58598 X-ID-Token header missing on Loki Datasource (#58784)

* Fix #58598 X-ID-Token header missing on Loki Datasource

* Remove unecessary continue statements

* Add getAuthHeadersForCallResource unit tests

* Fix test and switch statement issues introduced during merge

(cherry picked from commit f1ef63791a)

Co-authored-by: Yann Vigara <yvigara@users.noreply.github.com>
2022-11-23 12:24:17 +01:00
Grot (@grafanabot)
7e02e2aef3 [v9.3.x] I18n: Fix variables not interpolating with pseudo localisation (#59194)
I18n: Fix variables not interpolating with pseudo localisation (#59145)

* I18n: Don't pseudolocalise string variables

* clean up

(cherry picked from commit c02f2321c1)

Co-authored-by: Josh Hunt <joshhunt@users.noreply.github.com>
2022-11-23 05:42:46 -05:00
Ashley Harrison
a4919a6d69 Navigation: share logic between buildBreadcrumbs and usePageTitle… (#59155)
Navigation: share logic between `buildBreadcrumbs` and `usePageTitle` (#58819)

* simplify usePageTitle logic a bit

* use buildBreadcrumbs logic in usePageTitle

* always add home item to navTree, fix some tests

* fix remaining unit tests

(cherry picked from commit 824a562b03)
2022-11-23 10:26:55 +00:00
Grot (@grafanabot)
17d98d79d9 [v9.3.x] Azure Monitor: Fix namespace selection for storageaccounts (#59184)
Azure Monitor: Fix namespace selection for storageaccounts (#56449)

(cherry picked from commit 3bea8f2462)

Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com>
2022-11-23 11:25:48 +01:00
Grot (@grafanabot)
82e6fdfaf9 [v9.3.x] Navigation: use correct id to find cloud access policies page (#59187)
Navigation: use correct id to find cloud access policies page (#59123)

use correct id to find cloud access policies page

(cherry picked from commit 8761a71da2)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-23 05:15:43 -05:00
Grot (@grafanabot)
ff49b0de7e [v9.3.x] Internationalization: Translate ShareSnapshot label (#59144)
Internationalization: Translate ShareSnapshot label (#58802)

(cherry picked from commit 515440979b)

Co-authored-by: Laura Fernández <laura.fernandez@grafana.com>
2022-11-23 05:07:37 -05:00
Grot (@grafanabot)
068a41c6a7 [v9.3.x] SQL: Fix code editor for SQL datasources (#59185)
SQL: Fix code editor for SQL datasources (#58116)

* SQL: Fix code editor for sql datasources

* Fix: mysql completion with defaultdb

(cherry picked from commit 75097b99fb)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2022-11-23 11:06:48 +01:00
Grot (@grafanabot)
812c85602b [v9.3.x] Internationalization: Translate menu items (#59178)
Internationalization: Translate menu items (#59088)

(cherry picked from commit 4a628f18b0)

Co-authored-by: Laura Fernández <laura.fernandez@grafana.com>
2022-11-23 03:53:55 -05:00
Grot (@grafanabot)
2fff4175b4 [v9.3.x] StateTimeline: Prevent label text from overflowing state rects (#59175)
StateTimeline: Prevent label text from overflowing state rects (#59169)

Co-authored-by: Vegard Vatn <vegard.vatn@avento.no>
(cherry picked from commit 9512f1e1af)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2022-11-22 23:14:09 -05:00
Grot (@grafanabot)
6dc4223e0b [v9.3.x] Heatmap: Fix tooltip y range of top and bottom buckets in calculated heatmaps (#59174)
Heatmap: Fix tooltip y range of top and bottom buckets in calculated heatmaps (#59172)

Co-authored-by: xdavidwu <xdavidwuph@gmail.com>
(cherry picked from commit 2a8706b025)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2022-11-22 23:06:50 -05:00
Grot (@grafanabot)
946daeb01a [v9.3.x] Canvas: Add icon value mapping (#59171)
Canvas: Add icon value mapping (#59013)

(cherry picked from commit e157ef1171)

Co-authored-by: Nathan Marrs <nathanielmarrs@gmail.com>
2022-11-22 21:13:44 -05:00
Grot (@grafanabot)
280e796635 [v9.3.x] Geomap: Improve location editor (#59170)
Geomap: Improve location editor (#58017)

* add custom component for location editor

* FC cleanup

* Apply filter to add location fields call

* Create custom editor for location mode

* Apply validation logic and render warning

* Improve alert styling

* Add help url button to location alert

* Add success alert for auto

* Remove completed TODOs

* Only use alert on error, not success

* Change location mode to dropdown

* Change alert severity to less severe, info

* Prevent auto field selection during manual

* Update location testing to be for auto mode

* Run geo transformer editor init once

* Fix breaking test

* Clean up some anys

* Update styling for alert

* Remove auto success styling

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
(cherry picked from commit ee8f292c6a)

Co-authored-by: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com>
2022-11-22 21:06:01 -05:00
Grot (@grafanabot)
dc23aa9a0f [v9.3.x] Internationalization: Translate NavBar - 'Search dashboard' menu item (#59152)
Internationalization: Translate NavBar - 'Search dashboard' menu item (#58815)

(cherry picked from commit 3f63ca06c3)

Co-authored-by: Laura Fernández <laura.fernandez@grafana.com>
2022-11-22 11:52:49 -05:00
Grot (@grafanabot)
eb9f63c715 [v9.3.x] Internationalization: Translate ViewJSONmodal and ClipboardButton (#59151)
Internationalization: Translate ViewJSONmodal and ClipboardButton (#58807)

(cherry picked from commit 8f567d57fa)

Co-authored-by: Laura Fernández <laura.fernandez@grafana.com>
2022-11-22 11:49:26 -05:00
Joao Silva
4c3adeff7c [v9.3.x] Preferences: Add confirmation modal when saving org preferences (#59119) (#59141) 2022-11-22 17:45:52 +01:00
Grot (@grafanabot)
a2263b9249 [v9.3.x] Alerting: Enable interpolation for notification policies in file provisioning (#59140)
Alerting: Enable interpolation for notification policies in file provisioning (#58956)

(cherry picked from commit 41b3398eb4)

Co-authored-by: Jean-Philippe Quéméner <JohnnyQQQQ@users.noreply.github.com>
2022-11-22 17:27:08 +01:00
Grot (@grafanabot)
692bd0ac00 [v9.3.x] Internationalization: Translate 'Hide / show legend' of PanelHeaderMenuItem (#59133)
Internationalization: Translate 'Hide / show legend' of PanelHeaderMenuItem (#58800)

(cherry picked from commit f2cb248d93)

Co-authored-by: Laura Fernández <laura.fernandez@grafana.com>
2022-11-22 17:04:59 +01:00
Grot (@grafanabot)
df10c952c5 [v9.3.x] Azure Monitor: Fix resource picker selection for subresources (#59137)
Azure Monitor: Fix resource picker selection for subresources (#56392)

(cherry picked from commit 5b1ff83ee9)

Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com>
2022-11-22 17:02:24 +01:00
Grot (@grafanabot)
0491f55ad6 [v9.3.x] Internationalization: Translate VariableInput and VariableOptions components (#59131)
Internationalization: Translate VariableInput and VariableOptions components (#58748)

(cherry picked from commit 3cedcdedbd)

Co-authored-by: Laura Fernández <laura.fernandez@grafana.com>
2022-11-22 10:25:52 -05:00
Grot (@grafanabot)
f3ffc1a495 [v9.3.x] CloudWatch: fix custom namespace for listing dimension keys, refactor to non-pointer types, add test assertions, rename packages (#59130)
CloudWatch: fix custom namespace for listing dimension keys, refactor to non-pointer types, add test assertions, rename packages (#59106)

Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
(cherry picked from commit c43e1a721f)

Co-authored-by: Shirley <4163034+fridgepoet@users.noreply.github.com>
2022-11-22 16:25:27 +01:00
Grot (@grafanabot)
3a1ffd88d5 [v9.3.x] AzureMonitor: Separate subscription health check API version (#59122)
AzureMonitor: Separate subscription health check API version (#58253)

Separate subscription health check API version

(cherry picked from commit f8656d269d)

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
2022-11-22 14:52:14 +00:00
Sonia Aguilar
eda3fb190c Fix: Add checkForPathSeparator validation in group name for grafana-m… (#59100)
* Fix: Add checkForPathSeparator validation in group name for grafana-managed alerts in form

* Build: Disable flaky RuleEditor frontend test

* trigger CI
2022-11-22 15:41:03 +01:00
Grot (@grafanabot)
129b74fe08 [v9.3.x] Chore: Document theme toggle keybinding (#59120)
Chore: Document theme toggle keybinding (#59031)

* make theme toggle keybinding dev only

* fix bug + add support for theme change keybinding

(cherry picked from commit 6f26668a9f)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-22 14:21:31 +00:00
Grot (@grafanabot)
8ae02b4b7b [v9.3.x] DataSourceWithBackend - Set postResource method to POST (#59117)
DataSourceWithBackend - Set postResource method to POST (#59114)

Set postResource method to POST

(cherry picked from commit 4eed56193f)

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
2022-11-22 09:15:04 -05:00
Grot (@grafanabot)
0bb76df454 [v9.3.x] Accessibility: Improve keyboard accessibility in Collapse (#59097)
Accessibility: Improve keyboard accessibility in `Collapse` (#59022)

fix keyboard accessibility in Collapse

(cherry picked from commit 19e97a1f31)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-22 08:52:16 -05:00
Grot (@grafanabot)
7e509a19f1 [v9.3.x] Loki: Add gzip compression to resource calls (#59113)
Loki: Add `gzip` compression to resource calls (#59059)

* Loki: Add compression to `callResource`

* add missing tests

* fix formatting

(cherry picked from commit 08e87a217c)

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>
2022-11-22 08:16:45 -05:00
Grot (@grafanabot)
3de3eee655 [v9.3.x] MS/My/PostgresSQL: Migrate annotation query (#59112)
MS/My/PostgresSQL: Migrate annotation query (#58847)

(cherry picked from commit 38f25a0bf5)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2022-11-22 08:08:11 -05:00
Grot (@grafanabot)
1bcdaeb910 [v9.3.x] @grafana/runtime: Avoid calling applyTemplateVariables for the wrong datasource (#59029)
@grafana/runtime: Avoid calling applyTemplateVariables for the wrong datasource (#57921)

(cherry picked from commit 448358ac66)

Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com>
2022-11-22 06:50:47 -05:00
Grot (@grafanabot)
90a904fbd5 [v9.3.x] Alerting: Support Prometheus durations in Provisioning API (#59067)
Alerting: Support Prometheus durations in Provisioning API (#58293)

Provisioning API should support Prometheus durations

(cherry picked from commit 57d6adbc7c)

Co-authored-by: Bart Peeters <birtpeeters@hotmail.com>
2022-11-21 19:15:54 +00:00
Grot (@grafanabot)
7899b5ae72 [v9.3.x] Prometheus: fix Heatmap y buckets when legendFormat: auto (#59065)
Prometheus: fix Heatmap y buckets when legendFormat: auto (#59053)

(cherry picked from commit 1f4834a144)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2022-11-21 12:39:54 -05:00
Grot (@grafanabot)
3c353ab1c1 [v9.3.x] Chore: Fix SQLx failure when starting the server with newDBLibrary enabled (#59054)
Chore: Fix SQLx failure when starting the server with newDBLibrary enabled (#58985)

change the weekstart to pointer

Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
(cherry picked from commit ba8124ef93)

Co-authored-by: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com>
2022-11-21 17:55:36 +02:00
Grot (@grafanabot)
e1bf7aa65e [v9.3.x] Alerting: Add Troubleshooting to Images in notifications docs (#59026)
Alerting: Add Troubleshooting to Images in notifications docs (#58955)

(cherry picked from commit 2f878acd9d)

Co-authored-by: George Robinson <george.robinson@grafana.com>
2022-11-21 11:36:32 +00:00
Grot (@grafanabot)
6a6e05bfa1 [v9.3.x] Accessibility: Improve keyboard accessibility of FilterPill (#59017)
Accessibility: Improve keyboard accessibility of `FilterPill` (#58976)

fix keyboard a11y in FilterPill

(cherry picked from commit 8aa74fe9ee)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-21 06:15:49 -05:00
Grot (@grafanabot)
28b0af4476 [v9.3.x] Accessibility: Improve keyboard accessibility in AnnoListPanel (#59015)
Accessibility: Improve keyboard accessibility in `AnnoListPanel` (#58971)

fix keyboard accessibility in AnnoListPanel

(cherry picked from commit 598935cb34)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-21 04:37:38 -05:00
Grot (@grafanabot)
72ac08cc7b [v9.3.x] Navigation: Set navtree to an empty array instead of null (#59010)
Navigation: Set navtree to  an empty array instead of null (#58919)

set navtree to  an empty array instead of null

(cherry picked from commit 4aa5dea96b)

Co-authored-by: Leo <108552997+lpskdl@users.noreply.github.com>
2022-11-21 10:02:19 +01:00
Grot (@grafanabot)
90946b68a2 [v9.3.x] Navigation: Prevent viewer role accessing dashboard creation, import and folder creation (#59009)
Navigation: Prevent viewer role accessing dashboard creation, import and folder creation (#58842)

hide pages related dashboard import create and folder creation for viewer role

(cherry picked from commit 14fbd44ac0)

Co-authored-by: Leo <108552997+lpskdl@users.noreply.github.com>
2022-11-21 10:02:08 +01:00
Grot (@grafanabot)
9521c90651 [v9.3.x] Docs/removes admin screenshots (#58996)
Docs/removes admin screenshots (#58994)

* removes screenshots in the admin docs

* removes unneeded screenshot

(cherry picked from commit 8ac197b11c)

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
2022-11-18 16:04:46 -06:00
Grot (@grafanabot)
d6ca111109 [v9.3.x] Docs: adds permission validation setting (#58988)
Docs: adds permission validation setting (#58970)

* adds permission validation setting

* Update docs/sources/administration/roles-and-permissions/access-control/configure-rbac/index.md

Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>

* makes prettier

Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>
(cherry picked from commit 780efa5791)

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
2022-11-18 12:43:32 -06:00
Grot (@grafanabot)
200915fc91 [v9.3.x] Update verify-release script (#58984)
Update verify-release script (#58812)

Update verify-release script with required arg

(cherry picked from commit 19ff3645e8)

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
2022-11-18 17:27:08 +00:00
Grot (@grafanabot)
3bd24136a0 [v9.3.x] Docs: adjusts link to kms integration (#58983)
Docs: adjusts link to kms integration (#58981)

adjusts link to kms integration

(cherry picked from commit e7b5bd34bd)

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
2022-11-18 10:52:06 -06:00
Grot (@grafanabot)
47a04102ee [v9.3.x] Alerting: Fix metric in Images in Notifications docs (#58979)
Alerting: Fix metric in Images in Notifications docs (#58954)

(cherry picked from commit 4da97aef7d)

Co-authored-by: George Robinson <george.robinson@grafana.com>
2022-11-18 16:46:32 +00:00
Grot (@grafanabot)
f35ab2f79d [v9.3.x] Nav: Split Admin into three sections for new IA (#58974)
Nav: Split Admin into three sections for new IA (#58229)

* start to split admin into two sections

* most of new admin nav implemented

* landing pages

* hide admin for non-admins

* update admin redirects if not topnav

* clean up

* updated IA for admin (still WIP)

* move plugin pages into correct admin sections

* fix backend unit test

* move correlations into the correct section

* add translations for admin sections

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
(cherry picked from commit 5978dc138e)

Co-authored-by: Josh Hunt <joshhunt@users.noreply.github.com>
2022-11-18 11:15:17 -05:00
Grot (@grafanabot)
e07f115f98 [v9.3.x] Accessibility: Improve keyboard accessibility in GettingStarted panel (#58977)
Accessibility: Improve keyboard accessibility in `GettingStarted` panel (#58966)

fix keyboard a11y in `GettingStarted` panel

(cherry picked from commit e19957bbc8)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-18 10:54:21 -05:00
Grot (@grafanabot)
001179771e [v9.3.x] Navigation: Stop clearing search state when opening a result in a new tab (#58967)
Navigation: Stop clearing search state when opening a result in a new tab (#58880)

search: only clear search state if not opening in a new tab
(cherry picked from commit 5226a61f67)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-18 09:57:24 -05:00
Grot (@grafanabot)
661c72d6b7 [v9.3.x] Explore: Fix a11y issue with logs navigation buttons (#58963)
Explore: Fix a11y issue with logs navigation buttons (#58944)

(cherry picked from commit 27b5e5f781)

Co-authored-by: Giordano Ricci <me@giordanoricci.com>
2022-11-18 15:45:30 +01:00
Grot (@grafanabot)
4e31338e94 [v9.3.x] Navigation: move connections + integrations to be a top level item (#58939)
Navigation: move connections + integrations to be a top level item (#58902)

* move connections + integrations to be a top level item

* add a test to check we can move apps to the root

* split out movePlugin logic into a separate function

* fix linting

* rename movePlugin -> addPluginToSection

(cherry picked from commit d46e3916a1)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-18 08:08:36 -05:00
Grot (@grafanabot)
d5d4685d20 [v9.3.x] GaugePanel: Setting the neutral-point of a gauge (#58949) 2022-11-18 12:52:29 +01:00
Grot (@grafanabot)
d7e459fdee [v9.3.x] QueryData: skip header validation (revert check) (#58946)
QueryData: skip header validation (revert check) (#58871)

(cherry picked from commit 8e19a1618f)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2022-11-18 05:59:50 -05:00
Grot (@grafanabot)
4db1f3b850 [v9.3.x] OAuth: Refactor OAuth parameters handling to support obtaining refresh tokens for Google OAuth (#58940)
OAuth: Refactor OAuth parameters handling to support obtaining refresh tokens for Google OAuth (#58782)

* Add ApprovalForce to AuthCodeOptions

* Extract access token validity check to a function

* Refactor

* Oauth: set options internally instead of exposing new function

* Align tests

* Remove unused function

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
(cherry picked from commit 9c98314e9f)

Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>
2022-11-18 10:37:24 +01:00
Grot (@grafanabot)
4fb033d525 [v9.3.x] Quota: Fix failure in store due to missing scope parameters (#58923)
Quota: Fix failure in store due to missing scope parameters (#58874)

Quota: Fix failure in store
(cherry picked from commit 18738cfd77)

Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
2022-11-17 18:28:11 +01:00
Grot (@grafanabot)
ffa649e377 [v9.3.x] Quota: Fix failure when checking session limits (#58869)
Quota: Fix failure when checking session limits (#58865)

(cherry picked from commit ab36252c86)

Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
2022-11-17 18:09:29 +01:00
Grot (@grafanabot)
450ff445ea [v9.3.x] Navigation: rename Grafana Machine Learning to just Machine Learning (#58911)
Navigation: rename Grafana Machine Learning to just Machine Learning (#58893)

rename Grafana Machine Learning to just Machine Learning

(cherry picked from commit ac66e14054)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-17 09:49:07 -05:00
Grot (@grafanabot)
311b4b9b6f [v9.3.x] Chore: Extract server lock error so it can be used with errors.As (#58906)
Chore: Extract server lock error so it can be used with errors.As (#58899)

chore: extract server lock Error so it can be used with error.As
(cherry picked from commit 7e9d94cfda)

Co-authored-by: Jo <joao.guerreiro@grafana.com>
2022-11-17 15:30:24 +01:00
Grot (@grafanabot)
710124b5de [v9.3.x] Navigation: Change quick add styling (#58879)
Navigation: Change quick add styling (#58854)

Change quick add styling + allow for returning isOpen state to dropdown children

(cherry picked from commit b398e8640d)

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2022-11-17 04:18:54 -05:00
Grot (@grafanabot)
61eb256882 [v9.3.x] SSE: Keep value name from numeric table (#58851)
SSE: Keep value name from numeric table (#58831)

fixes #48868

(cherry picked from commit 1953d473c0)

Co-authored-by: Kyle Brandt <kyle@grafana.com>
2022-11-16 10:54:41 -05:00
Grot (@grafanabot)
7ec8550652 [v9.3.x] QueryData: fix header parsing to support expressions (#58848)
QueryData: fix header parsing to support expressions (#58826)

fixes #58821

(cherry picked from commit 934fb2f0ee)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2022-11-16 10:29:59 -05:00
Grot (@grafanabot)
4890db3089 [v9.3.x] AppRootPage: Render app plugins without pages (#58814)
AppRootPage: Render app plugins without pages (#58776)

fix: render app plugins that don't have a page in includes
(cherry picked from commit 4ee83a5f2b)

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
2022-11-16 06:05:19 -05:00
Grot (@grafanabot)
81b0dd7686 [v9.3.x] Fix: Bump-version action regex pattern to work with beta1 (#58806)
Fix: Bump-version action regex pattern to work with beta1 (#58805)

Fixing bump version regex

(cherry picked from commit 174a039ee1)

Co-authored-by: Timur Olzhabayev <timur.olzhabayev@grafana.com>
2022-11-16 04:36:53 -05:00
Grot (@grafanabot)
cb9df3bfdb Release: Bump version to 9.3.0-beta.1 (#58787)
"Release: Updated versions in package to 9.3.0-beta.1"
2022-11-15 22:30:16 +02:00
Grot (@grafanabot)
a0eb08f01b [v9.3.x] Changelog: Updated changelog for 9.3.0-beta1 (#58786)
Changelog: Updated changelog for 9.3.0-beta1 (#58785)

(cherry picked from commit d5318f02c6)
2022-11-15 17:04:45 -03:00
358 changed files with 7598 additions and 2307 deletions

View File

@@ -1538,7 +1538,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"packages/grafana-ui/src/components/Table/utils.test.ts:5381": [
"packages/grafana-ui/src/components/Table/utils.test.tsx: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"],
@@ -1548,19 +1548,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"]
[0, 0, 0, "Unexpected any. Specify a different type.", "9"]
],
"packages/grafana-ui/src/components/Table/utils.ts:5381": [
"packages/grafana-ui/src/components/Table/utils.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
@@ -3997,12 +3987,6 @@ exports[`better eslint`] = {
"public/app/features/folders/state/actions.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/geo/editor/GazetteerPathEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/features/geo/format/geohash.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@@ -5395,8 +5379,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/cloudwatch/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]

View File

@@ -87,7 +87,11 @@
"ignoreNonDOM": true
}
],
"jsx-a11y/no-static-element-interactions": "off"
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/label-has-associated-control": [ "error", {
"controlComponents": ["NumberInput"],
"depth": 2
}]
}
}
]

View File

@@ -17,7 +17,7 @@ jobs:
id: regex-match
with:
text: ${{ github.event.inputs.version }}
regex: '^(\d+.\d+).\d+(?:-beta.\d+)?$'
regex: '^(\d+.\d+).\d+(?:-beta\d+)?$'
- uses: actions-ecosystem/action-regex-match@v2.0.2
if: ${{ inputs.version_call != '' }}
id: regex-match-version-call
@@ -29,7 +29,7 @@ jobs:
run: |
echo "The input version format is not correct, please respect:\
major.minor.patch or major.minor.patch-beta.number format. \
example: 7.4.3 or 7.4.3-beta.1"
example: 7.4.3 or 7.4.3-beta1"
exit 1
- name: Validate input version call
if: ${{ inputs.version_call != '' && steps.regex-match-version-call.outputs.match == '' }}

View File

@@ -1,3 +1,173 @@
<!-- 9.3.0-beta1 START -->
# 9.3.0-beta1 (2022-11-15)
### Features and enhancements
- **Alerting:** Add Alertmanager choice warning. [#55311](https://github.com/grafana/grafana/pull/55311), [@konrad147](https://github.com/konrad147)
- **Alerting:** Add support for linking external images securely - Azure Blob (#1). [#56598](https://github.com/grafana/grafana/pull/56598), [@petr-stupka](https://github.com/petr-stupka)
- **Alerting:** Add threshold expression. [#55102](https://github.com/grafana/grafana/pull/55102), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Add traceability headers for alert queries. [#57127](https://github.com/grafana/grafana/pull/57127), [@alexweav](https://github.com/alexweav)
- **Alerting:** Allow none provenance alert rule creation from provisioning API. [#58410](https://github.com/grafana/grafana/pull/58410), [@alexmobo](https://github.com/alexmobo)
- **Alerting:** Cache result of dashboard ID lookups. [#56587](https://github.com/grafana/grafana/pull/56587), [@alexweav](https://github.com/alexweav)
- **Alerting:** Expressions pipeline redesign. [#54601](https://github.com/grafana/grafana/pull/54601), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Fall back to "range" query type for unified alerting when "both" is specified. [#57288](https://github.com/grafana/grafana/pull/57288), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Implement the Webex notifier. [#58480](https://github.com/grafana/grafana/pull/58480), [@gotjosh](https://github.com/gotjosh)
- **Alerting:** Improve group modal with validation on evaluation interval. [#57830](https://github.com/grafana/grafana/pull/57830), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Persist annotations from multidimensional rules in batches. [#56575](https://github.com/grafana/grafana/pull/56575), [@alexweav](https://github.com/alexweav)
- **Alerting:** Query time logging. [#57585](https://github.com/grafana/grafana/pull/57585), [@konrad147](https://github.com/konrad147)
- **Alerting:** Remove the alert manager selection from the data source configuration. [#57369](https://github.com/grafana/grafana/pull/57369), [@VikaCep](https://github.com/VikaCep)
- **Alerting:** Remove the alert manager selection from the data source configuration. [#56460](https://github.com/grafana/grafana/pull/56460), [@gitstart](https://github.com/gitstart)
- **Alerting:** Support values in notification templates. [#56457](https://github.com/grafana/grafana/pull/56457), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **Alerting:** Templated URLs for webhook type contact points. [#57296](https://github.com/grafana/grafana/pull/57296), [@santihernandezc](https://github.com/santihernandezc)
- **Annotations:** Disable "Add annotation" button when annotations are disabled. [#57481](https://github.com/grafana/grafana/pull/57481), [@ryantxu](https://github.com/ryantxu)
- **Auth:** Add validation and ingestion of conflict file. [#53014](https://github.com/grafana/grafana/pull/53014), [@eleijonmarck](https://github.com/eleijonmarck)
- **Auth:** Make built-in login configurable. [#46978](https://github.com/grafana/grafana/pull/46978), [@TsotosA](https://github.com/TsotosA)
- **Auth:** Refresh OAuth access_token automatically using the refresh_token. [#56076](https://github.com/grafana/grafana/pull/56076), [@mgyongyosi](https://github.com/mgyongyosi)
- **Auth:** Validate Azure ID token version on login is not v1. [#58088](https://github.com/grafana/grafana/pull/58088), [@Jguer](https://github.com/Jguer)
- **BackendSrv:** Make it possible to pass `options` to `.get|post|patch...` methods. [#51316](https://github.com/grafana/grafana/pull/51316), [@leventebalogh](https://github.com/leventebalogh)
- **Canvas:** Add tabs to inline editor. [#57778](https://github.com/grafana/grafana/pull/57778), [@adela-almasan](https://github.com/adela-almasan)
- **Canvas:** Extend root context menu. [#58097](https://github.com/grafana/grafana/pull/58097), [@adela-almasan](https://github.com/adela-almasan)
- **Chore:** Switch Grafana to using faro libraries. [#58186](https://github.com/grafana/grafana/pull/58186), [@tolzhabayev](https://github.com/tolzhabayev)
- **Chore:** Use strings.ReplaceAll and preallocate containers. [#58483](https://github.com/grafana/grafana/pull/58483), [@sashamelentyev](https://github.com/sashamelentyev)
- **CloudWatch:** Cache resource request responses in the browser. [#57082](https://github.com/grafana/grafana/pull/57082), [@sunker](https://github.com/sunker)
- **Config:** Change jwt config value to be "expect_claims". [#58284](https://github.com/grafana/grafana/pull/58284), [@conorevans](https://github.com/conorevans)
- **Configuration:** Update ssl_mode documentation in sample.ini to match default.ini. [#55138](https://github.com/grafana/grafana/pull/55138), [@alecxvs](https://github.com/alecxvs)
- **Correlations:** Add query editor and target field to settings page. [#55567](https://github.com/grafana/grafana/pull/55567), [@Elfo404](https://github.com/Elfo404)
- **Dashboard:** Record the number of cached queries for usage insights. [#56050](https://github.com/grafana/grafana/pull/56050), [@juanicabanas](https://github.com/juanicabanas)
- **Dashboard:** Record the number of cached queries for usage insights. (Enterprise)
- **Datasources:** Support mixed datasources in a single query. [#56832](https://github.com/grafana/grafana/pull/56832), [@mmandrus](https://github.com/mmandrus)
- **Docs:** Add documentation for Custom Branding on Public Dashboards. [#58090](https://github.com/grafana/grafana/pull/58090), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **Docs:** Add missing documentation for enterprise features. [#56753](https://github.com/grafana/grafana/pull/56753), [@mmandrus](https://github.com/mmandrus)
- **Docs:** Clarify that audit logs are generated only for API requests. [#57521](https://github.com/grafana/grafana/pull/57521), [@spinillos](https://github.com/spinillos)
- **Echo:** Add config option to prevent duplicate page views for GA4. [#57619](https://github.com/grafana/grafana/pull/57619), [@tolzhabayev](https://github.com/tolzhabayev)
- **Elasticsearch:** Add trace to logs functionality. [#58063](https://github.com/grafana/grafana/pull/58063), [@ivanahuckova](https://github.com/ivanahuckova)
- **Elasticsearch:** Reuse http client in the backend. [#55172](https://github.com/grafana/grafana/pull/55172), [@gabor](https://github.com/gabor)
- **Explore:** Add tracesToMetrics span time shift options (#54710). [#55335](https://github.com/grafana/grafana/pull/55335), [@hanjm](https://github.com/hanjm)
- **Explore:** Logs volume histogram: always start Y axis from zero. [#56200](https://github.com/grafana/grafana/pull/56200), [@gabor](https://github.com/gabor)
- **Explore:** Remove explore2Dashboard feature toggle. [#58329](https://github.com/grafana/grafana/pull/58329), [@Elfo404](https://github.com/Elfo404)
- **Explore:** Support fields interpolation in logs panel. [#58426](https://github.com/grafana/grafana/pull/58426), [@ifrost](https://github.com/ifrost)
- **Frontend Routing:** Always render standalone plugin pages using the `<AppRootPage>`. [#57771](https://github.com/grafana/grafana/pull/57771), [@leventebalogh](https://github.com/leventebalogh)
- **GRPC Server:** Add gRPC server service. [#47849](https://github.com/grafana/grafana/pull/47849), [@FZambia](https://github.com/FZambia)
- **Geomap:** Add photo layer. [#57307](https://github.com/grafana/grafana/pull/57307), [@drew08t](https://github.com/drew08t)
- **Geomap:** Upgrade to openlayers 7.x. [#57317](https://github.com/grafana/grafana/pull/57317), [@ryantxu](https://github.com/ryantxu)
- **GrafanaData:** Deprecate logs functions. [#56077](https://github.com/grafana/grafana/pull/56077), [@gabor](https://github.com/gabor)
- **GrafanaData:** Deprecate the LogsParser type. [#56242](https://github.com/grafana/grafana/pull/56242), [@gabor](https://github.com/gabor)
- **Kindsys:** Introduce Kind framework. [#56492](https://github.com/grafana/grafana/pull/56492), [@sdboyer](https://github.com/sdboyer)
- **LDAP:** Add `skip_org_role_sync` configuration option. [#56792](https://github.com/grafana/grafana/pull/56792), [@grafanabot](https://github.com/grafanabot)
- **LDAP:** Add `skip_org_role_sync` configuration option. [#56679](https://github.com/grafana/grafana/pull/56679), [@gamab](https://github.com/gamab)
- **LDAPSync:** Improve performance of sync and make it case insensitive. (Enterprise)
- **LibraryPanels:** Load library panels in the frontend rather than the backend. [#50560](https://github.com/grafana/grafana/pull/50560), [@ryantxu](https://github.com/ryantxu)
- **LogContext:** Add header and close button to modal. [#56283](https://github.com/grafana/grafana/pull/56283), [@svennergr](https://github.com/svennergr)
- **LogContext:** Improve text describing the loglines. [#55475](https://github.com/grafana/grafana/pull/55475), [@svennergr](https://github.com/svennergr)
- **Logs:** Allow collapsing the logs volume histogram. [#52808](https://github.com/grafana/grafana/pull/52808), [@gabor](https://github.com/gabor)
- **Logs:** Center `show context` modal on click. [#55989](https://github.com/grafana/grafana/pull/55989), [@svennergr](https://github.com/svennergr)
- **Logs:** Center `show context` modal on click. [#55405](https://github.com/grafana/grafana/pull/55405), [@svennergr](https://github.com/svennergr)
- **Logs:** Show LogRowMenu also for long logs and wrap-lines turned off. [#56030](https://github.com/grafana/grafana/pull/56030), [@svennergr](https://github.com/svennergr)
- **LogsContext:** Added button to load 10 more log lines. [#55923](https://github.com/grafana/grafana/pull/55923), [@svennergr](https://github.com/svennergr)
- **Loki:** Add case insensitive line contains operation. [#58177](https://github.com/grafana/grafana/pull/58177), [@gwdawson](https://github.com/gwdawson)
- **Loki:** Monaco Query Editor enabled by default. [#58080](https://github.com/grafana/grafana/pull/58080), [@matyax](https://github.com/matyax)
- **Loki:** Redesign and improve query patterns. [#55097](https://github.com/grafana/grafana/pull/55097), [@ivanahuckova](https://github.com/ivanahuckova)
- **Loki:** Rename log browser to label browser. [#58416](https://github.com/grafana/grafana/pull/58416), [@gwdawson](https://github.com/gwdawson)
- **Loki:** Show invalid fields in label filter. [#55751](https://github.com/grafana/grafana/pull/55751), [@ivanahuckova](https://github.com/ivanahuckova)
- **MSSQL:** Add connection timeout setting in configuration page. [#58631](https://github.com/grafana/grafana/pull/58631), [@mdvictor](https://github.com/mdvictor)
- **Navigation:** Add `pluginId` to standalone plugin page NavLinks. [#57769](https://github.com/grafana/grafana/pull/57769), [@leventebalogh](https://github.com/leventebalogh)
- **Navigation:** Expose new props to extend `Page`/`PluginPage`. [#58465](https://github.com/grafana/grafana/pull/58465), [@ashharrison90](https://github.com/ashharrison90)
- **Navtree:** Make it possible to configure standalone plugin pages. [#56393](https://github.com/grafana/grafana/pull/56393), [@leventebalogh](https://github.com/leventebalogh)
- **Node Graph:** Always show context menu. [#56876](https://github.com/grafana/grafana/pull/56876), [@joey-grafana](https://github.com/joey-grafana)
- **Number formatting:** Strip trailing zeros after decimal point when decimals=auto. [#57373](https://github.com/grafana/grafana/pull/57373), [@leeoniya](https://github.com/leeoniya)
- **OAuth:** Feature toggle for access token expiration check and docs. [#58179](https://github.com/grafana/grafana/pull/58179), [@mgyongyosi](https://github.com/mgyongyosi)
- **Opentsdb:** Allow template variables for filter keys. [#57226](https://github.com/grafana/grafana/pull/57226), [@bohandley](https://github.com/bohandley)
- **PanelEdit:** Allow test id to be passed to panel editors. [#55417](https://github.com/grafana/grafana/pull/55417), [@mckn](https://github.com/mckn)
- **Plugins:** Add hook to make it easier to track interactions in plugins. [#56126](https://github.com/grafana/grafana/pull/56126), [@mckn](https://github.com/mckn)
- **Plugins:** Introduce new Flame graph panel. [#56376](https://github.com/grafana/grafana/pull/56376), [@joey-grafana](https://github.com/joey-grafana)
- **Plugins:** Make "README" the default markdown request param. [#58264](https://github.com/grafana/grafana/pull/58264), [@wbrowne](https://github.com/wbrowne)
- **PostgreSQL:** Migrate to React. [#52831](https://github.com/grafana/grafana/pull/52831), [@zoltanbedi](https://github.com/zoltanbedi)
- **Preferences:** Create indices. [#48356](https://github.com/grafana/grafana/pull/48356), [@sakjur](https://github.com/sakjur)
- **Profiling:** Add Phlare and Parca datasources. [#57809](https://github.com/grafana/grafana/pull/57809), [@aocenas](https://github.com/aocenas)
- **Prometheus:** Handle errors and warnings in buffered client. [#58504](https://github.com/grafana/grafana/pull/58504), [@itsmylife](https://github.com/itsmylife)
- **Prometheus:** Make Prometheus streaming parser as default client. [#58365](https://github.com/grafana/grafana/pull/58365), [@itsmylife](https://github.com/itsmylife)
- **Public Dashboards:** Add audit table. [#54508](https://github.com/grafana/grafana/pull/54508), [@jalevin](https://github.com/jalevin)
- **PublicDashboards:** Add PubDash support to Angular panel plugins. [#57293](https://github.com/grafana/grafana/pull/57293), [@mmandrus](https://github.com/mmandrus)
- **PublicDashboards:** Add annotations support. [#56413](https://github.com/grafana/grafana/pull/56413), [@owensmallwood](https://github.com/owensmallwood)
- **PublicDashboards:** Add custom branding for Public Dashboard. (Enterprise)
- **PublicDashboards:** Add delete public dashboard button in public dashboard modal. [#58095](https://github.com/grafana/grafana/pull/58095), [@juanicabanas](https://github.com/juanicabanas)
- **PublicDashboards:** Cached queries column added in public dashboard insight query. (Enterprise)
- **PublicDashboards:** Can toggle annotations in modal. [#57312](https://github.com/grafana/grafana/pull/57312), [@owensmallwood](https://github.com/owensmallwood)
- **PublicDashboards:** Delete public dashboard in public dashboard table. [#57766](https://github.com/grafana/grafana/pull/57766), [@juanicabanas](https://github.com/juanicabanas)
- **PublicDashboards:** Delete public dashboard when dashboard is deleted. [#57291](https://github.com/grafana/grafana/pull/57291), [@juanicabanas](https://github.com/juanicabanas)
- **PublicDashboards:** Extract config of Public Dashboard. [#57788](https://github.com/grafana/grafana/pull/57788), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **PublicDashboards:** Hide top navigation bar. [#56873](https://github.com/grafana/grafana/pull/56873), [@evictorero](https://github.com/evictorero)
- **PublicDashboards:** Make mixed datasource calls concurrently. [#56421](https://github.com/grafana/grafana/pull/56421), [@juanicabanas](https://github.com/juanicabanas)
- **PublicDashboards:** Orphaned public dashboard item list modified. [#58014](https://github.com/grafana/grafana/pull/58014), [@juanicabanas](https://github.com/juanicabanas)
- **PublicDashboards:** Rename PubdashFooter frontend component. [#58137](https://github.com/grafana/grafana/pull/58137), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **PublicDashboards:** Update docs with supported datasources. [#57629](https://github.com/grafana/grafana/pull/57629), [@owensmallwood](https://github.com/owensmallwood)
- **PublicDashboards:** Validate access token. [#57298](https://github.com/grafana/grafana/pull/57298), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **PublicDashboards:** Validate access token not to be duplicated and add retries. [#56755](https://github.com/grafana/grafana/pull/56755), [@juanicabanas](https://github.com/juanicabanas)
- **RBAC:** Improve performance of dashboard filter query. [#56813](https://github.com/grafana/grafana/pull/56813), [@kalleep](https://github.com/kalleep)
- **Rendering:** Add configuration options for `renderKey` lifetime. [#57339](https://github.com/grafana/grafana/pull/57339), [@Willena](https://github.com/Willena)
- **Reports:** Dynamic scale factor per report. (Enterprise)
- **SAML:** Set cookie option SameSite=none and Secure=true. (Enterprise)
- **SQLStore:** Optionally retry queries if sqlite returns database is locked. [#56096](https://github.com/grafana/grafana/pull/56096), [@papagian](https://github.com/papagian)
- **Server:** Make unix socket permission configurable. [#52944](https://github.com/grafana/grafana/pull/52944), [@unknowndevQwQ](https://github.com/unknowndevQwQ)
- **Tempo:** Add start time and end time parameters while querying traces. [#48068](https://github.com/grafana/grafana/pull/48068), [@bikashmishra100](https://github.com/bikashmishra100)
- **TimeSeries:** Render null-bounded points at data edges. [#57798](https://github.com/grafana/grafana/pull/57798), [@leeoniya](https://github.com/leeoniya)
- **Tracing:** Allow trace to logs for OpenSearch. [#58161](https://github.com/grafana/grafana/pull/58161), [@gabor](https://github.com/gabor)
- **Transformers:** PartitionByValues. [#56767](https://github.com/grafana/grafana/pull/56767), [@leeoniya](https://github.com/leeoniya)
- **UsageStats:** Add traces when sending usage stats. [#55474](https://github.com/grafana/grafana/pull/55474), [@sakjur](https://github.com/sakjur)
### Bug fixes
- **Alerting:** Fix mathexp.NoData in ConditionsCmd. [#56812](https://github.com/grafana/grafana/pull/56812), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **BarChart:** Fix coloring from thresholds and value mappings. [#58285](https://github.com/grafana/grafana/pull/58285), [@leeoniya](https://github.com/leeoniya)
- **BarChart:** Fix stacked hover. [#57711](https://github.com/grafana/grafana/pull/57711), [@leeoniya](https://github.com/leeoniya)
- **Explore:** Fix shared crosshair for logs, logsvolume and graph panels. [#57892](https://github.com/grafana/grafana/pull/57892), [@Elfo404](https://github.com/Elfo404)
- **Flame Graph:** Exact search. [#56769](https://github.com/grafana/grafana/pull/56769), [@joey-grafana](https://github.com/joey-grafana)
- **Flame Graph:** Fix for dashboard scrolling. [#56555](https://github.com/grafana/grafana/pull/56555), [@joey-grafana](https://github.com/joey-grafana)
- **LogContext:** Fix scroll behavior in context modal. [#56070](https://github.com/grafana/grafana/pull/56070), [@svennergr](https://github.com/svennergr)
- **Loki:** Fix showing of history of querying in query editor. [#57344](https://github.com/grafana/grafana/pull/57344), [@ivanahuckova](https://github.com/ivanahuckova)
- **OAuth:** Fix misleading warn log related to oauth and increase logged content. [#57336](https://github.com/grafana/grafana/pull/57336), [@Jguer](https://github.com/Jguer)
- **Plugins:** Plugin details page visual alignment issues. [#57729](https://github.com/grafana/grafana/issues/57729)
- **PublicDashboards:** Fix GET public dashboard that doesn't match. [#57571](https://github.com/grafana/grafana/pull/57571), [@juanicabanas](https://github.com/juanicabanas)
- **PublicDashboards:** Fix annotations error for public dashboards. [#57455](https://github.com/grafana/grafana/pull/57455), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **PublicDashboards:** Fix granularity discrepancy between public and original dashboard. [#57129](https://github.com/grafana/grafana/pull/57129), [@guicaulada](https://github.com/guicaulada)
- **PublicDashboards:** Fix granularity issue caused by query caching. (Enterprise)
- **PublicDashboards:** Fix hidden queries execution. (Enterprise)
- **RBAC:** Add primary key to seed_assignment table. [#56540](https://github.com/grafana/grafana/pull/56540), [@kalleep](https://github.com/kalleep)
- **Tempo:** Fix search removing service name from query. [#58630](https://github.com/grafana/grafana/pull/58630), [@joey-grafana](https://github.com/joey-grafana)
- **TimeRangeInput:** Fix clear button type. [#56545](https://github.com/grafana/grafana/pull/56545), [@Clarity-89](https://github.com/Clarity-89)
### Breaking changes
Removes the unused close-milestone command from `@grafana/toolkit`. Issue [#57062](https://github.com/grafana/grafana/issues/57062)
@grafana/toolkit `cherrypick` command was removed. Issue [#56114](https://github.com/grafana/grafana/issues/56114)
`EmotionPerfTest` is no longer exported from the `@grafana/ui` bundle. Issue [#56100](https://github.com/grafana/grafana/issues/56100)
Removing the unused `changelog` command in `@grafana/toolkit`. Issue [#56073](https://github.com/grafana/grafana/issues/56073)
### Deprecations
The interface type `LogsParser` in `grafana-data` is deprecated. Issue [#56242](https://github.com/grafana/grafana/issues/56242)
The following functions and classes related to logs are deprecated in the `grafana-ui` package: `getLogLevel`, `getLogLevelFromKey`, `addLogLevelToSeries`, `LogsParsers`, `calculateFieldStats`, `calculateLogsLabelStats`, `calculateStats`, `getParser`, `sortInAscendingOrder`, `sortInDescendingOrder`, `sortLogsResult`, `sortLogRows`, `checkLogsError`, `escapeUnescapedString`. Issue [#56077](https://github.com/grafana/grafana/issues/56077)
### Plugin development fixes & changes
- **Toolkit:** Deprecate `plugin:update-circleci` command. [#57743](https://github.com/grafana/grafana/pull/57743), [@academo](https://github.com/academo)
- **Toolkit:** Deprecate `plugin:github-publish` command. [#57726](https://github.com/grafana/grafana/pull/57726), [@academo](https://github.com/academo)
- **Toolkit:** Deprecate `plugin:bundle-managed` command and move its functionality to a bash script. [#57719](https://github.com/grafana/grafana/pull/57719), [@academo](https://github.com/academo)
- **Toolkit:** Deprecate and replace toolkit:build with plain yarn scripts. [#57620](https://github.com/grafana/grafana/pull/57620), [@academo](https://github.com/academo)
- **Toolkit:** Deprecate node-version-check command. [#57591](https://github.com/grafana/grafana/pull/57591), [@academo](https://github.com/academo)
- **Toolkit:** Deprecate searchTestData command. [#57589](https://github.com/grafana/grafana/pull/57589), [@academo](https://github.com/academo)
- **Toolkit:** Remove unused close-milestone command. [#57062](https://github.com/grafana/grafana/pull/57062), [@academo](https://github.com/academo)
- **Toolkit:** Remove unused legacy cherrypick command. [#56114](https://github.com/grafana/grafana/pull/56114), [@academo](https://github.com/academo)
- **Grafana UI:** Clean up bundle. [#56100](https://github.com/grafana/grafana/pull/56100), [@jackw](https://github.com/jackw)
- **Toolkit:** Deprecate `component:create` command. [#56086](https://github.com/grafana/grafana/pull/56086), [@academo](https://github.com/academo)
- **Toolkit:** Remove changelog command. [#56073](https://github.com/grafana/grafana/pull/56073), [@gitstart](https://github.com/gitstart)
<!-- 9.3.0-beta1 END -->
<!-- 9.2.4 START -->
# 9.2.4 (2022-11-07)

View File

@@ -1256,6 +1256,8 @@ license_path =
# enable = feature1,feature2
enable =
internationalization = true
# feature1 = true
# feature2 = false

View File

@@ -27,9 +27,6 @@ Before you can create your first dashboard, you need to add your data source.
**To add a data source:**
1. Select the cog icon on the side menu to show the configuration options.
{{< figure src="/static/img/docs/v75/sidemenu-datasource-7-5.png" max-width="150px" class="docs-image--no-shadow">}}
1. Select **Data sources**.
This opens the data sources page, which displays a list of previously configured data sources for the Grafana instance.
@@ -44,9 +41,7 @@ Before you can create your first dashboard, you need to add your data source.
1. Move the cursor over the data source you want to add.
{{< figure src="/static/img/docs/v75/select-data-source-7-5.png" max-width="700px" class="docs-image--no-shadow">}}
1. Select **Select**.
1. Click **Select**.
This opens the data source configuration page.
@@ -63,8 +58,6 @@ Each data source's configuration includes a permissions page where you can enabl
### Enable data source permissions
{{< figure src="/static/img/docs/enterprise/datasource_permissions_enable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/static/img/docs/enterprise/datasource_permissions_enable.gif" >}}
By default, data sources in an organization can be queried by any user in that organization. For example, a user with the `Viewer` role can issue any possible query to a data source, not just
queries that exist on dashboards they have access to.
@@ -82,8 +75,6 @@ When permissions are enabled for a data source in an organization, the user who
### Allow users and teams to query a data source
{{< figure src="/static/img/docs/enterprise/datasource_permissions_add_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/static/img/docs/enterprise/datasource_permissions_add.gif" >}}
After you have enabled permissions for a data source you can assign query permissions to users and teams which will allow access to query the data source.
**Assign query permission to users and teams:**
@@ -98,8 +89,6 @@ After you have enabled permissions for a data source you can assign query permis
### Disable data source permissions
{{< figure src="/static/img/docs/enterprise/datasource_permissions_disable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/static/img/docs/enterprise/datasource_permissions_disable.gif" >}}
If you have enabled permissions for a data source and want to return data source permissions to the default, then you can disable permissions with a click of a button.
Note that _all_ existing permissions created for the data source will be deleted.

View File

@@ -60,8 +60,6 @@ Complete this task when you want to view a list of existing organizations.
A list of organizations appears.
![Server Admin organization list](/static/img/docs/manage-users/server-org-list-7-3.png)
## Create an organization
Create an organization when you want to isolate dashboards and other resources from each other.
@@ -117,5 +115,3 @@ Edit an organization when you want to change its name.
1. Hover your cursor over the **Server Admin** (shield) icon until a menu appears, and click **Orgs**.
1. Click the organization you want to edit.
1. Update the organization name and click **Update**.
![Server admin Edit Organization](/static/img/docs/manage-users/server-admin-edit-org-7-3.png)

View File

@@ -88,8 +88,6 @@ To browse for available plugins:
1. Click the **All** filter to browse all available plugins.
1. Click the **Data sources**, **Panels**, or **Applications** buttons to filter by plugin type.
![Plugin catalog browse](/static/img/docs/plugins/plugins-catalog-browse-9.2.png)
### Install a plugin
To install a plugin:
@@ -101,8 +99,6 @@ To install a plugin:
When the update is complete, you see a confirmation message that the installation was successful.
![Plugin catalog install](/static/img/docs/plugins/plugins-catalog-install-8-1.png)
### Update a plugin
To update a plugin:
@@ -113,8 +109,6 @@ To update a plugin:
When the update is complete, you see a confirmation message that the update was successful.
![Plugin catalog update](/static/img/docs/plugins/plugins-catalog-update-8-1.png)
### Uninstall a plugin
To uninstall a plugin:
@@ -125,8 +119,6 @@ To uninstall a plugin:
When the update is complete, you see a confirmation message that the uninstall was successful.
![Plugin catalog uninstall](/static/img/docs/plugins/plugins-catalog-uninstall-8-1.png)
## Install Grafana plugins
Grafana supports data source, panel, and app plugins. Having panels as plugins makes it easy to create and add any kind of panel, to show your data, or improve your favorite dashboards. Apps enable the bundling of data sources, panels, dashboards, and Grafana pages into a cohesive experience.

View File

@@ -53,8 +53,6 @@ In both cases, the assignment applies only to the user, team or service account
4. In the **Role** column, select the fixed role that you want to assign to the user, team or service account.
5. Click **Update**.
![User role picker in an organization](/static/img/docs/enterprise/user_role_picker_in_org.png)
**To assign a fixed role as a server administrator:**
1. Sign in to Grafana, hover your cursor over **Server Admin** (the shield icon) in the left navigation menu, and click **Users**.
@@ -62,8 +60,6 @@ In both cases, the assignment applies only to the user, team or service account
1. In the **Organizations** section, select a role within an organization that you want to assign to the user.
1. Click **Update**.
![User role picker in Organization](/static/img/docs/enterprise/user_role_picker_global.png)
## Assign fixed or custom roles to a team using provisioning
Instead of using the Grafana role picker, you can use file-based provisioning to assign fixed roles to teams. If you have a large number of teams, provisioning can provide an easier approach to assigning and managing role assignments.

View File

@@ -14,9 +14,10 @@ weight: 30
The table below describes all RBAC configuration options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../../../../setup-grafana/configure-grafana/#configure-with-environment-variables" >}}).
| Setting | Required | Description | Default |
| ------------------ | -------- | ---------------------------------------------------------------------------- | ------- |
| `permission_cache` | No | Enable to use in memory cache for loading and evaluating users' permissions. | `true` |
| Setting | Required | Description | Default |
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `permission_cache` | No | Enable to use in memory cache for loading and evaluating users' permissions. | `true` |
| `permission_validation_enabled` | No | Grafana enforces validation for permissions when a user creates or updates a role. The system checks the internal list of scopes and actions for each permission to determine they are valid. By default, if a scope or action is not recognized, Grafana logs a warning message. When set to `true`, Grafana returns an error. | `false` |
## Example RBAC configuration

View File

@@ -62,8 +62,6 @@ To add a team member:
1. Choose if you want to add the user as a team Member or an Admin.
1. Click **Add to team**.
![Add team member](/static/img/docs/manage-users/add-team-member-7-3.png)
## Grant team member permissions
Complete this task when you want to add or modify team member permissions.
@@ -76,8 +74,6 @@ To grant team member permissions:
1. In the team member list, find and click the user that you want to change. You can use the search field to filter the list if necessary.
1. Click the **Permission** list, and then click the new user permission level.
![Change team member permissions](/static/img/docs/manage-users/change-team-permissions-7-3.png)
## Remove a team member
You can remove a team member when you no longer want to apply team permissions to the user

View File

@@ -33,8 +33,6 @@ You can see a list of users with accounts in your Grafana organization. If neces
1. Sign in to Grafana as an organization administrator.
1. Hover your cursor over the **Configuration** (gear) icon in the side menu and click **Users**.
![Org Admin user list](/static/img/docs/manage-users/org-user-list-7-3.png)
> **Note:** If you have [server administrator]({{< relref "../../roles-and-permissions/#grafana-server-administrators" >}}) permissions, you can also [view a global list of users]({{< relref "../server-user-management#view-a-list-of-users" >}}) in the Server Admin section of Grafana.
## Change a user's organization permissions
@@ -96,8 +94,6 @@ When you invite users to join an organization, you assign the **Admin**, **Edito
If the invitee is not already a user, the system adds them.
![Invite User](/static/img/docs/manage-users/org-invite-user-7-3.png).
## Manage a pending invitation
Periodically review invitations you have sent so that you can see a list of users that have not yet accepted the invitation or cancel a pending invitation.
@@ -116,14 +112,10 @@ Periodically review invitations you have sent so that you can see a list of user
The **Pending Invites** button appears only when there are unaccepted invitations.
![Pending Invites button](/static/img/docs/manage-users/pending-invites-button-7-3.png)
To cancel an invitation, click the red **X** next to the invitation.
To copy an invitation link and send it directly to a user, click Copy Invite. You can then paste the invite link into a message.
![Pending Invites list](/static/img/docs/manage-users/pending-invites-list-7-3.png)
## Remove a user from an organization
You can remove a user from an organization when they no longer require access to the dashboard or data sources owned by the organization. No longer requiring access to an organization might occur when the user has left your company or has internally moved to another organization.

View File

@@ -38,8 +38,6 @@ You can see a list of users with accounts on your Grafana server. This action mi
1. Sign in to Grafana as a server administrator.
1. Hover your cursor over the **Server Admin** (shield) icon until a menu appears, and click **Users**.
![Server Admin user list](/static/img/docs/manage-users/server-user-list-7-3.png)
> **Note:** If you have [organization administrator]({{< relref "../../roles-and-permissions/#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../../roles-and-permissions/#grafana-server-administrators" >}}) permissions, you can still [view of list of users in a given organization]({{< relref "../manage-org-users/#view-a-list-of-organization-users" >}}).
## View user details
@@ -62,26 +60,18 @@ A user account contains the following sections.
This section contains basic user information, which users can update.
![Server Admin user information section](/static/img/docs/manage-users/server-admin-user-information-7-3.png)
#### Permissions
This indicates whether the user account has the Grafana administrator flag applied. If the flag is set to **Yes**, then the user is a Grafana server administrator.
![Server Admin Permissions section](/static/img/docs/manage-users/server-admin-permissions-7-3.png)
#### Organizations
This section lists the organizations the user belongs to and their assigned role.
![Server Admin Organizations section](/static/img/docs/manage-users/server-admin-organisations-7-3.png)
#### Sessions
This section includes recent user sessions and information about the time the user logged in and they system they used. You can force logouts, if necessary.
![Server Admin Sessions section](/static/img/docs/manage-users/server-admin-sessions-7-3.png)
## Edit a user account
Edit a user account when you want to modify user login credentials, or delete, disable, or enable a user.

View File

@@ -10,18 +10,34 @@ keywords:
- guide
- rules
- create
title: Annotations and labels for alerting rules
title: Labels and annotations
weight: 401
---
# Annotations and labels for alerting rules
# Labels and annotations
Annotations and labels are key value pairs associated with alerts originating from the alerting rule, datasource response, and as a result of alerting rule evaluation. They can be used in alert notifications directly or in templates and template functions to create notification content dynamically.
Labels and annotations contain information about an alert. Both labels and annotations have the same structure: a set of named values; however their intended uses are different. An example of label, or the equivalent annotation, might be `alertname="test"`.
## Annotations
The main difference between a label and an annotation is that labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert.
Annotations are key-value pairs that provide additional meta-information about an alert. You can use the following annotations: `description`, `summary`, `runbook_url`, `alertId`, `dashboardUid`, and `panelId`. For example, a description, a summary, and a runbook URL. These are displayed in rule and alert details in the UI and can be used in contact point message templates.
For example, consider two high CPU alerts: one for `server1` and another for `server2`. In such an example we might have a label called `server` where the first alert has the label `server="server1"` and the second alert has the label `server="server2"`. However, we might also want to add a description to each alert such as `"The CPU usage for server1 is above 75%."`, where `server1` and `75%` are replaced with the name and CPU usage of the server (please refer to the documentation on [templating labels and annotations]({{< relref "./variables-label-annotation" >}}) for how to do this). This kind of description would be more suitable as an annotation.
## Labels
Labels are key-value pairs that contain information about, and are used to uniquely identify an alert. The label set for an alert is generated and added to throughout the alerting evaluation and notification process.
Labels contain information that identifies an alert. An example of a label might be `server=server1`. Each alert can have more than one label, and the complete set of labels for an alert is called its label set. It is this label set that identifies the alert.
For example, an alert might have the label set `{alertname="High CPU usage",server="server1"}` while another alert might have the label set `{alertname="High CPU usage",server="server2"}`. These are two separate alerts because although their `alertname` labels are the same, their `server` labels are different.
The label set for an alert is a combination of the labels from the datasource, custom labels from the alert rule, and a number of reserved labels such as `alertname`.
### Custom Labels
Custom labels are additional labels from the alert rule. Like annotations, custom labels must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. Documentation on how to template custom labels can be found [here]({{< relref "./variables-label-annotation" >}}).
When using custom labels with templates it is important to make sure that the label value does not change between consecutive evaluations of the alert rule as this will end up creating large numbers of distinct alerts. However, it is OK for the template to produce different label values for different alerts. For example, do not put the value of the query in a custom label as this will end up creating a new set of alerts each time the value changes. Instead use annotations.
It is also important to make sure that the label set for an alert does not have two or more labels with the same name. If a custom label has the same name as a label from the datasource then it will replace that label. However, should a custom label have the same name as a reserved label then the custom label will be omitted from the alert.
## Annotations
Annotations are named pairs that add additional information to existing alerts. There are a number of suggested annotations in Grafana such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`. Like custom labels, annotations must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. If an annotation contains template code, the template is evaluated once when the alert is fired. It is not re-evaluated, even when the alert is resolved. Documentation on how to template annotations can be found [here]({{< relref "./variables-label-annotation" >}}).

View File

@@ -1,27 +1,23 @@
---
aliases:
- /docs/grafana/latest/alerting/fundamentals/annotation-label/variables-label-annotation/
description: Learn about labels and label matchers in alerting
description: Learn about templating of labels and annotations
keywords:
- grafana
- alerting
- guide
- fundamentals
title: How to template annotations and labels
title: Templating labels and annotations
weight: 117
---
# How to template annotations and labels
# Templating labels and annotations
In Grafana it is possible to template annotations and labels just like you would in Prometheus. Those who have used
Prometheus before should be familiar with the `$labels` variable which holds the label key/value pairs of the alert
instance and the `$value` variable which holds the evaluated value of the alert instance.
In Grafana it is possible to template labels and annotations just like you would in Prometheus. Those who have used Prometheus before should be familiar with the `$labels` variable which holds the label key/value pairs of the alert instance and the `$value` variable which holds the evaluated value of the alert instance.
In Grafana it is possible to use the same variables from Prometheus to template annotations and labels, even if your
alert does not use a Prometheus datasource.
In Grafana it is possible to use the same variables from Prometheus to template labels and annotations, even if your alert does not use a Prometheus datasource.
For example, let's suppose we want to create an alert in Grafana that tells us when one of our instances is down for
more than 5 minutes. Like in Prometheus, we can add a summary annotation to show the instance which is down:
For example, let's suppose we want to create an alert in Grafana that tells us when one of our instances is down for more than 5 minutes. Like in Prometheus, we can add a summary annotation to show the instance which is down:
```
Instance {{ $labels.instance }} has been down for more than 5 minutes
@@ -43,17 +39,13 @@ of the condition at the time the alert fired. For example:
## Alert rules with two or more queries or expressions
In the case where an alert rule has two or more queries, or uses reduce and math expressions, it is possible to template
the reduced result of each query and expression with the `$values` variable. This variable holds the labels and value of
each reduced query, and the results of any math expressions. However, it does not hold the samples for each query.
In the case where an alert rule has two or more queries, or uses reduce and math expressions, it is possible to template the reduced result of each query and expression with the `$values` variable. This variable holds the labels and value of each reduced query, and the results of any math expressions. However, it does not hold the samples for each query.
For example, suppose you have the following alert rule:
{{< figure src="/static/img/docs/alerting/unified/grafana-alerting-histogram-quantile.png" class="docs-image--no-shadow" caption="An alert rule that uses histogram_quantile to compute 95th percentile" >}}
Should this rule create an alert instance `$values` will hold the result of the reduce expression `B` and the math
expression `C`. It will not hold the results returned by query `A` because query `A` does not return a single value
but rather a series of values over time.
Should this rule create an alert instance `$values` will hold the result of the reduce expression `B` and the math expression `C`. It will not hold the results returned by query `A` because query `A` does not return a single value but rather a series of values over time.
If we were to write a summary annotation such as:
@@ -61,15 +53,13 @@ If we were to write a summary annotation such as:
{{ $labels.instance }} has a 95th percentile request latency above 1s: {{ $value }})
```
We would find that because the condition of the alert, the math expression `C` must be a boolean comparison, it must
return either a `0` or a `1`. What we want instead is the 95th percentile from the reduce expression `B`:
We would find that because the condition of the alert, the math expression `C` must be a boolean comparison, it must return either a `0` or a `1`. What we want instead is the 95th percentile from the reduce expression `B`:
```
{{ $labels.instance }} has a 95th percentile request latency above 1s: {{ $values.B }})
```
We can also show the labels of `B`, however since this alert rule has just one query the labels of `B` are equivalent to
`$labels`:
We can also show the labels of `B`, however since this alert rule has just one query the labels of `B` are equivalent to `$labels`:
```
{{ $values.B.Labels.instance }} has a 95th percentile request latency above 1s: {{ $values.B }})
@@ -78,8 +68,7 @@ We can also show the labels of `B`, however since this alert rule has just one q
### No data and execution errors or timeouts
Should query `A` return no data then the reduce expression `B` will also return no data. This means that
`{{ $values.B }}` will be nil. To ensure that annotations and labels can still be templated even when a query returns
no data, we can use an if statement to check for `$values.B`:
`{{ $values.B }}` will be nil. To ensure that labels and annotations can still be templated even when a query returns no data, we can use an if statement to check for `$values.B`:
```
{{ if $values.B }}{{ $labels.instance }} has a 95th percentile request latency above 1s: {{ $values.B }}){{ end }}
@@ -87,12 +76,11 @@ no data, we can use an if statement to check for `$values.B`:
## Classic conditions
If the rule uses a classic condition instead of a reduce and math expression, then `$values` contains the combination
of the `refID` and position of the condition. For example, `{{ $values.A0 }}` and `{{ $values.A1 }}`.
If the rule uses a classic condition instead of a reduce and math expression, then `$values` contains the combination of the `refID` and position of the condition. For example, `{{ $values.A0 }}` and `{{ $values.A1 }}`.
## Variables
The following template variables are available when expanding annotations and labels.
The following template variables are available when expanding labels and annotations.
| Name | Description |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -12,34 +12,58 @@ weight: 460
# Use images in notifications
Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including an image of the panel associated with the Grafana managed alert rule.
Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including a screenshot of the panel associated with the alert.
> **Note**: Images in notifications are not available for Grafana Mimir and Loki managed alert rules, or when Grafana is set up to send alert notifications to an external Alertmanager.
> **Note**: This feature is not supported for Mimir or Loki rules, or when Grafana sends alert notifications to an external Alertmanager.
If Grafana is set up to send images in notifications, it takes a screenshot of the panel for the Grafana managed alert rule when either of the following happen:
When an alert is fired or resolved Grafana takes a screenshot of the panel associated with the alert. This is determined via the Dashboard UID and Panel ID annotations of the rule. Grafana cannot take a screenshot for alerts that are not associated with a panel.
1. The alert rule transitions from pending to firing
2. The alert rule transitions from firing to OK
Because a number of contact points, such as email, do not support uploading screenshots at the time of sending a notification; Grafana can also upload the screenshot to a cloud storage service such as Amazon S3, Azure Blob Storage and Google Cloud Storage, where a link to the uploaded screenshot can be added to the notification. However, if using a cloud storage service is not an option then Grafana can be its own cloud storage service such that the screenshot is available under the same domain as Grafana.
Grafana does not support images for alert rules that are not associated with a panel. An alert rule is associated with a panel when it has both Dashboard UID and Panel ID annotations.
Should either the cloud storage service, or Grafana if acting as its own cloud storage service, be protected by a firewall, gateway service or VPN, then screenshots might not be shown in notifications.
Images are stored in the [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) path and so Grafana must have write-access to this path. If Grafana cannot write to this path then screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt. In addition to storing images on disk, Grafana can also store the image in an external image store such as Amazon S3, Azure Blob Storage, Google Cloud Storage and even Grafana where screenshots are stored in `public/img/attachments`. Screenshots older than `temp_data_lifetime` are deleted from disk but not the external image store. If Grafana is the external image store then screenshots are deleted from `data` but not from `public/img/attachments`.
How to choose between uploading screenshots at the time of sending the notification, using a cloud storage service, or using Grafana as its own cloud storage service, depends on which contact points you plan to use and whether you use a firewall, gateway service or VPN.
> **Note**: It is recommended that you use an external image store, as not all contact points support uploading images from disk. It is also possible that the image on disk is deleted before an alert notification is sent if `temp_data_lifetime` is less than the `group_wait` and `group_interval` options used in Alertmanager.
For example, if a contact point supports uploading images at the time of notification is it not required to use cloud storage. Cloud storage is required when a contact point does not support uploading images at the time of sending a notification, such as email. We don't recommend using cloud storage if the cloud storage service is behind a firewall, gateway service, or VPN, as screenshots might not be shown in notifications.
Please refer to the table at the end of this page for a list of contact points and their support for images in notifications.
## Requirements
To use images in notifications, Grafana must be set up to use [image rendering](https://grafana.com/docs/grafana/next/setup-grafana/image-rendering/). It is also recommended that Grafana is set up to upload images to an external image store, such as Amazon S3, Azure Blob Storage, Google Cloud Storage or even Grafana.
To use images in notifications, Grafana must be set up to use [image rendering](https://grafana.com/docs/grafana/next/setup-grafana/image-rendering/). You can either install the image rendering plugin or run it as a remote rendering service.
When a screenshot is taken it is saved to the [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) path. This is where screenshots are stored before being sent in a notification or uploaded to a cloud storage service. Grafana must have write-access to this path. If Grafana cannot write to this path then screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt.
If using a [cloud storage service](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storage) such as Amazon S3, Azure Blob Storage or Google Cloud Storage, uploaded images need to be accessible outside of a firewall, gateway service or VPN for screenshots to be shown in notifications. Grafana will not delete screenshots from cloud storage. We recommend configuring a retention policy on the bucket to delete screenshots older than 1 month.
If using Grafana as its own cloud storage service then screenshots will be saved to `static_root_path/img/attachments`. `static_root_path` is a configuration option for Grafana and can be found in `defaults.ini`. However, like when using a cloud storage service, images need to be accessible outside of a firewall, gateway service or VPN for screenshots to be shown in notifications.
When using Grafana as its own cloud storage service screenshots are copied from [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) to `static_root_path/img/attachments`. Screenshots older than `temp_data_lifetime` are deleted from [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) but not from `static_root_path/images/attachments`. To delete screenshots from `static_root_path` after a certain amount of time we recommend setting up a CRON job.
## Configuration
If Grafana has been set up to use image rendering, images in notifications can be turned on via the `capture` option in `[unified_alerting.screenshots]`:
Having installed either the image rendering plugin, or set up Grafana to use a remote rendering service, set `capture` in `[unified_alerting.screenshots]` to `true`:
# Enable screenshots in notifications. This option requires the Grafana Image Renderer plugin.
# For more information on configuration options, refer to [rendering].
capture = true
capture = false
It is recommended that `max_concurrent_screenshots` is set to a value that is less than or equal to `concurrent_render_request_limit`. The default value for both `max_concurrent_screenshots` and `concurrent_render_request_limit` is `5`:
If screenshots should be uploaded to cloud storage then `upload_external_image_storage` should also be set to `true`:
# Uploads screenshots to the local Grafana server or remote storage such as Azure, S3 and GCS. Please
# see [external_image_storage] for further configuration options. If this option is false, screenshots
# will be persisted to disk for up to temp_data_lifetime.
upload_external_image_storage = false
Please see [`[external_image_storage]`](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storage) for instructions on how to configure cloud storage. Grafana will not start if `upload_external_image_storage` is `true` and `[external_image_storage]` contains missing or invalid configuration.
If Grafana is acting as its own cloud storage then `[upload_external_image_storage]` should be set to `true` and the `local` provider should be set in [`[external_image_storage]`](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storage).
Restart Grafana for the changes to take effect.
## Advanced configuration
We recommended that `max_concurrent_screenshots` is less than or equal to `concurrent_render_request_limit`. The default value for both `max_concurrent_screenshots` and `concurrent_render_request_limit` is `5`:
# The maximum number of screenshots that can be taken at the same time. This option is different from
# concurrent_render_request_limit as max_concurrent_screenshots sets the number of concurrent screenshots
@@ -47,57 +71,60 @@ It is recommended that `max_concurrent_screenshots` is set to a value that is le
# the total number of concurrent screenshots across all Grafana services.
max_concurrent_screenshots = 5
If Grafana has been set up to use an external image store, `upload_external_image_storage` should be set to `true`:
## Support for images in contact points
# Uploads screenshots to the local Grafana server or remote storage such as Azure, S3 and GCS. Please
# see [external_image_storage] for further configuration options. If this option is false, screenshots
# will be persisted to disk for up to temp_data_lifetime.
upload_external_image_storage = false
Grafana supports a wide range of contact points with varied support for images in notifications. The table below shows the list of all contact points supported in Grafana and their support for uploading images at the time of sending the notification and images uploaded to cloud storage, including when Grafana is acting as its own cloud storage service.
Restart Grafana for the changes to take affect.
| Name | Upload image at time of notification | Cloud storage |
| ----------------------- | ------------------------------------ | ------------- |
| DingDing | No | No |
| Discord | Yes | Yes |
| Email | Yes | Yes |
| Google Hangouts Chat | No | Yes |
| Kafka | No | No |
| Line | No | No |
| Microsoft Teams | No | Yes |
| Opsgenie | No | Yes |
| Pagerduty | No | Yes |
| Prometheus Alertmanager | No | No |
| Pushover | Yes | No |
| Sensu Go | No | No |
| Slack | No (will be available in 9.4) | Yes |
| Telegram | Yes | No |
| Threema | No | No |
| VictorOps | No | No |
| Webhook | No | Yes |
| Cisco Webex Teams | No | Yes |
## Supported notifiers
## Limitations
Images in notifications are supported in the following notifiers and additional support will be added in the future:
- This feature is not supported for Mimir or Loki rules, or when Grafana sends alert notifications to an external Alertmanager.
- When multiple alerts are sent in a single notification a screenshot might be included for each alert. The order the images are shown in random.
- Some contact points support at most one image per notification. In this case, the first image associated with an alert will be attached.
- We don't recommend using cloud storage if the cloud storage service is behind a firewall, gateway service, or VPN, as screenshots might not be shown in notifications.
| Name | Upload images from disk | Include images from URL |
| ----------------------- | ----------------------- | ----------------------- |
| DingDing | No | No |
| Discord | Yes | Yes |
| Email | Yes | Yes |
| Google Hangouts Chat | No | Yes |
| Kafka | No | No |
| Line | No | No |
| Microsoft Teams | No | Yes |
| Opsgenie | No | Yes |
| Pagerduty | No | Yes |
| Prometheus Alertmanager | No | No |
| Pushover | Yes | No |
| Sensu Go | No | No |
| Slack | No | Yes |
| Telegram | No | No |
| Threema | No | No |
| VictorOps | No | No |
| Webhook | No | Yes |
| Cisco Webex Teams | No | Yes |
## Troubleshooting
Include images from URL refers to using the external image store.
If Grafana has been set up to send images in notifications, however notifications are still being received without them, follow the troubleshooting steps below:
1. Check that images in notifications has been set up as per the instructions.
2. Enable debug logging in Grafana and look for logs with the logger `ngalert.image`.
3. If the alert is not associated with a dashboard there will be logs for `Cannot take screenshot for alert rule as it is not associated with a dashboard`.
4. If the alert is associated with a dashboard, but no panel in the dashboard, there will be logs for `Cannot take screenshot for alert rule as it is not associated with a panel`.
5. If images cannot be taken because of mis-configuration or an issue with image rendering there will be logs for `Failed to take an image` including the Dashboard UID, Panel ID, and the error message.
6. Check that the contact point supports images in notifications, and the present configuration, as per the table.
7. If the image was uploaded to cloud storage make sure it is public.
8. If images are made available via Grafana's built in web server make sure it is accessible via the Internet.
## Metrics
Grafana provides the following metrics to observe the performance and failure rate of images in notifications.
For example, if a screenshot could not be taken within the expected time (10 seconds) then the counter `grafana_screenshot_failures_total` is updated.
- `grafana_screenshot_cache_hits_total`
- `grafana_screenshot_cache_misses_total`
- `grafana_alerting_image_cache_hits_total`
- `grafana_alerting_image_cache_misses_total`
- `grafana_screenshot_duration_seconds`
- `grafana_screenshot_failures_total`
- `grafana_screenshot_successes_total`
- `grafana_screenshot_upload_failures_total`
- `grafana_screenshot_upload_successes_total`
## Limitations
- Images in notifications are not available for Grafana Mimir and Loki managed alert rules, or when Grafana is set up to send alert notifications to an external Alertmanager.
- When alerts generated by different alert rules are sent in a single notification, there may be screenshots for each alert rule. This happens if an alert group contains multiple alerting rules. The order the images are attached is random. If you need to guarantee the ordering of images, make sure that your alert groups contain a single alerting rule.
- Some contact points only handle a single image. In this case, the first image associated with an alert will be attached. Because the ordering is random, this may not always be an image for the same alert rule. If you need to guarantee you receive a screenshot for a particular rule, make sure that your alert groups contain a single alerting rule.

View File

@@ -168,6 +168,21 @@ You can attach these permissions to the IAM role or IAM user you configured in [
}
```
**Cross-account observability:**
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Action": ["oam:ListSinks", "oam:ListAttachedLinks"],
"Effect": "Allow",
"Resource": "*"
}
]
}
```
### Configure CloudWatch settings
#### Namespaces of Custom Metrics

View File

@@ -214,6 +214,33 @@ When making `stats` queries in [Explore]({{< relref "../../../explore/" >}}), ma
{{< figure src="/static/img/docs/v70/explore-mode-switcher.png" max-width="500px" class="docs-image--right" caption="Explore mode switcher" >}}
## Cross-account observability
The CloudWatch plugin provides the ability to monitor and troubleshoot applications that span across multiple accounts within a region. Using cross-account observability, you can seamlessly search, visualize and analyze metrics and logs, without having to worry about account boundaries.
> **Note:** This feature is currently behind the `cloudWatchCrossAccountQuerying` 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.
> Grafana Cloud users can access this feature by [opening a support ticket in the Cloud Portal](https://grafana.com/profile/org#support).
### Getting started
To enable cross-account observability, first enable it in CloudWatch using the official [CloudWatch docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html), then add [two new API actions]({{< relref "../#cross-account-observability" >}}) to the IAM policy attached to the role/user running the plugin.
Cross-account querying is available in the plugin through the `Logs` mode and the `Metric search` mode. Once you have it configured correctly, you'll see a "Monitoring account" badge displayed in the query editor header.
{{< figure src="/static/img/docs/cloudwatch/cloudwatch-monitoring-badge-9.3.0.png" max-width="1200px" caption="Monitoring account badge" >}}
### Metrics editor
When you select the `Builder` mode within the Metric search editor, a new Account field displays. Use the Account field to specify which of the linked accounts to target for the given query. By default, the `All` option is specified, which will target all linked accounts.
While in `Code` mode, you can specify any math expression. If the Monitoring account badge displays in the query editor header, all `SEARCH` expressions entered in this field will be cross-account by default. You can limit the search to one or a set of accounts, as documented in the [AWS documentation](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html).
### Logs editor
The Log group selector allows you to specify what log groups to target in the logs query. If the Monitoring account badge is displayed in the query editor header, it is possible to search and select log groups across multiple accounts. You can use the Account field in the Log Group Selector to filter Log Groups by Account. If you have many log groups and do not see the log group you'd like to select in the selector, use the prefix search to narrow down the possible log groups.
### Deep-link Grafana panels to the CloudWatch console
{{< figure src="/static/img/docs/v70/cloudwatch-logs-deep-linking.png" max-width="500px" class="docs-image--right" caption="CloudWatch Logs deep linking" >}}

View File

@@ -61,7 +61,7 @@ Grafana Enterprise adds the following features:
- [Custom branding]({{< relref "../setup-grafana/configure-grafana/configure-custom-branding/" >}}) to customize Grafana from the brand and logo to the footer links.
- [Usage insights]({{< relref "../dashboards/assess-dashboard-usage/" >}}) to understand how your Grafana instance is used.
- [Recorded queries]({{< relref "../administration/recorded-queries" >}}) to see trends over time for your data sources.
- [Vault integration]({{< relref "../setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-hashicorp-key-vault/" >}}) to manage your configuration or provisioning secrets with Vault.
- [Vault integration]({{< relref "../setup-grafana/configure-security/configure-database-encryption/#encrypting-your-database-with-a-key-from-a-key-management-service-kms" >}}) to manage your configuration or provisioning secrets with Vault.
- [Auditing]({{< relref "../setup-grafana/configure-security/audit-grafana/" >}}) tracks important changes to your Grafana instance to help you manage and mitigate suspicious activity and meet compliance requirements.
- [Request security]({{< relref "../setup-grafana/configure-security/configure-request-security/" >}}) makes it possible to restrict outgoing requests from the Grafana server.
- [Settings updates at runtime]({{< relref "../setup-grafana/configure-grafana/settings-updates-at-runtime" >}}) allows you to update Grafana settings at runtime without requiring a restart.

View File

@@ -2,4 +2,5 @@
. scripts/grafana-server/variables
./e2e/run-suite verify/specs
# The run-suite script requires a second argument to determine if videos should be recorded
./e2e/run-suite verify/specs true

6
go.mod
View File

@@ -29,7 +29,7 @@ require (
github.com/BurntSushi/toml v1.1.0
github.com/Masterminds/semver v1.5.0
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
github.com/aws/aws-sdk-go v1.44.109
github.com/aws/aws-sdk-go v1.44.146
github.com/beevik/etree v1.1.0
github.com/benbjohnson/clock v1.3.0
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
@@ -108,7 +108,7 @@ require (
go.opentelemetry.io/otel/trace v1.7.0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
golang.org/x/sync v0.1.0
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
@@ -230,7 +230,7 @@ require (
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.9.0
go.uber.org/goleak v1.1.12 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/appengine v1.6.7 // indirect

13
go.sum
View File

@@ -368,8 +368,9 @@ github.com/aws/aws-sdk-go v1.38.60/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.40.37/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.109 h1:+Na5JPeS0kiEHoBp5Umcuuf+IDqXqD0lXnM920E31YI=
github.com/aws/aws-sdk-go v1.44.109/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.146 h1:7YdGgPxDPRJu/yYffzZp/H7yHzQ6AqmuNFZPYraaN8I=
github.com/aws/aws-sdk-go v1.44.146/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
github.com/aws/aws-sdk-go-v2 v1.16.2 h1:fqlCk6Iy3bnCumtrLz9r3mJ/2gUT0pJ0wLFVIdWh+JA=
@@ -2851,8 +2852,8 @@ golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -3073,14 +3074,16 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -1,6 +1,8 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"version": "9.4.0-pre"
"packages": [
"packages/*"
],
"version": "9.3.0-beta.1"
}

View File

@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"repository": "github:grafana/grafana",
"scripts": {
"build": "yarn i18n:compile && NODE_ENV=production webpack --config scripts/webpack/webpack.prod.js",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -34,7 +34,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "6.0.1",
"@grafana/schema": "9.4.0-pre",
"@grafana/schema": "9.3.0-beta.1",
"@types/d3-interpolate": "^1.4.0",
"d3-interpolate": "1.4.0",
"date-fns": "2.29.3",

View File

@@ -69,6 +69,7 @@ export interface FeatureToggles {
objectStore?: boolean;
traceqlEditor?: boolean;
flameGraph?: boolean;
cloudWatchCrossAccountQuerying?: boolean;
redshiftAsyncQueryDataSupport?: boolean;
athenaAsyncQueryDataSupport?: boolean;
increaseInMemDatabaseQueryCache?: boolean;

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",

View File

@@ -3,7 +3,7 @@ const execa = require('execa');
const { resolve, sep } = require('path');
const resolveBin = require('resolve-as-bin');
const cypress = (commandName, { updateScreenshots }) => {
const cypress = (commandName, { updateScreenshots, browser }) => {
// Support running an unpublished dev build
const dirname = __dirname.split(sep).pop();
const projectPath = resolve(`${__dirname}${dirname === 'dist' ? '/..' : ''}`);
@@ -16,6 +16,10 @@ const cypress = (commandName, { updateScreenshots }) => {
const cypressOptions = [commandName, '--env', `${CWD},${UPDATE_SCREENSHOTS}`, `--project=${projectPath}`];
if (browser) {
cypressOptions.push('--browser', browser);
}
const execaOptions = {
cwd: __dirname,
stdio: 'inherit',
@@ -32,17 +36,21 @@ const cypress = (commandName, { updateScreenshots }) => {
module.exports = () => {
const updateOption = '-u, --update-screenshots';
const updateDescription = 'update expected screenshots';
const browserOption = '-b, --browser <browser>';
const browserDescription = 'specify which browser to use';
program
.command('open')
.description('runs tests within the interactive GUI')
.option(updateOption, updateDescription)
.option(browserOption, browserDescription)
.action((options) => cypress('open', options));
program
.command('run')
.description('runs tests from the CLI without the GUI')
.option(updateOption, updateDescription)
.option(browserOption, browserDescription)
.action((options) => cypress('run', options));
program.parse(process.argv);

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -61,7 +61,7 @@
"@babel/core": "7.19.6",
"@babel/preset-env": "7.19.4",
"@cypress/webpack-preprocessor": "5.15.2",
"@grafana/e2e-selectors": "9.4.0-pre",
"@grafana/e2e-selectors": "9.3.0-beta.1",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "9.1.0",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -35,10 +35,10 @@
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
},
"dependencies": {
"@grafana/data": "9.4.0-pre",
"@grafana/e2e-selectors": "9.4.0-pre",
"@grafana/data": "9.3.0-beta.1",
"@grafana/e2e-selectors": "9.3.0-beta.1",
"@grafana/faro-web-sdk": "1.0.0-beta2",
"@grafana/ui": "9.4.0-pre",
"@grafana/ui": "9.3.0-beta.1",
"@sentry/browser": "6.19.7",
"history": "4.10.1",
"lodash": "4.17.21",

View File

@@ -42,7 +42,8 @@ jest.mock('../services', () => ({
describe('DataSourceWithBackend', () => {
test('check the executed queries', () => {
const mock = runQueryAndReturnFetchMock({
const { mock, ds } = createMockDatasource();
ds.query({
maxDataPoints: 10,
intervalMs: 5000,
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
@@ -93,8 +94,22 @@ describe('DataSourceWithBackend', () => {
`);
});
test('should apply template variables only for the current data source', () => {
const { mock, ds } = createMockDatasource();
ds.applyTemplateVariables = jest.fn();
ds.query({
maxDataPoints: 10,
intervalMs: 5000,
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
} as DataQueryRequest);
expect(mock.calls.length).toBe(1);
expect(ds.applyTemplateVariables).toHaveBeenCalledTimes(1);
});
test('check that the executed queries is hidden from inspector', () => {
const mock = runQueryAndReturnFetchMock({
const { mock, ds } = createMockDatasource();
ds.query({
maxDataPoints: 10,
intervalMs: 5000,
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
@@ -169,9 +184,7 @@ describe('DataSourceWithBackend', () => {
});
});
function runQueryAndReturnFetchMock(
request: DataQueryRequest
): jest.MockContext<Promise<FetchResponse>, BackendSrvRequest[]> {
function createMockDatasource() {
const settings = {
name: 'test',
id: 1234,
@@ -184,7 +197,5 @@ function runQueryAndReturnFetchMock(
mockDatasourceRequest.mockReturnValue(Promise.resolve({} as FetchResponse));
const ds = new MyDataSource(settings);
ds.query(request);
return mockDatasourceRequest.mock;
return { ds, mock: mockDatasourceRequest.mock };
}

View File

@@ -133,6 +133,7 @@ class DataSourceWithBackend<
const queries = targets.map((q) => {
let datasource = this.getRef();
let datasourceId = this.id;
let shouldApplyTemplateVariables = true;
if (isExpressionReference(q.datasource)) {
hasExpr = true;
@@ -149,8 +150,15 @@ class DataSourceWithBackend<
throw new Error(`Unknown Datasource: ${JSON.stringify(q.datasource)}`);
}
datasource = ds.rawRef ?? getDataSourceRef(ds);
datasourceId = ds.id;
const dsRef = ds.rawRef ?? getDataSourceRef(ds);
const dsId = ds.id;
if (dsRef.uid !== datasource.uid || datasourceId !== dsId) {
datasource = dsRef;
datasourceId = dsId;
// If the query is using a different datasource, we would need to retrieve the datasource
// instance (async) and apply the template variables but it seems it's not necessary for now.
shouldApplyTemplateVariables = false;
}
}
if (datasource.type?.length) {
pluginIDs.add(datasource.type);
@@ -159,7 +167,7 @@ class DataSourceWithBackend<
dsUIDs.add(datasource.uid);
}
return {
...this.applyTemplateVariables(q, request.scopedVars),
...(shouldApplyTemplateVariables ? this.applyTemplateVariables(q, request.scopedVars) : q),
datasource,
datasourceId, // deprecated!
intervalMs,
@@ -291,7 +299,7 @@ class DataSourceWithBackend<
const result = await lastValueFrom(
getBackendSrv().fetch<T>({
...options,
method: 'GET',
method: 'POST',
headers: options?.headers ? { ...options.headers, ...headers } : headers,
data: data ?? { ...data },
url: `/api/datasources/${this.id}/resources/${path}`,

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"description": "Grafana Schema Library",
"keywords": [
"typescript"

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -51,10 +51,10 @@
"@babel/preset-env": "7.18.9",
"@babel/preset-react": "7.18.6",
"@babel/preset-typescript": "7.18.6",
"@grafana/data": "9.4.0-pre",
"@grafana/data": "9.3.0-beta.1",
"@grafana/eslint-config": "5.0.0",
"@grafana/tsconfig": "^1.2.0-rc1",
"@grafana/ui": "9.4.0-pre",
"@grafana/ui": "9.3.0-beta.1",
"@jest/core": "27.5.1",
"@types/command-exists": "^1.2.0",
"@types/eslint": "8.4.1",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -47,9 +47,9 @@
"dependencies": {
"@emotion/css": "11.10.5",
"@emotion/react": "11.10.5",
"@grafana/data": "9.4.0-pre",
"@grafana/e2e-selectors": "9.4.0-pre",
"@grafana/schema": "9.4.0-pre",
"@grafana/data": "9.3.0-beta.1",
"@grafana/e2e-selectors": "9.3.0-beta.1",
"@grafana/schema": "9.3.0-beta.1",
"@leeoniya/ufuzzy": "0.8.0",
"@monaco-editor/react": "4.4.6",
"@popperjs/core": "2.11.6",

View File

@@ -3,6 +3,7 @@ import React, { useCallback, useRef, useState, useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '../../../src/utils/i18n';
import { useStyles2 } from '../../themes';
import { Button, ButtonProps } from '../Button';
import { Icon } from '../Icon/Icon';
@@ -62,7 +63,7 @@ export function ClipboardButton({
<>
{showCopySuccess && (
<InlineToast placement="top" referenceElement={buttonRef.current}>
Copied
<Trans i18nKey="clipboard-button.inline-toast.success">Copied</Trans>
</InlineToast>
)}

View File

@@ -4,6 +4,7 @@ import React, { FunctionComponent, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes/ThemeContext';
import { clearButtonStyles } from '../Button';
import { Icon } from '../Icon/Icon';
const getStyles = (theme: GrafanaTheme2) => ({
@@ -71,9 +72,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
label: collapse__header;
padding: ${theme.spacing(1, 2, 1, 2)};
display: flex;
cursor: inherit;
transition: all 0.1s linear;
cursor: pointer;
`,
headerCollapsed: css`
label: collapse__header--collapsed;
@@ -132,6 +131,7 @@ export const Collapse = ({
className,
children,
}: React.PropsWithChildren<Props>) => {
const buttonStyles = useStyles2(clearButtonStyles);
const style = useStyles2(getStyles);
const onClickToggle = () => {
if (onToggle) {
@@ -145,10 +145,10 @@ export const Collapse = ({
return (
<div className={panelClass}>
<div className={headerClass} onClick={onClickToggle}>
<button type="button" className={cx(buttonStyles, headerClass)} onClick={onClickToggle}>
{collapsible && <Icon className={style.icon} name={isOpen ? 'angle-down' : 'angle-right'} />}
<div className={cx([style.headerLabel])}>{label}</div>
</div>
</button>
{isOpen && (
<div className={cx([style.collapseBody])}>
<div className={loaderClass} />

View File

@@ -46,6 +46,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
body,
description,
confirmText,
confirmButtonVariant,
dismissText,
icon,
isOpen,
@@ -58,6 +59,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
body={body}
description={description}
confirmText={confirmText}
confirmButtonVariant={confirmButtonVariant}
dismissText={dismissText}
icon={icon}
onConfirm={onConfirm}
@@ -77,6 +79,7 @@ Basic.args = {
body: 'Are you sure you want to delete this user?',
description: 'Removing the user will not remove any dashboards the user has created',
confirmText: 'Delete',
confirmButtonVariant: 'destructive',
dismissText: 'Cancel',
icon: 'exclamation-triangle',
isOpen: true,
@@ -112,7 +115,7 @@ export const AlternativeAction: ComponentStory<typeof ConfirmModal> = ({
AlternativeAction.parameters = {
controls: {
exclude: [...defaultExcludes, 'confirmationText'],
exclude: [...defaultExcludes, 'confirmationText', 'confirmButtonVariant'],
},
};
@@ -155,7 +158,7 @@ export const WithConfirmation: ComponentStory<typeof ConfirmModal> = ({
WithConfirmation.parameters = {
controls: {
exclude: [...defaultExcludes, 'alternativeText'],
exclude: [...defaultExcludes, 'alternativeText', 'confirmButtonVariant'],
},
};

View File

@@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { HorizontalGroup, Input } from '..';
import { useStyles2 } from '../../themes';
import { IconName } from '../../types/icon';
import { Button } from '../Button';
import { Button, ButtonVariant } from '../Button';
import { Modal } from '../Modal/Modal';
export interface ConfirmModalProps {
@@ -31,6 +31,8 @@ export interface ConfirmModalProps {
confirmationText?: string;
/** Text for alternative button */
alternativeText?: string;
/** Confirm button variant */
confirmButtonVariant?: ButtonVariant;
/** Confirm action callback */
onConfirm(): void;
/** Dismiss action callback */
@@ -53,6 +55,7 @@ export const ConfirmModal = ({
onConfirm,
onDismiss,
onAlternative,
confirmButtonVariant = 'destructive',
}: ConfirmModalProps): JSX.Element => {
const [disabled, setDisabled] = useState(Boolean(confirmationText));
const styles = useStyles2(getStyles);
@@ -86,7 +89,7 @@ export const ConfirmModal = ({
{dismissText}
</Button>
<Button
variant="destructive"
variant={confirmButtonVariant}
onClick={onConfirm}
disabled={disabled}
ref={buttonRef}

View File

@@ -11,7 +11,7 @@ import { TooltipPlacement } from '../Tooltip/types';
export interface Props {
overlay: React.ReactElement | (() => React.ReactElement);
placement?: TooltipPlacement;
children: React.ReactElement;
children: React.ReactElement | ((isOpen: boolean) => React.ReactElement);
}
export const Dropdown = React.memo(({ children, overlay, placement }: Props) => {
@@ -38,7 +38,7 @@ export const Dropdown = React.memo(({ children, overlay, placement }: Props) =>
return (
<>
{React.cloneElement(children, {
{React.cloneElement(typeof children === 'function' ? children(visible) : children, {
ref: setTriggerRef,
})}
{visible && (

View File

@@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { IconName } from '../../types';
import { clearButtonStyles } from '../Button';
import { Icon } from '../Icon/Icon';
export interface FilterPillProps {
@@ -16,28 +17,28 @@ export interface FilterPillProps {
export const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick, icon = 'check' }) => {
const styles = useStyles2(getStyles);
const clearButton = useStyles2(clearButtonStyles);
return (
<div className={cx(styles.wrapper, selected && styles.selected)} onClick={onClick}>
<button type="button" className={cx(clearButton, styles.wrapper, selected && styles.selected)} onClick={onClick}>
<span>{label}</span>
{selected && <Icon name={icon} className={styles.icon} />}
</div>
</button>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css`
padding: ${theme.spacing(0.25)} ${theme.spacing(1)};
background: ${theme.colors.background.secondary};
border-radius: ${theme.shape.borderRadius(8)};
padding: ${theme.spacing(0, 2)};
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.fontWeightMedium};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.bodySmall.lineHeight};
color: ${theme.colors.text.secondary};
display: flex;
align-items: center;
height: 32px;
cursor: pointer;
&:hover {
background: ${theme.colors.action.hover};

View File

@@ -19,6 +19,9 @@ const field: FieldConfig = {
mode: ThresholdsMode.Absolute,
steps: [{ value: -Infinity, color: '#7EB26D' }],
},
custom: {
neeutral: 0,
},
};
const props: Props = {

View File

@@ -98,6 +98,7 @@ export class Gauge extends PureComponent<Props> {
gauge: {
min,
max,
neutralValue: field.custom?.neutral,
background: { color: backgroundColor },
border: { color: null },
shadow: { show: false },

View File

@@ -0,0 +1,41 @@
import React, { FC } from 'react';
import { Row } from 'react-table';
import { useStyles2 } from '../../themes';
import { Icon } from '../Icon/Icon';
import { getTableStyles } from './styles';
export interface Props {
row: Row;
expandedIndexes: Set<number>;
setExpandedIndexes: (indexes: Set<number>) => void;
}
export const RowExpander: FC<Props> = ({ row, expandedIndexes, setExpandedIndexes }) => {
const tableStyles = useStyles2(getTableStyles);
const isExpanded = expandedIndexes.has(row.index);
// Use Cell to render an expander for each row.
// We can use the getToggleRowExpandedProps prop-getter
// to build the expander.
return (
<div
className={tableStyles.expanderCell}
onClick={() => {
const newExpandedIndexes = new Set(expandedIndexes);
if (isExpanded) {
newExpandedIndexes.delete(row.index);
} else {
newExpandedIndexes.add(row.index);
}
setExpandedIndexes(newExpandedIndexes);
}}
>
<Icon
aria-label={isExpanded ? 'Close trace' : 'Open trace'}
name={isExpanded ? 'angle-down' : 'angle-right'}
size="xl"
/>
</div>
);
};

View File

@@ -5,6 +5,14 @@ import { Table } from './Table';
Used for displaying tabular data
## Sub-tables
Sub-tables are supported through the usage of the prop `subData` Dataframe array.
The frames are linked to each row using the following custom properties under `dataframe.meta.custom`
- **parentRowIndex**: number - The index of the parent row in the main dataframe (under the `data` prop of the Table component)
- **noHeader**: boolean - Sets the noHeader of each sub-table
## Usage
<Props of={Table} />

View File

@@ -36,7 +36,7 @@ const meta: ComponentMeta<typeof Table> = {
args: {
width: 700,
height: 500,
columnMinWidth: 150,
columnMinWidth: 130,
},
};
@@ -98,6 +98,61 @@ function buildData(theme: GrafanaTheme2, config: Record<string, FieldConfig>): D
return prepDataForStorybook([data], theme)[0];
}
function buildSubTablesData(theme: GrafanaTheme2, config: Record<string, FieldConfig>): DataFrame[] {
const frames: DataFrame[] = [];
for (let i = 0; i < 1000; i++) {
const data = new MutableDataFrame({
meta: {
custom: {
parentRowIndex: i,
},
},
fields: [
{ name: 'Time', type: FieldType.time, values: [] }, // The time field
{
name: 'Quantity',
type: FieldType.number,
values: [],
config: {
decimals: 0,
custom: {
align: 'center',
},
},
},
{ name: 'Quality', type: FieldType.string, values: [] }, // The time field
{
name: 'Progress',
type: FieldType.number,
values: [],
config: {
unit: 'percent',
min: 0,
max: 100,
},
},
],
});
for (const field of data.fields) {
field.config = merge(field.config, config[field.name]);
}
for (let i = 0; i < Math.random() * 4 + 1; i++) {
data.appendRow([
new Date().getTime(),
Math.random() * 2,
Math.random() > 0.7 ? 'Good' : 'Bad',
Math.random() * 100,
]);
}
frames.push(data);
}
return prepDataForStorybook(frames, theme);
}
function buildFooterData(data: DataFrame): FooterItem[] {
const values = data.fields[3].values.toArray();
const valueSum = values.reduce((prev, curr) => {
@@ -195,4 +250,23 @@ Pagination.args = {
enablePagination: true,
};
export const SubTables: ComponentStory<typeof Table> = (args) => {
const theme = useTheme2();
const data = buildData(theme, {});
const subData = buildSubTablesData(theme, {
Progress: {
custom: {
displayMode: 'gradient-gauge',
},
thresholds: defaultThresholds,
},
});
return (
<div className="panel-container" style={{ width: 'auto', height: 'unset' }}>
<Table {...args} data={data} subData={subData} />
</div>
);
};
export default meta;

View File

@@ -398,4 +398,54 @@ describe('Table', () => {
expect(() => screen.getByTestId('table-footer')).toThrow('Unable to find an element');
});
});
describe('when mounted with data and sub-data', () => {
it('then correct rows should be rendered and new table is rendered when expander is clicked', () => {
getTestContext({
subData: new Array(getDefaultDataFrame().length).fill(0).map((i) =>
toDataFrame({
name: 'A',
fields: [
{
name: 'number' + i,
type: FieldType.number,
values: [i, i, i],
config: {
custom: {
filterable: true,
},
},
},
],
meta: {
custom: {
parentRowIndex: i,
},
},
})
),
});
expect(getTable()).toBeInTheDocument();
expect(screen.getAllByRole('columnheader')).toHaveLength(4);
expect(getColumnHeader(/time/)).toBeInTheDocument();
expect(getColumnHeader(/temperature/)).toBeInTheDocument();
expect(getColumnHeader(/img/)).toBeInTheDocument();
const rows = within(getTable()).getAllByRole('row');
expect(rows).toHaveLength(5);
expect(getRowsData(rows)).toEqual([
{ time: '2021-01-01 00:00:00', temperature: '10', link: '10' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: 'NaN' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '11' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '12' },
]);
within(rows[1]).getByLabelText('Open trace').click();
const rowsAfterClick = within(getTable()).getAllByRole('row');
expect(within(rowsAfterClick[1]).getByRole('table')).toBeInTheDocument();
expect(within(rowsAfterClick[1]).getByText(/number0/)).toBeInTheDocument();
expect(within(rowsAfterClick[2]).queryByRole('table')).toBeNull();
});
});
});

View File

@@ -9,7 +9,8 @@ import {
useSortBy,
useTable,
} from 'react-table';
import { FixedSizeList } from 'react-window';
import usePrevious from 'react-use/lib/usePrevious';
import { VariableSizeList } from 'react-window';
import { DataFrame, getFieldDisplayName, Field } from '@grafana/data';
@@ -30,7 +31,14 @@ import {
TableFooterCalc,
GrafanaTableColumn,
} from './types';
import { getColumns, sortCaseInsensitive, sortNumber, getFooterItems, createFooterCalculationValues } from './utils';
import {
getColumns,
sortCaseInsensitive,
sortNumber,
getFooterItems,
createFooterCalculationValues,
EXPANDER_WIDTH,
} from './utils';
const COLUMN_MIN_WIDTH = 150;
@@ -51,6 +59,8 @@ export interface Props {
footerOptions?: TableFooterCalc;
footerValues?: FooterItem[];
enablePagination?: boolean;
/** @alpha */
subData?: DataFrame[];
}
function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
@@ -121,6 +131,7 @@ export const Table = memo((props: Props) => {
const {
ariaLabel,
data,
subData,
height,
onCellFilterAdded,
width,
@@ -134,13 +145,15 @@ export const Table = memo((props: Props) => {
enablePagination,
} = props;
const listRef = useRef<FixedSizeList>(null);
const listRef = useRef<VariableSizeList>(null);
const tableDivRef = useRef<HTMLDivElement>(null);
const fixedSizeListScrollbarRef = useRef<HTMLDivElement>(null);
const variableSizeListScrollbarRef = useRef<HTMLDivElement>(null);
const tableStyles = useStyles2(getTableStyles);
const theme = useTheme2();
const headerHeight = noHeader ? 0 : tableStyles.cellHeight;
const [footerItems, setFooterItems] = useState<FooterItem[] | undefined>(footerValues);
const [expandedIndexes, setExpandedIndexes] = useState<Set<number>>(new Set());
const prevExpandedIndexes = usePrevious(expandedIndexes);
const footerHeight = useMemo(() => {
const EXTENDED_ROW_HEIGHT = 33;
@@ -177,8 +190,8 @@ export const Table = memo((props: Props) => {
// React-table column definitions
const memoizedColumns = useMemo(
() => getColumns(data, width, columnMinWidth, footerItems),
[data, width, columnMinWidth, footerItems]
() => getColumns(data, width, columnMinWidth, expandedIndexes, setExpandedIndexes, !!subData?.length, footerItems),
[data, width, columnMinWidth, footerItems, subData, expandedIndexes]
);
// Internal react table state reducer
@@ -260,14 +273,23 @@ export const Table = memo((props: Props) => {
setPageSize(pageSize);
}, [pageSize, setPageSize]);
useEffect(() => {
// react-table caches the height of cells so we need to reset them when expanding/collapsing rows
// We need to take the minimum of the current expanded indexes and the previous expandedIndexes array to account
// for collapsed rows, since they disappear from expandedIndexes but still keep their expanded height
listRef.current?.resetAfterIndex(
Math.min(...Array.from(expandedIndexes), ...(prevExpandedIndexes ? Array.from(prevExpandedIndexes) : []))
);
}, [expandedIndexes, prevExpandedIndexes]);
useEffect(() => {
// To have the custom vertical scrollbar always visible (https://github.com/grafana/grafana/issues/52136),
// we need to bring the element from the FixedSizeList scope to the outer Table container scope,
// because the FixedSizeList scope has overflow. By moving scrollbar to container scope we will have
// we need to bring the element from the VariableSizeList scope to the outer Table container scope,
// because the VariableSizeList scope has overflow. By moving scrollbar to container scope we will have
// it always visible since the entire width is in view.
// Select the scrollbar element from the FixedSizeList scope
const listVerticalScrollbarHTML = (fixedSizeListScrollbarRef.current as HTMLDivElement)?.querySelector(
// Select the scrollbar element from the VariableSizeList scope
const listVerticalScrollbarHTML = (variableSizeListScrollbarRef.current as HTMLDivElement)?.querySelector(
'.track-vertical'
);
@@ -283,6 +305,36 @@ export const Table = memo((props: Props) => {
}
});
const renderSubTable = React.useCallback(
(rowIndex: number) => {
if (expandedIndexes.has(rowIndex)) {
const rowSubData = subData?.find((frame) => frame.meta?.custom?.parentRowIndex === rowIndex);
if (rowSubData) {
const noHeader = !!rowSubData.meta?.custom?.noHeader;
const subTableStyle: CSSProperties = {
height: tableStyles.rowHeight * (rowSubData.length + (noHeader ? 0 : 1)), // account for the header with + 1
background: theme.colors.emphasize(theme.colors.background.primary, 0.015),
paddingLeft: EXPANDER_WIDTH,
position: 'absolute',
bottom: 0,
};
return (
<div style={subTableStyle}>
<Table
data={rowSubData}
width={width - EXPANDER_WIDTH}
height={tableStyles.rowHeight * (rowSubData.length + 1)}
noHeader={noHeader}
/>
</div>
);
}
}
return null;
},
[expandedIndexes, subData, tableStyles.rowHeight, theme.colors, width]
);
const RenderRow = React.useCallback(
({ index: rowIndex, style }: { index: number; style: CSSProperties }) => {
let row = rows[rowIndex];
@@ -290,8 +342,11 @@ export const Table = memo((props: Props) => {
row = page[rowIndex];
}
prepareRow(row);
return (
<div {...row.getRowProps({ style })} className={tableStyles.row}>
{/*add the subtable to the DOM first to prevent a 1px border CSS issue on the last cell of the row*/}
{renderSubTable(rowIndex)}
{row.cells.map((cell: Cell, index: number) => (
<TableCell
key={index}
@@ -305,7 +360,7 @@ export const Table = memo((props: Props) => {
</div>
);
},
[onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles]
[onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles, renderSubTable]
);
const onNavigate = useCallback(
@@ -344,6 +399,17 @@ export const Table = memo((props: Props) => {
);
}
const getItemSize = (index: number): number => {
if (expandedIndexes.has(index)) {
const rowSubData = subData?.find((frame) => frame.meta?.custom?.parentRowIndex === index);
if (rowSubData) {
const noHeader = !!rowSubData.meta?.custom?.noHeader;
return tableStyles.rowHeight * (rowSubData.length + 1 + (noHeader ? 0 : 1)); // account for the header and the row data with + 1 + 1
}
}
return tableStyles.rowHeight;
};
const handleScroll: React.UIEventHandler = (event) => {
const { scrollTop } = event.target as HTMLDivElement;
@@ -358,18 +424,18 @@ export const Table = memo((props: Props) => {
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
{!noHeader && <HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} />}
{itemCount > 0 ? (
<div ref={fixedSizeListScrollbarRef}>
<div ref={variableSizeListScrollbarRef}>
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true}>
<FixedSizeList
<VariableSizeList
height={listHeight}
itemCount={itemCount}
itemSize={tableStyles.rowHeight}
itemSize={getItemSize}
width={'100%'}
ref={listRef}
style={{ overflow: undefined }}
>
{RenderRow}
</FixedSizeList>
</VariableSizeList>
</CustomScrollbar>
</div>
) : (

View File

@@ -11,6 +11,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
const lineHeight = theme.typography.body.lineHeight;
const bodyFontSize = 14;
const cellHeight = cellPadding * 2 + bodyFontSize * lineHeight;
const rowHeight = cellHeight + 2;
const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03);
const buildCellContainerStyle = (color?: string, background?: string, overflowOnHover?: boolean) => {
@@ -36,7 +37,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
label: ${overflowOnHover ? 'cellContainerOverflow' : 'cellContainerNoOverflow'};
padding: ${cellPadding}px;
width: 100%;
height: 100%;
height: ${rowHeight}px;
display: flex;
align-items: center;
border-right: 1px solid ${borderColor};
@@ -95,7 +96,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
buildCellContainerStyle,
cellPadding,
cellHeightInner: bodyFontSize * lineHeight,
rowHeight: cellHeight + 2,
rowHeight,
table: css`
height: 100%;
width: 100%;
@@ -253,6 +254,13 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
justify-content: center;
width: 100%;
`,
expanderCell: css`
display: flex;
flex-direction: column;
justify-content: center;
height: ${rowHeight}px;
cursor: pointer;
`,
};
};

View File

@@ -1,3 +1,5 @@
import { Row } from 'react-table';
import { ArrayVector, Field, FieldType, MutableDataFrame, SelectableValue } from '@grafana/data';
import {
@@ -44,21 +46,29 @@ function getData() {
describe('Table utils', () => {
describe('getColumns', () => {
it('Should build columns from DataFrame', () => {
const columns = getColumns(getData(), 1000, 120);
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
expect(columns[0].Header).toBe('Time');
expect(columns[1].Header).toBe('Value');
});
it('Should distribute width and use field config width', () => {
const columns = getColumns(getData(), 1000, 120);
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
expect(columns[0].width).toBe(450);
expect(columns[1].width).toBe(100);
});
it('Should distribute width and use field config width with expander enabled', () => {
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, true);
expect(columns[0].width).toBe(50); // expander column
expect(columns[1].width).toBe(425);
expect(columns[2].width).toBe(100);
});
it('Should set field on columns', () => {
const columns = getColumns(getData(), 1000, 120);
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
expect(columns[0].field.name).toBe('Time');
expect(columns[1].field.name).toBe('Value');
@@ -82,8 +92,8 @@ describe('Table utils', () => {
describe('filterByValue', () => {
describe('happy path', () => {
const field: any = { values: new ArrayVector(['a', 'aa', 'ab', 'b', 'ba', 'bb', 'c']) };
const rows: any = [
const field = { values: new ArrayVector(['a', 'aa', 'ab', 'b', 'ba', 'bb', 'c']) } as unknown as Field;
const rows = [
{ index: 0, values: { 0: 'a' } },
{ index: 1, values: { 0: 'aa' } },
{ index: 2, values: { 0: 'ab' } },
@@ -91,7 +101,7 @@ describe('Table utils', () => {
{ index: 4, values: { 0: 'ba' } },
{ index: 5, values: { 0: 'bb' } },
{ index: 6, values: { 0: 'c' } },
];
] as unknown as Row[];
const filterValues = [{ value: 'a' }, { value: 'b' }, { value: 'c' }];
const result = filterByValue(field)(rows, '0', filterValues);
@@ -106,8 +116,8 @@ describe('Table utils', () => {
describe('fast exit cases', () => {
describe('no rows', () => {
it('should return empty array', () => {
const field: any = { values: new ArrayVector(['a']) };
const rows: any = [];
const field = { values: new ArrayVector(['a']) } as unknown as Field;
const rows: Row[] = [];
const filterValues = [{ value: 'a' }];
const result = filterByValue(field)(rows, '', filterValues);
@@ -118,8 +128,8 @@ describe('Table utils', () => {
describe('no filterValues', () => {
it('should return rows', () => {
const field: any = { values: new ArrayVector(['a']) };
const rows: any = [{}];
const field = { values: new ArrayVector(['a']) } as unknown as Field;
const rows = [{}] as Row[];
const filterValues = undefined;
const result = filterByValue(field)(rows, '', filterValues);
@@ -131,7 +141,7 @@ describe('Table utils', () => {
describe('no field', () => {
it('should return rows', () => {
const field = undefined;
const rows: any = [{}];
const rows = [{}] as Row[];
const filterValues = [{ value: 'a' }];
const result = filterByValue(field)(rows, '', filterValues);
@@ -142,12 +152,12 @@ describe('Table utils', () => {
describe('missing id in values', () => {
it('should return rows', () => {
const field: any = { values: new ArrayVector(['a', 'b', 'c']) };
const rows: any = [
const field = { values: new ArrayVector(['a', 'b', 'c']) } as unknown as Field;
const rows = [
{ index: 0, values: { 0: 'a' } },
{ index: 1, values: { 0: 'b' } },
{ index: 2, values: { 0: 'c' } },
];
] as unknown as Row[];
const filterValues = [{ value: 'a' }, { value: 'b' }, { value: 'c' }];
const result = filterByValue(field)(rows, '1', filterValues);
@@ -188,7 +198,7 @@ describe('Table utils', () => {
text: '1.0',
}),
};
const rows: any[] = [];
const rows = [] as Row[];
const result = calculateUniqueFieldValues(rows, field);

View File

@@ -1,6 +1,7 @@
import { Property } from 'csstype';
import { clone } from 'lodash';
import memoizeOne from 'memoize-one';
import React from 'react';
import { Row } from 'react-table';
import {
@@ -23,6 +24,7 @@ import { getFooterValue } from './FooterRow';
import { GeoCell } from './GeoCell';
import { ImageCell } from './ImageCell';
import { JSONViewCell } from './JSONViewCell';
import { RowExpander } from './RowExpander';
import {
CellComponent,
TableCellDisplayMode,
@@ -32,6 +34,8 @@ import {
TableFooterCalc,
} from './types';
export const EXPANDER_WIDTH = 50;
export function getTextAlign(field?: Field): Property.JustifyContent {
if (!field) {
return 'flex-start';
@@ -61,11 +65,37 @@ export function getColumns(
data: DataFrame,
availableWidth: number,
columnMinWidth: number,
expandedIndexes: Set<number>,
setExpandedIndexes: (indexes: Set<number>) => void,
expander: boolean,
footerValues?: FooterItem[]
): GrafanaTableColumn[] {
const columns: GrafanaTableColumn[] = [];
const columns: GrafanaTableColumn[] = expander
? [
{
// Make an expander cell
Header: () => null, // No header
id: 'expander', // It needs an ID
Cell: ({ row }) => {
return <RowExpander row={row} expandedIndexes={expandedIndexes} setExpandedIndexes={setExpandedIndexes} />;
},
width: EXPANDER_WIDTH,
minWidth: EXPANDER_WIDTH,
filter: (rows: Row[], id: string, filterValues?: SelectableValue[]) => {
return [];
},
justifyContent: 'left',
field: data.fields[0],
sortType: 'basic',
},
]
: [];
let fieldCountWithoutWidth = 0;
if (expander) {
availableWidth -= EXPANDER_WIDTH;
}
for (const [fieldIndex, field] of data.fields.entries()) {
const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions;

View File

@@ -65,8 +65,8 @@ export class UPlotChart extends Component<PlotProps, UPlotChartState> {
});
const config: Options = {
width: this.props.width,
height: this.props.height,
width: Math.floor(this.props.width),
height: Math.floor(this.props.height),
...this.props.config.getConfig(),
};
@@ -93,8 +93,8 @@ export class UPlotChart extends Component<PlotProps, UPlotChartState> {
if (!sameDims(prevProps, this.props)) {
plot?.setSize({
width: this.props.width,
height: this.props.height,
width: Math.floor(this.props.width),
height: Math.floor(this.props.height),
});
} else if (!sameConfig(prevProps, this.props)) {
this.reinitPlot();

View File

@@ -1,6 +1,6 @@
{
"name": "@jaegertracing/jaeger-ui-components",
"version": "9.4.0-pre",
"version": "9.3.0-beta.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,10 +31,10 @@
},
"dependencies": {
"@emotion/css": "11.10.5",
"@grafana/data": "9.4.0-pre",
"@grafana/e2e-selectors": "9.4.0-pre",
"@grafana/runtime": "9.4.0-pre",
"@grafana/ui": "9.4.0-pre",
"@grafana/data": "9.3.0-beta.1",
"@grafana/e2e-selectors": "9.3.0-beta.1",
"@grafana/runtime": "9.3.0-beta.1",
"@grafana/ui": "9.3.0-beta.1",
"chance": "^1.0.10",
"classnames": "^2.2.5",
"combokeys": "^3.0.0",

View File

@@ -215,10 +215,20 @@ export default class ListView extends React.Component<TListViewProps> {
}
}
componentDidUpdate() {
componentDidUpdate(prevProps: TListViewProps) {
if (this._itemHolderElm) {
this._scanItemHeights();
}
// When windowScroller is set to false, we can continue to handle scrollElement
if (this.props.windowScroller) {
return;
}
// check if the scrollElement changes and update its scroll listener
if (prevProps.scrollElement !== this.props.scrollElement) {
prevProps.scrollElement?.removeEventListener('scroll', this._onScroll);
this._wrapperElm = this.props.scrollElement;
this._wrapperElm?.addEventListener('scroll', this._onScroll);
}
}
componentWillUnmount() {

View File

@@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
@@ -214,7 +215,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, &usertest.FakeUserService{}, sqlStore)
loginService := &logintest.LoginServiceFake{}
authenticator := &logintest.AuthenticatorFake{}
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake(), nil, featuremgmt.WithFeatures())
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake(), nil, featuremgmt.WithFeatures(), nil)
return ctxHdlr
}
@@ -250,15 +251,16 @@ func (s *fakeRenderService) Init() error {
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
store := sqlstore.InitTestDB(t)
hs := &HTTPServer{
Cfg: cfg,
Live: newTestLive(t, store),
License: &licensing.OSSLicensingService{},
Features: featuremgmt.WithFeatures(),
QuotaService: quotatest.New(false, nil),
RouteRegister: routing.NewRouteRegister(),
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()),
ldapGroups: ldap.ProvideGroupsService(),
Cfg: cfg,
Live: newTestLive(t, store),
License: &licensing.OSSLicensingService{},
Features: featuremgmt.WithFeatures(),
QuotaService: quotatest.New(false, nil),
RouteRegister: routing.NewRouteRegister(),
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()),
ldapGroups: ldap.ProvideGroupsService(),
accesscontrolService: actest.FakeService{},
}
sc := setupScenarioContext(t, url)

View File

@@ -470,7 +470,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
}
if liveerr != nil {
hs.log.Warn("unable to broadcast save event", "uid", dashboard.Uid, "error", err)
hs.log.Warn("unable to broadcast save event", "uid", dashboard.Uid, "error", liveerr)
}
}
@@ -478,6 +478,12 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, err)
}
// Clear permission cache for the user who's created the dashboard, so that new permissions are fetched for their next call
// Required for cases when caller wants to immediately interact with the newly created object
if newDashboard && !hs.accesscontrolService.IsDisabled() {
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
}
// connect library panels for this dashboard after the dashboard is stored and has an ID
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(ctx, c.SignedInUser, dashboard)
if err != nil {

View File

@@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry/corekind"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
@@ -1093,6 +1094,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
folderService: folderService,
Features: featuremgmt.WithFeatures(),
Kinds: corekind.NewBase(nil),
accesscontrolService: actest.FakeService{},
}
sc := setupScenarioContext(t, url)
@@ -1201,6 +1203,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
Features: featuremgmt.WithFeatures(),
dashboardVersionService: fakeDashboardVersionService,
Kinds: corekind.NewBase(nil),
accesscontrolService: actest.FakeService{},
}
sc := setupScenarioContext(t, url)

View File

@@ -396,6 +396,12 @@ func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to add datasource", err)
}
// Clear permission cache for the user who's created the data source, so that new permissions are fetched for their next call
// Required for cases when caller wants to immediately interact with the newly created object
if !hs.AccessControl.IsDisabled() {
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
}
ds := hs.convertModelToDtos(c.Req.Context(), cmd.Result)
return response.JSON(http.StatusOK, util.DynMap{
"message": "Datasource added",

View File

@@ -19,6 +19,8 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/services/org"
@@ -112,7 +114,9 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
DataSourcesService: &dataSourcesServiceMock{
expectedDatasource: &datasources.DataSource{},
},
Cfg: setting.NewCfg(),
Cfg: setting.NewCfg(),
AccessControl: acimpl.ProvideAccessControl(setting.NewCfg()),
accesscontrolService: actest.FakeService{},
}
sc := setupScenarioContext(t, "/api/datasources")
@@ -224,7 +228,9 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
DataSourcesService: &dataSourcesServiceMock{
expectedDatasource: &datasources.DataSource{},
},
Cfg: setting.NewCfg(),
Cfg: setting.NewCfg(),
AccessControl: acimpl.ProvideAccessControl(setting.NewCfg()),
accesscontrolService: actest.FakeService{},
}
sc := setupScenarioContext(t, "/api/datasources/1234")

View File

@@ -128,6 +128,12 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
return apierrors.ToFolderErrorResponse(err)
}
// Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call
// Required for cases when caller wants to immediately interact with the newly created object
if !hs.AccessControl.IsDisabled() {
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
}
g := guardian.New(c.Req.Context(), folder.ID, c.OrgID, c.SignedInUser)
// TODO set ParentUID if nested folders are enabled
return response.JSON(http.StatusOK, hs.newToFolderDto(c, g, folder))

View File

@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -242,10 +243,11 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st
store := mockstore.NewSQLStoreMock()
guardian.InitLegacyGuardian(store, dashSvc, teamSvc)
hs := HTTPServer{
AccessControl: acmock.New(),
folderService: folderService,
Cfg: setting.NewCfg(),
Features: featuremgmt.WithFeatures(),
AccessControl: acmock.New(),
folderService: folderService,
Cfg: setting.NewCfg(),
Features: featuremgmt.WithFeatures(),
accesscontrolService: actest.FakeService{},
}
sc := setupScenarioContext(t, url)

View File

@@ -58,7 +58,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
grafanaUpdateChecker: &updatechecker.GrafanaService{},
AccessControl: accesscontrolmock.New().WithDisabled(),
PluginSettings: pluginSettings.ProvideService(sqlStore, secretsService),
SocialService: social.ProvideService(cfg),
SocialService: social.ProvideService(cfg, features),
}
m := web.New()

View File

@@ -77,6 +77,11 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
settings["isPublicDashboardView"] = true
}
weekStart := ""
if prefs.WeekStart != nil {
weekStart = *prefs.WeekStart
}
data := dtos.IndexViewData{
User: &dtos.CurrentUser{
Id: c.UserID,
@@ -93,7 +98,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
IsGrafanaAdmin: c.IsGrafanaAdmin,
LightTheme: prefs.Theme == lightName,
Timezone: prefs.Timezone,
WeekStart: prefs.WeekStart,
WeekStart: weekStart,
Locale: locale,
HelpFlags1: c.HelpFlags1,
HasEditPermissionInFolders: hasEditPerm,

View File

@@ -97,9 +97,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
code := ctx.Query("code")
if code == "" {
// FIXME: access_type is a Google OAuth2 specific thing, consider refactoring this and moving to google_oauth.go
opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}
var opts []oauth2.AuthCodeOption
if provider.UsePKCE {
ascii, pkce, err := genPKCECode()
if err != nil {

View File

@@ -9,15 +9,15 @@ import (
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
@@ -36,7 +36,7 @@ func setupOAuthTest(t *testing.T, cfg *setting.Cfg) *web.Mux {
Cfg: cfg,
License: &licensing.OSSLicensingService{Cfg: cfg},
SQLStore: sqlStore,
SocialService: social.ProvideService(cfg),
SocialService: social.ProvideService(cfg, featuremgmt.WithFeatures()),
HooksService: hooks.ProvideService(),
SecretsService: fakes.NewFakeSecretsService(),
}

View File

@@ -82,12 +82,17 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team
}
}
weekStart := ""
if preference.WeekStart != nil {
weekStart = *preference.WeekStart
}
dto := dtos.Prefs{
Theme: preference.Theme,
HomeDashboardID: preference.HomeDashboardID,
HomeDashboardUID: dashboardUID,
Timezone: preference.Timezone,
WeekStart: preference.WeekStart,
WeekStart: weekStart,
}
if preference.JSONData != nil {

View File

@@ -41,6 +41,12 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to create Team", err)
}
// Clear permission cache for the user who's created the team, so that new permissions are fetched for their next call
// Required for cases when caller wants to immediately interact with the newly created object
if !hs.AccessControl.IsDisabled() {
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
}
if accessControlEnabled || (c.OrgRole == org.RoleEditor && hs.Cfg.EditorsCanAdmin) {
// if the request is authenticated using API tokens
// the SignedInUser is an empty struct therefore

View File

@@ -15,6 +15,8 @@ import (
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/org"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/preference/preftest"
@@ -213,6 +215,8 @@ func TestTeamAPIEndpoint_CreateTeam_RBAC(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.teamService = teamtest.NewFakeService()
hs.AccessControl = acimpl.ProvideAccessControl(setting.NewCfg())
hs.accesscontrolService = actest.FakeService{}
})
input := strings.NewReader(fmt.Sprintf(teamCmd, 1))

View File

@@ -144,11 +144,12 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) {
}
rn := &rawNode{
Query: rawQueryProp,
RefID: query.RefID,
TimeRange: query.TimeRange,
QueryType: query.QueryType,
DataSource: query.DataSource,
Query: rawQueryProp,
RefID: query.RefID,
TimeRange: query.TimeRange,
QueryType: query.QueryType,
DataSource: query.DataSource,
QueryEnricher: query.QueryEnricher,
}
var node Node

View File

@@ -42,11 +42,12 @@ type baseNode struct {
}
type rawNode struct {
RefID string `json:"refId"`
Query map[string]interface{}
QueryType string
TimeRange TimeRange
DataSource *datasources.DataSource
RefID string `json:"refId"`
Query map[string]interface{}
QueryType string
TimeRange TimeRange
DataSource *datasources.DataSource
QueryEnricher QueryDataRequestEnricher
}
func (rn *rawNode) GetCommandType() (c CommandType, err error) {
@@ -139,8 +140,9 @@ const (
// DSNode is a DPNode that holds a datasource request.
type DSNode struct {
baseNode
query json.RawMessage
datasource *datasources.DataSource
query json.RawMessage
datasource *datasources.DataSource
queryEnricher QueryDataRequestEnricher
orgID int64
queryType string
@@ -169,14 +171,15 @@ func (s *Service) buildDSNode(dp *simple.DirectedGraph, rn *rawNode, req *Reques
id: dp.NewNode().ID(),
refID: rn.RefID,
},
orgID: req.OrgId,
query: json.RawMessage(encodedQuery),
queryType: rn.QueryType,
intervalMS: defaultIntervalMS,
maxDP: defaultMaxDP,
timeRange: rn.TimeRange,
request: *req,
datasource: rn.DataSource,
orgID: req.OrgId,
query: json.RawMessage(encodedQuery),
queryType: rn.QueryType,
intervalMS: defaultIntervalMS,
maxDP: defaultMaxDP,
timeRange: rn.TimeRange,
request: *req,
datasource: rn.DataSource,
queryEnricher: rn.QueryEnricher,
}
var floatIntervalMS float64
@@ -211,24 +214,29 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s
OrgID: dn.orgID,
DataSourceInstanceSettings: dsInstanceSettings,
PluginID: dn.datasource.Type,
User: dn.request.User,
}
q := []backend.DataQuery{
{
RefID: dn.refID,
MaxDataPoints: dn.maxDP,
Interval: time.Duration(int64(time.Millisecond) * dn.intervalMS),
JSON: dn.query,
TimeRange: dn.timeRange.AbsoluteTime(now),
QueryType: dn.queryType,
},
}
resp, err := s.dataService.QueryData(ctx, &backend.QueryDataRequest{
req := &backend.QueryDataRequest{
PluginContext: pc,
Queries: q,
Headers: dn.request.Headers,
})
Queries: []backend.DataQuery{
{
RefID: dn.refID,
MaxDataPoints: dn.maxDP,
Interval: time.Duration(int64(time.Millisecond) * dn.intervalMS),
JSON: dn.query,
TimeRange: dn.timeRange.AbsoluteTime(now),
QueryType: dn.queryType,
},
},
Headers: dn.request.Headers,
}
if dn.queryEnricher != nil {
ctx = dn.queryEnricher(ctx, req)
}
resp, err := s.dataService.QueryData(ctx, req)
if err != nil {
return mathexp.Results{}, err
}
@@ -389,7 +397,7 @@ func extractNumberSet(frame *data.Frame) ([]mathexp.Number, error) {
labels[key] = val.(string) // TODO check assertion / return error
}
n := mathexp.NewNumber("", labels)
n := mathexp.NewNumber(frame.Fields[numericField].Name, labels)
// The new value fields' configs gets pointed to the one in the original frame
n.Frame.Fields[0].Config = frame.Fields[numericField].Config

View File

@@ -35,14 +35,19 @@ type Request struct {
Debug bool
OrgId int64
Queries []Query
User *backend.User
}
// QueryDataRequestEnricher function definition for enriching a backend.QueryDataRequest request.
type QueryDataRequestEnricher func(ctx context.Context, req *backend.QueryDataRequest) context.Context
// Query is like plugins.DataSubQuery, but with a a time range, and only the UID
// for the data source. Also interval is a time.Duration.
type Query struct {
RefID string
TimeRange TimeRange
DataSource *datasources.DataSource `json:"datasource"`
QueryEnricher QueryDataRequestEnricher
JSON json.RawMessage
Interval time.Duration
QueryType string

View File

@@ -0,0 +1,9 @@
package serverlock
type ServerLockExistsError struct {
actionName string
}
func (e *ServerLockExistsError) Error() string {
return "there is already a lock for this actionName: " + e.actionName
}

View File

@@ -2,7 +2,6 @@ package serverlock
import (
"context"
"errors"
"time"
"go.opentelemetry.io/otel/attribute"
@@ -185,7 +184,7 @@ func (sl *ServerLockService) acquireForRelease(ctx context.Context, actionName s
if len(lockRows) > 0 {
result := lockRows[0]
if sl.isLockWithinInterval(result, maxInterval) {
return errors.New("there is already a lock for this actionName: " + actionName)
return &ServerLockExistsError{actionName: actionName}
} else {
// lock has timeouted, so we update the timestamp
result.LastExecution = time.Now().Unix()

View File

@@ -13,6 +13,8 @@ import (
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
func trueBoolPtr() *bool {
@@ -54,7 +56,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false),
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
},
want: &BasicUserInfo{
Id: "1234",
@@ -93,7 +95,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false),
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
},
want: &BasicUserInfo{
Id: "1234",
@@ -143,7 +145,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Only other roles",
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false),
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
},
claims: &azureClaims{
Email: "me@example.com",
@@ -171,7 +173,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Editor", false),
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()),
},
want: &BasicUserInfo{
Id: "1234",
@@ -220,7 +222,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
},
{
name: "Grafana Admin but setting is disabled",
fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false)},
fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures())},
claims: &azureClaims{
Email: "me@example.com",
PreferredUsername: "",
@@ -242,7 +244,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
name: "Editor roles in claim and GrafanaAdminAssignment enabled",
fields: fields{
SocialBase: newSocialBase("azuread",
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false)},
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures())},
claims: &azureClaims{
Email: "me@example.com",
PreferredUsername: "",
@@ -263,7 +265,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Grafana Admin and Editor roles in claim",
fields: fields{SocialBase: newSocialBase("azuread",
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false)},
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures())},
claims: &azureClaims{
Email: "me@example.com",
PreferredUsername: "",
@@ -302,7 +304,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
fields: fields{
allowedGroups: []string{"foo", "bar"},
SocialBase: newSocialBase("azuread",
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer", false),
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer", false, *featuremgmt.WithFeatures()),
},
claims: &azureClaims{
Email: "me@example.com",
@@ -324,7 +326,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch groups when ClaimsNames and ClaimsSources is set",
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false),
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()),
},
claims: &azureClaims{
ID: "1",
@@ -349,7 +351,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch groups when forceUseGraphAPI is set",
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false),
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()),
forceUseGraphAPI: true,
},
claims: &azureClaims{
@@ -376,7 +378,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch empty role when strict attribute role is true and no match",
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false),
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()),
},
claims: &azureClaims{
Email: "me@example.com",
@@ -392,7 +394,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch empty role when strict attribute role is true and no role claims returned",
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false),
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()),
},
claims: &azureClaims{
Email: "me@example.com",
@@ -416,7 +418,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
}
if tt.fields.SocialBase == nil {
s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false)
s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures())
}
key := []byte("secret")

View File

@@ -15,6 +15,8 @@ import (
"github.com/grafana/grafana/pkg/models"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
type SocialGenericOAuth struct {
@@ -509,3 +511,10 @@ func (s *SocialGenericOAuth) FetchOrganizations(client *http.Client) ([]string,
return logins, true
}
func (s *SocialGenericOAuth) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
opts = append(opts, oauth2.AccessTypeOffline)
}
return s.SocialBase.AuthCodeURL(state, opts...)
}

View File

@@ -9,6 +9,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
const testGHUserTeamsJSON = `[
@@ -202,7 +204,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
s := &SocialGithub{
SocialBase: newSocialBase("github", &oauth2.Config{},
&OAuthInfo{RoleAttributePath: tt.roleAttributePath}, tt.autoAssignOrgRole, false),
&OAuthInfo{RoleAttributePath: tt.roleAttributePath}, tt.autoAssignOrgRole, false, *featuremgmt.WithFeatures()),
allowedOrganizations: []string{},
apiUrl: server.URL + "/user",
teamIds: []int{},

View File

@@ -8,6 +8,8 @@ import (
"github.com/grafana/grafana/pkg/models"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
type SocialGoogle struct {
@@ -44,3 +46,10 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
Login: data.Email,
}, nil
}
func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}
return s.SocialBase.AuthCodeURL(state, opts...)
}

View File

@@ -16,6 +16,7 @@ import (
"golang.org/x/text/language"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -58,7 +59,7 @@ type OAuthInfo struct {
UsePKCE bool
}
func ProvideService(cfg *setting.Cfg) *SocialService {
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *SocialService {
ss := SocialService{
cfg: cfg,
oAuthProvider: make(map[string]*OAuthInfo),
@@ -139,7 +140,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
// GitHub.
if name == "github" {
ss.socialMap["github"] = &SocialGithub{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamIds: sec.Key("team_ids").Ints(","),
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
@@ -149,7 +150,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
// GitLab.
if name == "gitlab" {
ss.socialMap["gitlab"] = &SocialGitlab{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
}
@@ -158,7 +159,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
// Google.
if name == "google" {
ss.socialMap["google"] = &SocialGoogle{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
hostedDomain: info.HostedDomain,
apiUrl: info.ApiUrl,
}
@@ -167,7 +168,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
// AzureAD.
if name == "azuread" {
ss.socialMap["azuread"] = &SocialAzureAD{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false),
}
@@ -176,7 +177,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
// Okta
if name == "okta" {
ss.socialMap["okta"] = &SocialOkta{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
}
@@ -185,7 +186,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
// Generic - Uses the same scheme as GitHub.
if name == "generic_oauth" {
ss.socialMap["generic_oauth"] = &SocialGenericOAuth{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamsUrl: info.TeamsUrl,
emailAttributeName: info.EmailAttributeName,
@@ -214,8 +215,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
}
ss.socialMap[grafanaCom] = &SocialGrafanaCom{
SocialBase: newSocialBase(name, &config, info,
cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
url: cfg.GrafanaComURL,
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
}
@@ -262,6 +262,7 @@ type SocialBase struct {
roleAttributeStrict bool
autoAssignOrgRole string
skipOrgRoleSync bool
features featuremgmt.FeatureManager
}
type Error struct {
@@ -296,6 +297,7 @@ func newSocialBase(name string,
info *OAuthInfo,
autoAssignOrgRole string,
skipOrgRoleSync bool,
features featuremgmt.FeatureManager,
) *SocialBase {
logger := log.New("oauth." + name)
@@ -309,6 +311,7 @@ func newSocialBase(name string,
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
skipOrgRoleSync: skipOrgRoleSync,
features: features,
}
}

View File

@@ -833,7 +833,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *dbtest.Fake
tracer := tracing.InitializeTracerForTest()
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, userService, mockSQLStore)
authenticator := &logintest.AuthenticatorFake{ExpectedUser: &user.User{}}
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, apiKeyService, authenticator, userService, orgService, oauthTokenService, featuremgmt.WithFeatures(featuremgmt.FlagAccessTokenExpirationCheck))
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, apiKeyService, authenticator, userService, orgService, oauthTokenService, featuremgmt.WithFeatures(featuremgmt.FlagAccessTokenExpirationCheck), nil)
}
type fakeRenderService struct {

View File

@@ -127,7 +127,7 @@ func (s *Server) init() error {
}
login.ProvideService(s.HTTPServer.SQLStore, s.HTTPServer.Login, s.loginAttemptService, s.userService)
social.ProvideService(s.cfg)
social.ProvideService(s.cfg, s.HTTPServer.Features)
if err := s.roleRegistry.RegisterFixedRoles(s.context); err != nil {
return err

View File

@@ -26,6 +26,8 @@ type Service interface {
registry.ProvidesUsageStats
// GetUserPermissions returns user permissions with only action and scope fields set.
GetUserPermissions(ctx context.Context, user *user.SignedInUser, options Options) ([]Permission, error)
// ClearUserPermissionCache removes the permission cache entry for the given user
ClearUserPermissionCache(user *user.SignedInUser)
// DeleteUserPermissions removes all permissions user has in org and all permission to that user
// If orgID is set to 0 remove permissions from all orgs
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error

View File

@@ -147,6 +147,14 @@ func (s *Service) getCachedUserPermissions(ctx context.Context, user *user.Signe
return permissions, nil
}
func (s *Service) ClearUserPermissionCache(user *user.SignedInUser) {
key, err := permissionCacheKey(user)
if err != nil {
return
}
s.cache.Delete(key)
}
func (s *Service) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error {
return s.store.DeleteUserPermissions(ctx, orgID, userID)
}

View File

@@ -24,6 +24,8 @@ func (f FakeService) GetUserPermissions(ctx context.Context, user *user.SignedIn
return f.ExpectedPermissions, f.ExpectedErr
}
func (f FakeService) ClearUserPermissionCache(user *user.SignedInUser) {}
func (f FakeService) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
return f.ExpectedErr
}

View File

@@ -20,6 +20,7 @@ type fullAccessControl interface {
type Calls struct {
Evaluate []interface{}
GetUserPermissions []interface{}
ClearUserPermissionCache []interface{}
IsDisabled []interface{}
DeclareFixedRoles []interface{}
DeclarePluginRoles []interface{}
@@ -43,6 +44,7 @@ type Mock struct {
// Override functions
EvaluateFunc func(context.Context, *user.SignedInUser, accesscontrol.Evaluator) (bool, error)
GetUserPermissionsFunc func(context.Context, *user.SignedInUser, accesscontrol.Options) ([]accesscontrol.Permission, error)
ClearUserPermissionCacheFunc func(*user.SignedInUser)
IsDisabledFunc func() bool
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
DeclarePluginRolesFunc func(context.Context, string, string, []plugins.RoleRegistration) error
@@ -138,6 +140,14 @@ func (m *Mock) GetUserPermissions(ctx context.Context, user *user.SignedInUser,
return m.permissions, nil
}
func (m *Mock) ClearUserPermissionCache(user *user.SignedInUser) {
m.Calls.ClearUserPermissionCache = append(m.Calls.ClearUserPermissionCache, []interface{}{user})
// Use override if provided
if m.ClearUserPermissionCacheFunc != nil {
m.ClearUserPermissionCacheFunc(user)
}
}
// Middleware checks if service disabled or not to switch to fallback authorization.
// This mock return m.disabled unless an override is provided.
func (m *Mock) IsDisabled() bool {

View File

@@ -104,7 +104,7 @@ func getContextHandler(t *testing.T) *ContextHandler {
return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc,
renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator,
&userService, orgService, nil, nil)
&userService, orgService, nil, nil, nil)
}
type FakeGetSignUserStore struct {

View File

@@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -48,6 +49,11 @@ func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, jwtS
tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service,
apiKeyService apikey.Service, authenticator loginpkg.Authenticator, userService user.Service,
orgService org.Service, oauthTokenService oauthtoken.OAuthTokenService, features *featuremgmt.FeatureManager,
// before 9.3.0 the quota service used to depend on on the ActiveTokenService
// since 9.3.0 after the quota refactoring ActiveTokenService depends on the quota
// therefore it's added to avoid cycle dependencies
// since it's used only by the middleware for enforcing quota limits.
activeTokenService auth.ActiveTokenService,
) *ContextHandler {
return &ContextHandler{
Cfg: cfg,
@@ -443,20 +449,14 @@ func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, org
return false
}
getTime := h.GetTime
if getTime == nil {
getTime = time.Now
}
if h.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
// Check whether the logged in User has a token (whether the User used an OAuth provider to login)
oauthToken, exists, _ := h.oauthTokenService.HasOAuthEntry(ctx, queryResult)
if exists {
// Skip where the OAuthExpiry is default/zero/unset
if !oauthToken.OAuthExpiry.IsZero() && oauthToken.OAuthExpiry.Round(0).Add(-oauthtoken.ExpiryDelta).Before(getTime()) {
if h.hasAccessTokenExpired(oauthToken) {
reqContext.Logger.Info("access token expired", "userId", query.UserID, "expiry", fmt.Sprintf("%v", oauthToken.OAuthExpiry))
// If the User doesn't have a refresh_token or refreshing the token was unsuccessful then log out the User and Invalidate the OAuth tokens
// If the User doesn't have a refresh_token or refreshing the token was unsuccessful then log out the User and invalidate the OAuth tokens
if err = h.oauthTokenService.TryTokenRefresh(ctx, oauthToken); err != nil {
if !errors.Is(err, oauthtoken.ErrNoRefreshTokenFound) {
reqContext.Logger.Error("could not fetch a new access token", "userId", oauthToken.UserId, "error", err)
@@ -726,3 +726,16 @@ func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList {
}
return nil
}
func (h *ContextHandler) hasAccessTokenExpired(token *models.UserAuth) bool {
if token.OAuthExpiry.IsZero() {
return false
}
getTime := h.GetTime
if getTime == nil {
getTime = time.Now
}
return token.OAuthExpiry.Round(0).Add(-oauthtoken.ExpiryDelta).Before(getTime())
}

View File

@@ -270,8 +270,8 @@ var (
},
{
Name: "internationalization",
Description: "Enables work-in-progress internationalization",
State: FeatureStateAlpha,
Description: "Enables internationalization",
State: FeatureStateStable,
},
{
Name: "topnav",
@@ -300,6 +300,11 @@ var (
Description: "Show the flame graph",
State: FeatureStateAlpha,
},
{
Name: "cloudWatchCrossAccountQuerying",
Description: "Use cross-account querying in CloudWatch datasource",
State: FeatureStateAlpha,
},
{
Name: "redshiftAsyncQueryDataSupport",
Description: "Enable async query data support for Redshift",

View File

@@ -196,7 +196,7 @@ const (
FlagDataConnectionsConsole = "dataConnectionsConsole"
// FlagInternationalization
// Enables work-in-progress internationalization
// Enables internationalization
FlagInternationalization = "internationalization"
// FlagTopnav
@@ -219,6 +219,10 @@ const (
// Show the flame graph
FlagFlameGraph = "flameGraph"
// FlagCloudWatchCrossAccountQuerying
// Use cross-account querying in CloudWatch datasource
FlagCloudWatchCrossAccountQuerying = "cloudWatchCrossAccountQuerying"
// FlagRedshiftAsyncQueryDataSupport
// Enable async query data support for Redshift
FlagRedshiftAsyncQueryDataSupport = "redshiftAsyncQueryDataSupport"

View File

@@ -36,10 +36,14 @@ const (
)
const (
NavIDRoot = "root"
NavIDDashboards = "dashboards"
NavIDDashboardsBrowse = "dashboards/browse"
NavIDCfg = "cfg" // NavIDCfg is the id for org configuration navigation node
NavIDAdmin = "admin"
NavIDAdminGeneral = "admin/general"
NavIDAdminPlugins = "admin/plugins"
NavIDAdminAccess = "admin/access"
NavIDAlertsAndIncidents = "alerts-and-incidents"
NavIDAlerting = "alerting"
NavIDMonitoring = "monitoring"
@@ -110,22 +114,7 @@ func (root *NavTreeRoot) RemoveEmptySectionsAndApplyNewInformationArchitecture(t
}
if topNavEnabled {
orgAdminNode := root.FindById(NavIDCfg)
if orgAdminNode != nil {
orgAdminNode.Url = "/admin"
orgAdminNode.Text = "Administration"
}
if serverAdminNode := root.FindById(NavIDAdmin); serverAdminNode != nil {
serverAdminNode.Url = "/admin/server"
serverAdminNode.SortWeight = 0
if orgAdminNode != nil {
orgAdminNode.Children = append(orgAdminNode.Children, serverAdminNode)
root.RemoveSection(serverAdminNode)
}
}
ApplyAdminIA(root)
// Move reports into dashboards
if reports := root.FindById(NavIDReporting); reports != nil {
@@ -151,6 +140,10 @@ func (root *NavTreeRoot) RemoveEmptySectionsAndApplyNewInformationArchitecture(t
node.Url = node.Children[0].Url
}
}
if len(root.Children) < 1 {
root.Children = make([]*NavLink, 0)
}
}
func (root *NavTreeRoot) Sort() {
@@ -180,6 +173,99 @@ func Sort(nodes []*NavLink) {
}
}
func ApplyAdminIA(root *NavTreeRoot) {
orgAdminNode := root.FindById(NavIDCfg)
if orgAdminNode != nil {
orgAdminNode.Url = "/admin"
orgAdminNode.Text = "Administration"
generalNodeLinks := []*NavLink{}
pluginsNodeLinks := []*NavLink{}
accessNodeLinks := []*NavLink{}
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("upgrading"))
if orgSettings := root.FindById("org-settings"); orgSettings != nil {
orgSettings.Text = "Default preferences"
generalNodeLinks = append(generalNodeLinks, orgSettings)
}
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("global-orgs"))
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("server-settings"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("plugins"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("datasources"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("correlations"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("plugin-page-grafana-cloud-link-app"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("recordedQueries")) // enterprise only
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("users"))
if globalUsers := root.FindById("global-users"); globalUsers != nil {
globalUsers.Text = "Users (All orgs)"
accessNodeLinks = append(accessNodeLinks, globalUsers)
}
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("teams"))
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("serviceaccounts"))
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("apikeys"))
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("standalone-plugin-page-/a/grafana-auth-app")) // Cloud Access Policies
generalNode := &NavLink{
Text: "General",
Id: NavIDAdminGeneral,
Url: "/admin/general",
Icon: "shield",
Children: generalNodeLinks,
}
pluginsNode := &NavLink{
Text: "Plugins and data",
Id: NavIDAdminPlugins,
Url: "/admin/plugins",
Icon: "shield",
Children: pluginsNodeLinks,
}
accessNode := &NavLink{
Text: "Users and access",
Id: NavIDAdminAccess,
Url: "/admin/access",
Icon: "shield",
Children: accessNodeLinks,
}
adminNodeLinks := []*NavLink{}
if len(generalNode.Children) > 0 {
adminNodeLinks = append(adminNodeLinks, generalNode)
}
if len(pluginsNode.Children) > 0 {
adminNodeLinks = append(adminNodeLinks, pluginsNode)
}
if len(accessNode.Children) > 0 {
adminNodeLinks = append(adminNodeLinks, accessNode)
}
if len(adminNodeLinks) > 0 {
orgAdminNode.Children = adminNodeLinks
} else {
root.RemoveSection(orgAdminNode)
}
}
if serverAdminNode := root.FindById(NavIDAdmin); serverAdminNode != nil {
root.RemoveSection(serverAdminNode)
}
}
func AppendIfNotNil(children []*NavLink, newChild *NavLink) []*NavLink {
if newChild != nil {
return append(children, newChild)
}
return children
}
func FindById(nodes []*NavLink, id string) *NavLink {
for _, child := range nodes {
if child.Id == id {

View File

@@ -33,18 +33,20 @@ func TestNavTreeRoot(t *testing.T) {
require.Equal(t, 2, len(treeRoot.Children))
})
t.Run("Should move admin section into cfg and rename when topnav is enabled", func(t *testing.T) {
t.Run("Should create 3 new sections in the Admin node when topnav is enabled", func(t *testing.T) {
treeRoot := NavTreeRoot{
Children: []*NavLink{
{Id: NavIDCfg},
{Id: NavIDAdmin, Children: []*NavLink{{Id: "child"}}},
{Id: NavIDAdmin, Children: []*NavLink{{Id: "upgrading"}, {Id: "plugins"}, {Id: "teams"}}},
},
}
treeRoot.RemoveEmptySectionsAndApplyNewInformationArchitecture(true)
require.Equal(t, "Administration", treeRoot.Children[0].Text)
require.Equal(t, NavIDAdmin, treeRoot.Children[0].Children[0].Id)
require.Equal(t, NavIDAdminGeneral, treeRoot.Children[0].Children[0].Id)
require.Equal(t, NavIDAdminPlugins, treeRoot.Children[0].Children[1].Id)
require.Equal(t, NavIDAdminAccess, treeRoot.Children[0].Children[2].Id)
})
t.Run("Should move reports into Dashboards", func(t *testing.T) {

View File

@@ -166,16 +166,8 @@ func (s *ServiceImpl) getServerAdminNode(c *models.ReqContext) *navtree.NavLink
Children: adminNavLinks,
}
if s.cfg.IsFeatureToggleEnabled(featuremgmt.FlagTopnav) {
adminNode.SubTitle = "Manage server-wide settings and access to resources such as organizations, users, and licenses"
}
if len(adminNavLinks) > 0 {
if s.cfg.IsFeatureToggleEnabled(featuremgmt.FlagTopnav) {
adminNode.Url = s.cfg.AppSubURL + "/admin/server"
} else {
adminNode.Url = adminNavLinks[0].Url
}
adminNode.Url = adminNavLinks[0].Url
}
return adminNode

View File

@@ -169,6 +169,12 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo
}
appLink.Children = childrenWithoutDefault
s.addPluginToSection(c, treeRoot, plugin, appLink)
return nil
}
func (s *ServiceImpl) addPluginToSection(c *models.ReqContext, treeRoot *navtree.NavTreeRoot, plugin plugins.PluginDTO, appLink *navtree.NavLink) {
// Handle moving apps into specific navtree sections
alertingNode := treeRoot.FindById(navtree.NavIDAlerting)
sectionID := navtree.NavIDApps
@@ -182,7 +188,9 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo
}
}
if navNode := treeRoot.FindById(sectionID); navNode != nil {
if sectionID == navtree.NavIDRoot {
treeRoot.AddSection(appLink)
} else if navNode := treeRoot.FindById(sectionID); navNode != nil {
navNode.Children = append(navNode.Children, appLink)
} else {
switch sectionID {
@@ -226,8 +234,6 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo
s.log.Error("Plugin app nav id not found", "pluginId", plugin.ID, "navId", sectionID)
}
}
return nil
}
func (s *ServiceImpl) readNavigationSettings() {
@@ -236,8 +242,9 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDMonitoring, SortWeight: 2, Text: "Synthetics"},
"grafana-oncall-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "OnCall"},
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incident"},
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3},
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"},
"grafana-cloud-link-app": {SectionID: navtree.NavIDCfg},
"grafana-easystart-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightSavedItems + 1, Text: "Connections"},
}
s.navigationAppPathConfig = map[string]NavigationAppConfig{

View File

@@ -145,6 +145,30 @@ func TestAddAppLinks(t *testing.T) {
require.Equal(t, "Page2", app1Node.Children[0].Text)
})
// This can be done by using `[navigation.app_sections]` in the INI config
t.Run("Should move apps that have root nav id configured to the root", func(t *testing.T) {
service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav)
service.navigationAppConfig = map[string]NavigationAppConfig{
"test-app1": {SectionID: navtree.NavIDRoot},
}
treeRoot := navtree.NavTreeRoot{}
err := service.addAppLinks(&treeRoot, reqCtx)
require.NoError(t, err)
// Check if the plugin gets moved to the root
require.Len(t, treeRoot.Children, 2)
require.Equal(t, "plugin-page-test-app1", treeRoot.Children[0].Id)
// Check if it is not under the "Apps" section anymore
appsNode := treeRoot.FindById(navtree.NavIDApps)
require.NotNil(t, appsNode)
require.Len(t, appsNode.Children, 2)
require.Equal(t, "plugin-page-test-app2", appsNode.Children[0].Id)
require.Equal(t, "plugin-page-test-app3", appsNode.Children[1].Id)
})
// This can be done by using `[navigation.app_sections]` in the INI config
t.Run("Should move apps that have specific nav id configured to correct section", func(t *testing.T) {
service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav)

View File

@@ -73,9 +73,7 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
hasAccess := ac.HasAccess(s.accessControl, c)
treeRoot := &navtree.NavTreeRoot{}
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
treeRoot.AddSection(s.getHomeNode(c, prefs))
}
treeRoot.AddSection(s.getHomeNode(c, prefs))
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsRead)) {
starredItemsLinks, err := s.buildStarredItemsNavLinks(c)
@@ -224,7 +222,7 @@ func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference)
}
}
return &navtree.NavLink{
homeNode := &navtree.NavLink{
Text: "Home",
Id: "home",
Url: homeUrl,
@@ -232,6 +230,10 @@ func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference)
Section: navtree.NavSectionCore,
SortWeight: navtree.WeightHome,
}
if !s.features.IsEnabled(featuremgmt.FlagTopnav) {
homeNode.HideFromMenu = true
}
return homeNode
}
func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqContext) {

View File

@@ -104,10 +104,6 @@ type ProvisionedAlertRule struct {
}
func (a *ProvisionedAlertRule) UpstreamModel() (models.AlertRule, error) {
forDur, err := time.ParseDuration(a.For.String())
if err != nil {
return models.AlertRule{}, err
}
return models.AlertRule{
ID: a.ID,
UID: a.UID,
@@ -120,7 +116,7 @@ func (a *ProvisionedAlertRule) UpstreamModel() (models.AlertRule, error) {
Updated: a.Updated,
NoDataState: a.NoDataState,
ExecErrState: a.ExecErrState,
For: forDur,
For: time.Duration(a.For),
Annotations: a.Annotations,
Labels: a.Labels,
}, nil

View File

@@ -20,7 +20,7 @@ type Preference struct {
Version int `db:"version"`
HomeDashboardID int64 `xorm:"home_dashboard_id" db:"home_dashboard_id"`
Timezone string `db:"timezone"`
WeekStart string `db:"week_start"`
WeekStart *string `db:"week_start"`
Theme string `db:"theme"`
Created time.Time `db:"created"`
Updated time.Time `db:"updated"`

View File

@@ -54,7 +54,7 @@ func (s *Service) GetWithDefaults(ctx context.Context, query *pref.GetPreference
if p.Timezone != "" {
res.Timezone = p.Timezone
}
if p.WeekStart != "" {
if p.WeekStart != nil && *p.WeekStart != "" {
res.WeekStart = p.WeekStart
}
if p.HomeDashboardID != 0 {
@@ -108,7 +108,7 @@ func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) err
TeamID: cmd.TeamID,
HomeDashboardID: cmd.HomeDashboardID,
Timezone: cmd.Timezone,
WeekStart: cmd.WeekStart,
WeekStart: &cmd.WeekStart,
Theme: cmd.Theme,
Created: time.Now(),
Updated: time.Now(),
@@ -125,7 +125,7 @@ func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) err
}
preference.Timezone = cmd.Timezone
preference.WeekStart = cmd.WeekStart
preference.WeekStart = &cmd.WeekStart
preference.Theme = cmd.Theme
preference.Updated = time.Now()
preference.Version += 1
@@ -200,7 +200,7 @@ func (s *Service) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) e
}
if cmd.WeekStart != nil {
preference.WeekStart = *cmd.WeekStart
preference.WeekStart = cmd.WeekStart
}
if cmd.Theme != nil {
@@ -232,7 +232,7 @@ func (s *Service) GetDefaults() *pref.Preference {
defaults := &pref.Preference{
Theme: s.cfg.DefaultTheme,
Timezone: s.cfg.DateFormats.DefaultTimezone,
WeekStart: s.cfg.DateFormats.DefaultWeekStart,
WeekStart: &s.cfg.DateFormats.DefaultWeekStart,
HomeDashboardID: 0,
JSONData: &pref.PreferenceJSONData{},
}

View File

@@ -36,10 +36,12 @@ func TestGetDefaults(t *testing.T) {
prefService.cfg.DefaultLocale = "en-US"
prefService.cfg.DefaultTheme = "light"
prefService.cfg.DateFormats.DefaultTimezone = "UTC"
weekStart := ""
t.Run("GetDefaults", func(t *testing.T) {
preference := prefService.GetDefaults()
expected := &pref.Preference{
WeekStart: &weekStart,
Theme: "light",
Timezone: "UTC",
HomeDashboardID: 0,
@@ -55,6 +57,7 @@ func TestGetDefaults(t *testing.T) {
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
WeekStart: &weekStart,
Theme: "light",
Timezone: "UTC",
HomeDashboardID: 0,
@@ -72,6 +75,7 @@ func TestGetDefaultsWithI18nFeatureFlag(t *testing.T) {
cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(featuremgmt.FlagInternationalization),
}
weekStart := ""
prefService.cfg.DefaultLocale = "en-US"
prefService.cfg.DefaultTheme = "light"
prefService.cfg.DateFormats.DefaultTimezone = "UTC"
@@ -79,6 +83,7 @@ func TestGetDefaultsWithI18nFeatureFlag(t *testing.T) {
t.Run("GetDefaults", func(t *testing.T) {
preference := prefService.GetDefaults()
expected := &pref.Preference{
WeekStart: &weekStart,
Theme: "light",
Timezone: "UTC",
HomeDashboardID: 0,
@@ -100,13 +105,15 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
}
prefService.cfg.DefaultLocale = "en-US"
weekStartOne := "1"
weekStartTwo := "2"
insertPrefs(t, prefService.store,
pref.Preference{
OrgID: 1,
HomeDashboardID: 1,
Theme: "dark",
Timezone: "UTC",
WeekStart: "1",
WeekStart: &weekStartOne,
JSONData: &pref.PreferenceJSONData{
Locale: "en-GB",
},
@@ -117,7 +124,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
HomeDashboardID: 4,
Theme: "light",
Timezone: "browser",
WeekStart: "2",
WeekStart: &weekStartTwo,
JSONData: &pref.PreferenceJSONData{
Locale: "en-AU",
},
@@ -131,7 +138,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
expected := &pref.Preference{
Theme: "light",
Timezone: "browser",
WeekStart: "2",
WeekStart: &weekStartTwo,
HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{
Locale: "en-AU",
@@ -150,7 +157,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
expected := &pref.Preference{
Theme: "dark",
Timezone: "UTC",
WeekStart: "1",
WeekStart: &weekStartOne,
HomeDashboardID: 1,
JSONData: &pref.PreferenceJSONData{
Locale: "en-GB",
@@ -163,6 +170,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
}
func TestGetDefaults_JSONData(t *testing.T) {
weekStart := ""
queryPreference := pref.QueryHistoryPreference{
HomeTab: "hometab",
}
@@ -235,7 +243,8 @@ func TestGetDefaults_JSONData(t *testing.T) {
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
require.Equal(t, &pref.Preference{
JSONData: &userPreferencesJsonData,
WeekStart: &weekStart,
JSONData: &userPreferencesJsonData,
}, preference)
})
@@ -262,6 +271,7 @@ func TestGetDefaults_JSONData(t *testing.T) {
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
require.Equal(t, &pref.Preference{
WeekStart: &weekStart,
JSONData: &pref.PreferenceJSONData{
Locale: "en-GB",
Navbar: userNavbarPreferences,
@@ -300,12 +310,15 @@ func TestGetDefaults_JSONData(t *testing.T) {
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
require.Equal(t, &pref.Preference{
JSONData: &team2PreferencesJsonData,
JSONData: &team2PreferencesJsonData,
WeekStart: &weekStart,
}, preference)
})
}
func TestGetWithDefaults_teams(t *testing.T) {
weekStartOne := "1"
weekStartTwo := "2"
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
@@ -317,7 +330,7 @@ func TestGetWithDefaults_teams(t *testing.T) {
HomeDashboardID: 1,
Theme: "light",
Timezone: "browser",
WeekStart: "1",
WeekStart: &weekStartOne,
},
pref.Preference{
OrgID: 1,
@@ -325,7 +338,7 @@ func TestGetWithDefaults_teams(t *testing.T) {
HomeDashboardID: 3,
Theme: "light",
Timezone: "browser",
WeekStart: "2",
WeekStart: &weekStartTwo,
},
pref.Preference{
OrgID: 1,
@@ -333,7 +346,7 @@ func TestGetWithDefaults_teams(t *testing.T) {
HomeDashboardID: 4,
Theme: "light",
Timezone: "browser",
WeekStart: "2",
WeekStart: &weekStartTwo,
},
)
@@ -343,7 +356,7 @@ func TestGetWithDefaults_teams(t *testing.T) {
expected := &pref.Preference{
Theme: "light",
Timezone: "browser",
WeekStart: "2",
WeekStart: &weekStartTwo,
HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{},
}
@@ -399,7 +412,7 @@ func TestSave(t *testing.T) {
assert.Equal(t, "dark", stored.Theme)
assert.Equal(t, "browser", stored.Timezone)
assert.EqualValues(t, 5, stored.HomeDashboardID)
assert.Equal(t, "1", stored.WeekStart)
assert.Equal(t, "1", *stored.WeekStart)
assert.EqualValues(t, 0, stored.Version)
})
@@ -421,7 +434,7 @@ func TestSave(t *testing.T) {
assert.Empty(t, stored.Theme)
assert.Equal(t, "UTC", stored.Timezone)
assert.Zero(t, stored.HomeDashboardID)
assert.Equal(t, "1", stored.WeekStart)
assert.Equal(t, "1", *stored.WeekStart)
assert.EqualValues(t, 1, stored.Version)
})
@@ -440,7 +453,7 @@ func TestSave(t *testing.T) {
assert.Equal(t, themeValue, stored.Theme)
assert.Equal(t, "UTC", stored.Timezone)
assert.Zero(t, stored.HomeDashboardID)
assert.Equal(t, "1", stored.WeekStart)
assert.Equal(t, "1", *stored.WeekStart)
assert.EqualValues(t, 2, stored.Version)
})
}

View File

@@ -17,7 +17,7 @@ type getStore func(db.DB) store
func testIntegrationPreferencesDataAccess(t *testing.T, fn getStore) {
t.Helper()
weekStartOne := "1"
ss := db.InitTestDB(t)
prefStore := fn(ss)
orgNavbarPreferences := pref.NavbarPreference{
@@ -123,7 +123,7 @@ func testIntegrationPreferencesDataAccess(t *testing.T, fn getStore) {
Theme: "dark",
Timezone: "browser",
HomeDashboardID: 5,
WeekStart: "1",
WeekStart: &weekStartOne,
JSONData: &pref.PreferenceJSONData{Navbar: orgNavbarPreferences},
Created: time.Now(),
Updated: time.Now(),
@@ -135,7 +135,7 @@ func testIntegrationPreferencesDataAccess(t *testing.T, fn getStore) {
Theme: "dark",
HomeDashboardID: 5,
Timezone: "browser",
WeekStart: "1",
WeekStart: &weekStartOne,
Created: time.Now(),
Updated: time.Now(),
JSONData: &pref.PreferenceJSONData{},
@@ -149,7 +149,7 @@ func testIntegrationPreferencesDataAccess(t *testing.T, fn getStore) {
Version: prefs[0].Version,
HomeDashboardID: 5,
Timezone: "browser",
WeekStart: "1",
WeekStart: &weekStartOne,
Theme: "dark",
JSONData: prefs[0].JSONData,
Created: prefs[0].Created,

View File

@@ -1,26 +1,54 @@
package alerting
import (
"encoding/json"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
type NotificiationPolicyV1 struct {
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
Policy definitions.Route `json:",inline" yaml:",inline"`
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
// We use JSONValue here, as we want to have interpolation the values.
Policy values.JSONValue `json:"-" yaml:"-"`
}
func (v1 *NotificiationPolicyV1) mapToModel() NotificiationPolicy {
func (v1 *NotificiationPolicyV1) UnmarshalYAML(unmarshal func(interface{}) error) error {
err := v1.Policy.UnmarshalYAML(unmarshal)
if err != nil {
return err
}
// As we also want to unmarshal the orgId and any other field that might be
// added in the future we create an alias type that prevents recursion
// and just uses the default marshler.
type plain NotificiationPolicyV1
return unmarshal((*plain)(v1))
}
func (v1 *NotificiationPolicyV1) mapToModel() (NotificiationPolicy, error) {
orgID := v1.OrgID.Value()
if orgID < 1 {
orgID = 1
}
// we don't need any further validation here as it's done by
// the notification policy service
var route definitions.Route
// We need the string json representation, so we marshal the policy back
// as a string and interpolate it at the same time.
data, err := json.Marshal(v1.Policy.Value())
if err != nil {
return NotificiationPolicy{}, err
}
// Now we can take the interpolated string json represtenation of the policy
// and unmarshal it in the concrete type.
err = json.Unmarshal(data, &route)
if err != nil {
return NotificiationPolicy{}, err
}
// We don't need any further validation here as it's done by
// the notification policy service.
return NotificiationPolicy{
OrgID: orgID,
Policy: v1.Policy,
}
Policy: route,
}, nil
}
type NotificiationPolicy struct {

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