From 04c037b2cb7f889c151999e004b60dad175618b3 Mon Sep 17 00:00:00 2001
From: Ashley Harrison
Date: Wed, 6 Apr 2022 16:53:59 +0100
Subject: [PATCH 01/55] Navigation: implement full-width mobile menu (#47383)
* navigation: implement proper mobile menu
* Update public/app/core/components/NavBar/Next/NavBarMenu.tsx
Co-authored-by: Jack Westbrook
* animation feedback
Co-authored-by: Jack Westbrook
---
.../components/NavBar/Next/NavBarMenu.tsx | 41 +++++++++++++++----
1 file changed, 34 insertions(+), 7 deletions(-)
diff --git a/public/app/core/components/NavBar/Next/NavBarMenu.tsx b/public/app/core/components/NavBar/Next/NavBarMenu.tsx
index 751de398665..b363797036f 100644
--- a/public/app/core/components/NavBar/Next/NavBarMenu.tsx
+++ b/public/app/core/components/NavBar/Next/NavBarMenu.tsx
@@ -1,7 +1,7 @@
import React, { useRef } from 'react';
import CSSTransition from 'react-transition-group/CSSTransition';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
-import { CollapsableSection, CustomScrollbar, Icon, IconName, useStyles2, useTheme2 } from '@grafana/ui';
+import { CollapsableSection, CustomScrollbar, Icon, IconButton, IconName, useStyles2, useTheme2 } from '@grafana/ui';
import { FocusScope } from '@react-aria/focus';
import { useDialog } from '@react-aria/dialog';
import { OverlayContainer, useOverlay } from '@react-aria/overlays';
@@ -48,6 +48,16 @@ export function NavBarMenu({ activeItem, isOpen, navItems, onClose, setMenuAnima
timeout={ANIMATION_DURATION}
>
+
+
+
+
@@ -82,8 +92,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
zIndex: theme.zIndex.modalBackdrop,
}),
container: css({
- bottom: 0,
display: 'flex',
+ bottom: 0,
flexDirection: 'column',
left: 0,
whiteSpace: 'nowrap',
@@ -104,6 +114,15 @@ const getStyles = (theme: GrafanaTheme2) => ({
flexDirection: 'column',
overflow: 'auto',
}),
+ mobileHeader: css({
+ borderBottom: `1px solid ${theme.colors.border.weak}`,
+ display: 'flex',
+ justifyContent: 'space-between',
+ padding: theme.spacing(1, 2, 2),
+ [theme.breakpoints.up('md')]: {
+ display: 'none',
+ },
+ }),
itemList: css({
display: 'grid',
gridAutoRows: `minmax(${theme.spacing(6)}, auto)`,
@@ -118,14 +137,16 @@ const getStyles = (theme: GrafanaTheme2) => ({
const getAnimStyles = (theme: GrafanaTheme2, animationDuration: number) => {
const commonTransition = {
- transitionProperty: 'width, background-color, opacity',
transitionDuration: `${animationDuration}ms`,
transitionTimingFunction: theme.transitions.easing.easeInOut,
+ [theme.breakpoints.down('md')]: {
+ overflow: 'hidden',
+ },
};
const overlayTransition = {
...commonTransition,
- transitionProperty: 'width, background-color, box-shadow',
+ transitionProperty: 'background-color, box-shadow, width',
};
const backdropTransition = {
@@ -136,13 +157,19 @@ const getAnimStyles = (theme: GrafanaTheme2, animationDuration: number) => {
const overlayOpen = {
backgroundColor: theme.colors.background.canvas,
boxShadow: theme.shadows.z3,
- width: '300px',
+ width: '100%',
+ [theme.breakpoints.up('md')]: {
+ width: '300px',
+ },
};
const overlayClosed = {
- backgroundColor: theme.colors.background.primary,
boxShadow: 'none',
- width: theme.spacing(7),
+ width: 0,
+ [theme.breakpoints.up('md')]: {
+ backgroundColor: theme.colors.background.primary,
+ width: theme.spacing(7),
+ },
};
const backdropOpen = {
From 460b8e85d79dbf8951dd72c63fe271f07a1abe4a Mon Sep 17 00:00:00 2001
From: Martin
Date: Wed, 6 Apr 2022 18:10:58 +0200
Subject: [PATCH 02/55] Build: bump gonum library version to fix #46759
(#46997)
---
go.mod | 4 ++--
go.sum | 26 +++++++++++++++++++++++---
2 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/go.mod b/go.mod
index 0c8971576ff..d0646277b24 100644
--- a/go.mod
+++ b/go.mod
@@ -108,8 +108,8 @@ require (
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
- golang.org/x/tools v0.1.5
- gonum.org/v1/gonum v0.9.3
+ golang.org/x/tools v0.1.9
+ gonum.org/v1/gonum v0.11.0
google.golang.org/api v0.60.0
google.golang.org/grpc v1.42.0
google.golang.org/protobuf v1.27.1
diff --git a/go.sum b/go.sum
index 9d96f8e6df1..707fb9c42b0 100644
--- a/go.sum
+++ b/go.sum
@@ -76,6 +76,7 @@ cuelang.org/go v0.4.0/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
+git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/Azure/azure-amqp-common-go/v3 v3.0.0/go.mod h1:SY08giD/XbhTz07tJdpw1SoxQXHPN30+DI3Z04SYqyg=
github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI=
@@ -286,7 +287,10 @@ github.com/aerospike/aerospike-client-go v1.27.0/go.mod h1:zj8LBEnWBDOVEIJt8LvaR
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
+github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
+github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -440,6 +444,7 @@ github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+Wji
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bonitoo-io/go-sql-bigquery v0.3.4-1.4.0/go.mod h1:J4Y6YJm0qTWB9aFziB7cPeSyc6dOZFyJdteSeybVpXQ=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
@@ -892,6 +897,7 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -905,6 +911,7 @@ github.com/go-kit/kit v0.11.0/go.mod h1:73/6Ixaufkvb5Osvkls8C79vuQ49Ba1rUEUYNSf+
github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
+github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -1044,6 +1051,8 @@ github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9G
github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0=
github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts=
github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0=
+github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
+github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@@ -2068,6 +2077,7 @@ github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -2264,6 +2274,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
@@ -2546,6 +2557,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20180630135845-46796da1b0b4/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
@@ -2754,6 +2766,10 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -2782,6 +2798,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -3206,8 +3223,9 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
+golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -3220,13 +3238,14 @@ gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJ
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
-gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
+gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
+gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
-gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
+gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
@@ -3520,6 +3539,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
honnef.co/go/tools v0.2.0/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
inet.af/netaddr v0.0.0-20210707202901-70468d781e6c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
From 49505b9a3bc284b1d18bcf5ea1ddf4aa19c6b547 Mon Sep 17 00:00:00 2001
From: Nathan Rodman
Date: Wed, 6 Apr 2022 09:24:33 -0700
Subject: [PATCH 03/55] Alerting: fgac for notification policies and contact
points (#46939)
* add FGAC actions for silences table
* redirect users without permissions
* add permissions checks to routes
* add fgac to notifications and contact points
* fgac for notification policies
* fix mute timing authorization
* use consistent naming for checking grafana alertmanager
* tests for fgac in contact points and notification policies
* bump up timeout on rule editor test
* use new permissions util
* break out route evaluation into util
* Remove test timeout
* Change permissions for the alert-notifiers endpoint
* Use signed in handler for alert-notifiers when unified alerting enabled
Co-authored-by: Konrad Lalik
---
pkg/api/api.go | 10 ++-
public/app/features/alerting/routes.tsx | 59 ++++++++++---
.../alerting/unified/AmRoutes.test.tsx | 19 ++++
.../features/alerting/unified/AmRoutes.tsx | 1 +
.../alerting/unified/Receivers.test.tsx | 33 ++++++-
.../features/alerting/unified/Receivers.tsx | 5 +-
.../unified/components/EmptyAreaWithCTA.tsx | 21 +++--
.../components/alert-groups/AlertDetails.tsx | 18 +---
.../components/amroutes/AmRootRoute.tsx | 12 ++-
.../amroutes/AmRoutesExpandedRead.tsx | 34 +++++---
.../components/amroutes/AmRoutesTable.tsx | 12 ++-
.../components/amroutes/AmSpecificRouting.tsx | 22 +++--
.../components/amroutes/MuteTimingsTable.tsx | 54 ++++++++----
.../receivers/ReceiversAndTemplatesView.tsx | 22 +++--
.../components/receivers/ReceiversTable.tsx | 86 +++++++++++--------
.../components/receivers/TemplatesTable.tsx | 41 ++++++---
.../components/silences/SilencesTable.tsx | 23 ++---
.../alerting/unified/utils/access-control.ts | 78 +++++++++++++++++
18 files changed, 399 insertions(+), 151 deletions(-)
create mode 100644 public/app/features/alerting/unified/utils/access-control.ts
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 16af1f809c6..12c7dbd4a8c 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
+ "github.com/grafana/grafana/pkg/web"
)
var plog = log.New("api")
@@ -417,7 +418,14 @@ func (hs *HTTPServer) registerRoutes() {
alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard))
})
- apiRoute.Get("/alert-notifiers", reqEditorRole, routing.Wrap(
+ var notifiersAuthHandler web.Handler
+ if hs.Cfg.UnifiedAlerting.IsEnabled() {
+ notifiersAuthHandler = reqSignedIn
+ } else {
+ notifiersAuthHandler = reqEditorRole
+ }
+
+ apiRoute.Get("/alert-notifiers", notifiersAuthHandler, routing.Wrap(
hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())),
)
diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx
index b7a90c80d4a..1e64912cc57 100644
--- a/public/app/features/alerting/routes.tsx
+++ b/public/app/features/alerting/routes.tsx
@@ -6,6 +6,7 @@ import { RouteDescriptor } from 'app/core/navigation/types';
import { uniq } from 'lodash';
import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types';
+import { evaluateAccess } from './unified/utils/access-control';
const commonRoutes: RouteDescriptor[] = [
{
@@ -95,84 +96,118 @@ const unifiedRoutes: RouteDescriptor[] = [
},
{
path: '/alerting/routes',
- roles: () => ['Admin', 'Editor'],
+ roles: () =>
+ contextSrv.evaluatePermission(config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : () => [], [
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsExternalRead,
+ ]),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
),
},
{
path: '/alerting/routes/mute-timing/new',
- roles: () => ['Admin', 'Editor'],
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings')
),
},
{
path: '/alerting/routes/mute-timing/edit',
- roles: () => ['Admin', 'Editor'],
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings')
),
},
{
path: '/alerting/silences',
- roles: () => contextSrv.evaluatePermission(() => [], [AccessControlAction.AlertingInstanceRead]),
+ roles: evaluateAccess([AccessControlAction.AlertingInstanceRead], ['Editor', 'Admin']),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
),
},
{
path: '/alerting/silence/new',
- roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceCreate]),
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstancesExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
),
},
{
path: '/alerting/silence/:id/edit',
- roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceUpdate]),
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingInstanceUpdate, AccessControlAction.AlertingInstancesExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
),
},
{
path: '/alerting/notifications',
- roles: config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : undefined,
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
),
},
{
path: '/alerting/notifications/templates/new',
- roles: () => ['Editor', 'Admin'],
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
),
},
{
path: '/alerting/notifications/templates/:id/edit',
- roles: () => ['Editor', 'Admin'],
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
),
},
{
path: '/alerting/notifications/receivers/new',
- roles: () => ['Editor', 'Admin'],
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
),
},
{
path: '/alerting/notifications/receivers/:id/edit',
- roles: () => ['Editor', 'Admin'],
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
),
},
{
path: '/alerting/notifications/global-config',
- roles: () => ['Admin', 'Editor'],
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
+ ['Editor', 'Admin']
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
),
diff --git a/public/app/features/alerting/unified/AmRoutes.test.tsx b/public/app/features/alerting/unified/AmRoutes.test.tsx
index 01f339bbf66..85a561165ba 100644
--- a/public/app/features/alerting/unified/AmRoutes.test.tsx
+++ b/public/app/features/alerting/unified/AmRoutes.test.tsx
@@ -20,9 +20,12 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import userEvent from '@testing-library/user-event';
import { selectOptionInTest } from '@grafana/ui';
import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
+import { contextSrv } from 'app/core/services/context_srv';
+import { AccessControlAction } from 'app/types';
jest.mock('./api/alertmanager');
jest.mock('./utils/config');
+jest.mock('app/core/services/context_srv');
const mocks = {
getAllDataSourcesMock: jest.mocked(getAllDataSources),
@@ -32,6 +35,7 @@ const mocks = {
updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig),
fetchStatus: jest.mocked(fetchStatus),
},
+ contextSrv: jest.mocked(contextSrv),
};
const renderAmRoutes = (alertManagerSourceName?: string) => {
@@ -177,6 +181,9 @@ describe('AmRoutes', () => {
beforeEach(() => {
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
+ mocks.contextSrv.hasAccess.mockImplementation(() => true);
+ mocks.contextSrv.hasPermission.mockImplementation(() => true);
+ mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
setDataSourceSrv(new MockDataSourceSrv(dataSources));
});
@@ -359,6 +366,18 @@ describe('AmRoutes', () => {
});
});
+ it('hides create and edit button if user does not have permission', () => {
+ mocks.contextSrv.hasAccess.mockImplementation((action) =>
+ [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsRead].includes(
+ action as AccessControlAction
+ )
+ );
+
+ renderAmRoutes();
+ expect(ui.newPolicyButton.query()).not.toBeInTheDocument();
+ expect(ui.editButton.query()).not.toBeInTheDocument();
+ });
+
it('Show error message if loading Alertmanager config fails', async () => {
mocks.api.fetchAlertManagerConfig.mockRejectedValue({
status: 500,
diff --git a/public/app/features/alerting/unified/AmRoutes.tsx b/public/app/features/alerting/unified/AmRoutes.tsx
index 43c09b1d4e2..8bcdaa7e184 100644
--- a/public/app/features/alerting/unified/AmRoutes.tsx
+++ b/public/app/features/alerting/unified/AmRoutes.tsx
@@ -122,6 +122,7 @@ const AmRoutes: FC = () => {
/>
{
@@ -125,8 +128,23 @@ describe('Receivers', () => {
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
setDataSourceSrv(new MockDataSourceSrv(dataSources));
- contextSrv.isEditor = true;
+ mocks.contextSrv.isEditor = true;
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
+
+ mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
+ mocks.contextSrv.hasPermission.mockImplementation((action) => {
+ const permissions = [
+ AccessControlAction.AlertingNotificationsRead,
+ AccessControlAction.AlertingNotificationsCreate,
+ AccessControlAction.AlertingNotificationsUpdate,
+ AccessControlAction.AlertingNotificationsDelete,
+ AccessControlAction.AlertingNotificationsExternalRead,
+ AccessControlAction.AlertingNotificationsExternalWrite,
+ ];
+ return permissions.includes(action as AccessControlAction);
+ });
+
+ mocks.contextSrv.hasAccess.mockImplementation(() => true);
});
it('Template and receiver tables are rendered, alertmanager can be selected', async () => {
@@ -295,6 +313,19 @@ describe('Receivers', () => {
});
});
+ it('Hides create contact point button for users without permission', () => {
+ mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
+ mocks.api.updateConfig.mockResolvedValue();
+ mocks.contextSrv.hasAccess.mockImplementation((action) =>
+ [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead].some(
+ (a) => a === action
+ )
+ );
+ renderReceivers();
+
+ expect(ui.newContactPointButton.query()).not.toBeInTheDocument();
+ });
+
it('Cloud alertmanager receiver can be edited', async () => {
mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx
index 02bee4161d8..cf503117041 100644
--- a/public/app/features/alerting/unified/Receivers.tsx
+++ b/public/app/features/alerting/unified/Receivers.tsx
@@ -41,7 +41,10 @@ const Receivers: FC = () => {
}, [alertManagerSourceName, dispatch, shouldLoadConfig]);
useEffect(() => {
- if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && !(receiverTypes.result || receiverTypes.loading)) {
+ if (
+ alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
+ !(receiverTypes.result || receiverTypes.loading || receiverTypes.error)
+ ) {
dispatch(fetchGrafanaNotifiersAction());
}
}, [alertManagerSourceName, dispatch, receiverTypes]);
diff --git a/public/app/features/alerting/unified/components/EmptyAreaWithCTA.tsx b/public/app/features/alerting/unified/components/EmptyAreaWithCTA.tsx
index c507512ab07..a1c8d834104 100644
--- a/public/app/features/alerting/unified/components/EmptyAreaWithCTA.tsx
+++ b/public/app/features/alerting/unified/components/EmptyAreaWithCTA.tsx
@@ -13,6 +13,7 @@ export interface EmptyAreaWithCTAProps {
buttonIcon?: IconName;
buttonSize?: 'xs' | 'sm' | 'md' | 'lg';
buttonVariant?: ButtonVariant;
+ showButton?: boolean;
}
export const EmptyAreaWithCTA: FC = ({
@@ -23,6 +24,7 @@ export const EmptyAreaWithCTA: FC = ({
onButtonClick,
text,
href,
+ showButton = true,
}) => {
const styles = useStyles(getStyles);
@@ -37,15 +39,16 @@ export const EmptyAreaWithCTA: FC = ({
<>
{text}
- {href ? (
-
- {buttonLabel}
-
- ) : (
-
- {buttonLabel}
-
- )}
+ {showButton &&
+ (href ? (
+
+ {buttonLabel}
+
+ ) : (
+
+ {buttonLabel}
+
+ ))}
>
);
diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx
index e7867add2ba..53818045141 100644
--- a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx
+++ b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx
@@ -3,12 +3,11 @@ import { GrafanaTheme2 } from '@grafana/data';
import { LinkButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
-import { AccessControlAction } from 'app/types';
import React, { FC } from 'react';
-import { isGrafanaRulesSource } from '../../utils/datasource';
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
import { AnnotationDetailsField } from '../AnnotationDetailsField';
import { Authorize } from '../Authorize';
+import { getInstancesPermissions } from '../../utils/access-control';
interface AmNotificationsAlertDetailsProps {
alertManagerSourceName: string;
@@ -17,18 +16,11 @@ interface AmNotificationsAlertDetailsProps {
export const AlertDetails: FC = ({ alert, alertManagerSourceName }) => {
const styles = useStyles2(getStyles);
- const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
+ const permissions = getInstancesPermissions(alertManagerSourceName);
return (
<>
-
+
{alert.status.state === AlertState.Suppressed && (
= ({ alert, aler
)}
-
+
{alert.generatorURL && (
See source
diff --git a/public/app/features/alerting/unified/components/amroutes/AmRootRoute.tsx b/public/app/features/alerting/unified/components/amroutes/AmRootRoute.tsx
index e248d4e2609..e3c1a7d7299 100644
--- a/public/app/features/alerting/unified/components/amroutes/AmRootRoute.tsx
+++ b/public/app/features/alerting/unified/components/amroutes/AmRootRoute.tsx
@@ -6,7 +6,8 @@ import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import { AmRootRouteForm } from './AmRootRouteForm';
import { AmRootRouteRead } from './AmRootRouteRead';
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
-
+import { Authorize } from '../../components/Authorize';
+import { getNotificationsPermissions } from '../../utils/access-control';
export interface AmRootRouteProps {
isEditMode: boolean;
onEnterEditMode: () => void;
@@ -28,6 +29,7 @@ export const AmRootRoute: FC = ({
}) => {
const styles = useStyles2(getStyles);
+ const permissions = getNotificationsPermissions(alertManagerSourceName);
const isReadOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
return (
@@ -37,9 +39,11 @@ export const AmRootRoute: FC = ({
Root policy - default for all alerts
{!isEditMode && !isReadOnly && (
-
- Edit
-
+
+
+ Edit
+
+
)}
diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedRead.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedRead.tsx
index 04d3431e5b2..1ea268ae584 100644
--- a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedRead.tsx
+++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedRead.tsx
@@ -7,13 +7,15 @@ import { emptyRoute } from '../../utils/amroutes';
import { AmRoutesTable } from './AmRoutesTable';
import { getGridStyles } from './gridStyles';
import { MuteTimingsTable } from './MuteTimingsTable';
-import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
+import { Authorize } from '../Authorize';
+import { getNotificationsPermissions } from '../../utils/access-control';
export interface AmRoutesExpandedReadProps {
onChange: (routes: FormAmRoute) => void;
receivers: AmRouteReceiver[];
routes: FormAmRoute;
readOnly?: boolean;
+ alertManagerSourceName: string;
}
export const AmRoutesExpandedRead: FC = ({
@@ -21,10 +23,11 @@ export const AmRoutesExpandedRead: FC = ({
receivers,
routes,
readOnly = false,
+ alertManagerSourceName,
}) => {
const styles = useStyles2(getStyles);
const gridStyles = useStyles2(getGridStyles);
- const [alertManagerSourceName] = useAlertManagerSourceName();
+ const permissions = getNotificationsPermissions(alertManagerSourceName);
const groupWait = routes.groupWaitValue ? `${routes.groupWaitValue}${routes.groupWaitValueType}` : '-';
const groupInterval = routes.groupIntervalValue
@@ -71,23 +74,26 @@ export const AmRoutesExpandedRead: FC = ({
}}
receivers={receivers}
routes={subroutes}
+ alertManagerSourceName={alertManagerSourceName}
/>
) : (
No nested policies configured.
)}
{!isAddMode && !readOnly && (
- {
- setSubroutes((subroutes) => [...subroutes, emptyRoute]);
- setIsAddMode(true);
- }}
- variant="secondary"
- type="button"
- >
- Add nested policy
-
+
+ {
+ setSubroutes((subroutes) => [...subroutes, emptyRoute]);
+ setIsAddMode(true);
+ }}
+ variant="secondary"
+ type="button"
+ >
+ Add nested policy
+
+
)}
Mute timings
diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx
index e5cb1df5d46..c25f812d57a 100644
--- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx
+++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx
@@ -9,6 +9,8 @@ import { Matchers } from '../silences/Matchers';
import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager';
import { intersectionWith, isEqual } from 'lodash';
import { EmptyArea } from '../EmptyArea';
+import { contextSrv } from 'app/core/services/context_srv';
+import { getNotificationsPermissions } from '../../utils/access-control';
export interface AmRoutesTableProps {
isAddMode: boolean;
@@ -18,6 +20,7 @@ export interface AmRoutesTableProps {
routes: FormAmRoute[];
filters?: { queryString?: string; contactPoint?: string };
readOnly?: boolean;
+ alertManagerSourceName: string;
}
type RouteTableColumnProps = DynamicTableColumnProps;
@@ -69,9 +72,15 @@ export const AmRoutesTable: FC = ({
routes,
filters,
readOnly = false,
+ alertManagerSourceName,
}) => {
const [editMode, setEditMode] = useState(false);
const [expandedId, setExpandedId] = useState();
+ const permissions = getNotificationsPermissions(alertManagerSourceName);
+ const canEditRoutes = contextSrv.hasPermission(permissions.update);
+ const canDeleteRoutes = contextSrv.hasPermission(permissions.delete);
+
+ const showActions = !readOnly && (canEditRoutes || canDeleteRoutes);
const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []);
const collapseItem = useCallback(() => setExpandedId(undefined), []);
@@ -102,7 +111,7 @@ export const AmRoutesTable: FC = ({
renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-',
size: 5,
},
- ...(readOnly
+ ...(!showActions
? []
: [
{
@@ -212,6 +221,7 @@ export const AmRoutesTable: FC = ({
receivers={receivers}
routes={item.data}
readOnly={readOnly}
+ alertManagerSourceName={alertManagerSourceName}
/>
)
}
diff --git a/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx b/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx
index c2acff52421..7fba03bfc12 100644
--- a/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx
+++ b/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx
@@ -11,8 +11,12 @@ import { MatcherFilter } from '../alert-groups/MatcherFilter';
import { EmptyArea } from '../EmptyArea';
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
import { AmRoutesTable } from './AmRoutesTable';
+import { Authorize } from '../../components/Authorize';
+import { contextSrv } from 'app/core/services/context_srv';
+import { getNotificationsPermissions } from '../../utils/access-control';
export interface AmSpecificRoutingProps {
+ alertManagerSourceName: string;
onChange: (routes: FormAmRoute) => void;
onRootRouteEdit: () => void;
receivers: AmRouteReceiver[];
@@ -26,6 +30,7 @@ interface Filters {
}
export const AmSpecificRouting: FC = ({
+ alertManagerSourceName,
onChange,
onRootRouteEdit,
receivers,
@@ -34,6 +39,8 @@ export const AmSpecificRouting: FC = ({
}) => {
const [actualRoutes, setActualRoutes] = useState([...routes.routes]);
const [isAddMode, setIsAddMode] = useState(false);
+ const permissions = getNotificationsPermissions(alertManagerSourceName);
+ const canCreateNotifications = contextSrv.hasPermission(permissions.create);
const [searchParams, setSearchParams] = useURLSearchParams();
const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams);
@@ -97,6 +104,7 @@ export const AmSpecificRouting: FC = ({
buttonLabel="Set a default contact point"
onButtonClick={onRootRouteEdit}
text="You haven't set a default contact point for the root route yet."
+ showButton={canCreateNotifications}
/>
)
) : actualRoutes.length > 0 ? (
@@ -132,11 +140,13 @@ export const AmSpecificRouting: FC = ({
)}
{!isAddMode && !readOnly && (
-
-
- New policy
-
-
+
+
+
+ New policy
+
+
+
)}
= ({
receivers={receivers}
routes={actualRoutes}
filters={{ queryString, contactPoint }}
+ alertManagerSourceName={alertManagerSourceName}
/>
>
) : readOnly ? (
@@ -159,6 +170,7 @@ export const AmSpecificRouting: FC = ({
buttonLabel="New specific policy"
onButtonClick={addNewRoute}
text="You haven't created any specific policies yet."
+ showButton={canCreateNotifications}
/>
)}
diff --git a/public/app/features/alerting/unified/components/amroutes/MuteTimingsTable.tsx b/public/app/features/alerting/unified/components/amroutes/MuteTimingsTable.tsx
index 7ee43ea5b43..04497c62d28 100644
--- a/public/app/features/alerting/unified/components/amroutes/MuteTimingsTable.tsx
+++ b/public/app/features/alerting/unified/components/amroutes/MuteTimingsTable.tsx
@@ -17,6 +17,9 @@ import {
getYearsString,
} from '../../utils/alertmanager';
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
+import { Authorize } from '../../components/Authorize';
+import { contextSrv } from 'app/core/services/context_srv';
+import { getNotificationsPermissions } from '../../utils/access-control';
interface Props {
alertManagerSourceName: string;
@@ -27,6 +30,7 @@ interface Props {
export const MuteTimingsTable: FC = ({ alertManagerSourceName, muteTimingNames, hideActions }) => {
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
+ const permissions = getNotificationsPermissions(alertManagerSourceName);
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const [muteTimingName, setMuteTimingName] = useState('');
const { result }: AsyncRequestState =
@@ -56,14 +60,16 @@ export const MuteTimingsTable: FC = ({ alertManagerSourceName, muteTiming
)}
{!hideActions && items.length > 0 && (
-
- New mute timing
-
+
+
+ New mute timing
+
+
)}
{items.length > 0 ? (
@@ -74,6 +80,7 @@ export const MuteTimingsTable: FC = ({ alertManagerSourceName, muteTiming
buttonIcon="plus"
buttonSize="lg"
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
+ showButton={contextSrv.hasPermission(permissions.create)}
/>
) : (
No mute timings configured
@@ -93,6 +100,11 @@ export const MuteTimingsTable: FC = ({ alertManagerSourceName, muteTiming
};
function useColumns(alertManagerSourceName: string, hideActions = false, setMuteTimingName: (name: string) => void) {
+ const permissions = getNotificationsPermissions(alertManagerSourceName);
+
+ const userHasEditPermissions = contextSrv.hasPermission(permissions.update);
+ const userHasDeletePermissions = contextSrv.hasPermission(permissions.delete);
+ const showActions = !hideActions && (userHasEditPermissions || userHasDeletePermissions);
return useMemo((): Array> => {
const columns: Array> = [
{
@@ -109,19 +121,29 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
renderCell: ({ data }) => renderTimeIntervals(data.time_intervals),
},
];
- if (!hideActions) {
+ if (showActions) {
columns.push({
id: 'actions',
label: 'Actions',
renderCell: function renderActions({ data }) {
return (
-
-
-
-
setMuteTimingName(data.name)} />
+
+
+
+
+
+
+ setMuteTimingName(data.name)}
+ />
+
);
},
@@ -129,7 +151,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
});
}
return columns;
- }, [alertManagerSourceName, hideActions, setMuteTimingName]);
+ }, [alertManagerSourceName, setMuteTimingName, showActions, permissions]);
}
function renderTimeIntervals(timeIntervals: TimeInterval[]) {
diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx
index 7a03daa5322..439b851212d 100644
--- a/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx
+++ b/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx
@@ -2,9 +2,11 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
+import { AccessControlAction } from 'app/types';
import React, { FC } from 'react';
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
import { makeAMLink } from '../../utils/misc';
+import { Authorize } from '../Authorize';
import { ReceiversTable } from './ReceiversTable';
import { TemplatesTable } from './TemplatesTable';
@@ -22,15 +24,17 @@ export const ReceiversAndTemplatesView: FC = ({ config, alertManagerName
{!isVanillaAM && }
{isCloud && (
-
-
- For each external Alertmanager you can define global settings, like server addresses, usernames and
- password, for all the supported contact points.
-
-
- {isVanillaAM ? 'View global config' : 'Edit global config'}
-
-
+
+
+
+ For each external Alertmanager you can define global settings, like server addresses, usernames and
+ password, for all the supported contact points.
+
+
+ {isVanillaAM ? 'View global config' : 'Edit global config'}
+
+
+
)}
>
);
diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx
index ba453192eb8..6a3b6761efa 100644
--- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx
+++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx
@@ -13,6 +13,9 @@ import { isReceiverUsed } from '../../utils/alertmanager';
import { useDispatch } from 'react-redux';
import { deleteReceiverAction } from '../../state/actions';
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
+import { Authorize } from '../../components/Authorize';
+import { contextSrv } from 'app/core/services/context_srv';
+import { getNotificationsPermissions } from '../../utils/access-control';
interface Props {
config: AlertManagerCortexConfig;
@@ -24,6 +27,7 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => {
const tableStyles = useStyles2(getAlertTableStyles);
const styles = useStyles2(getStyles);
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
+ const permissions = getNotificationsPermissions(alertManagerName);
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
// receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted
@@ -66,7 +70,7 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => {
className={styles.section}
title="Contact points"
description="Define where the notifications will be sent to, for example email or Slack."
- showButton={!isVanillaAM}
+ showButton={!isVanillaAM && contextSrv.hasPermission(permissions.create)}
addButtonLabel="New contact point"
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
>
@@ -74,13 +78,17 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => {
-
+
+
+
Contact point name
Type
- Actions
+
+ Actions
+
@@ -93,38 +101,46 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => {
{receiver.name}
{receiver.types.join(', ')}
-
- {!isVanillaAM && (
- <>
-
- onClickDeleteReceiver(receiver.name)}
- tooltip="Delete contact point"
- icon="trash-alt"
- />
- >
- )}
- {isVanillaAM && (
-
- )}
-
+
+
+ {!isVanillaAM && (
+ <>
+
+
+
+
+ onClickDeleteReceiver(receiver.name)}
+ tooltip="Delete contact point"
+ icon="trash-alt"
+ />
+
+ >
+ )}
+ {isVanillaAM && (
+
+
+
+ )}
+
+
))}
diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx
index 48f9d83b7a3..b661c350a98 100644
--- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx
+++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx
@@ -9,6 +9,9 @@ import { ReceiversSection } from './ReceiversSection';
import { makeAMLink } from '../../utils/misc';
import { useDispatch } from 'react-redux';
import { deleteTemplateAction } from '../../state/actions';
+import { contextSrv } from 'app/core/services/context_srv';
+import { Authorize } from '../../components/Authorize';
+import { getNotificationsPermissions } from '../../utils/access-control';
interface Props {
config: AlertManagerCortexConfig;
@@ -19,6 +22,7 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => {
const dispatch = useDispatch();
const [expandedTemplates, setExpandedTemplates] = useState>({});
const tableStyles = useStyles2(getAlertTableStyles);
+ const permissions = getNotificationsPermissions(alertManagerName);
const templateRows = useMemo(() => Object.entries(config.template_files), [config]);
const [templateToDelete, setTemplateToDelete] = useState();
@@ -36,6 +40,7 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => {
description="Templates construct the messages that get sent to the contact points."
addButtonLabel="New template"
addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)}
+ showButton={contextSrv.hasPermission(permissions.create)}
>
@@ -47,7 +52,9 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => {
Template
- Actions
+
+ Actions
+
@@ -68,17 +75,27 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => {
/>
{name}
-
-
- setTemplateToDelete(name)} tooltip="delete template" icon="trash-alt" />
-
+
+
+
+
+
+
+ setTemplateToDelete(name)}
+ tooltip="delete template"
+ icon="trash-alt"
+ />
+
+
+
{isExpanded && (
diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
index 86ef755f6a7..b70fd472166 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
@@ -18,9 +18,8 @@ import { useDispatch } from 'react-redux';
import { expireSilenceAction } from '../../state/actions';
import { SilenceDetails } from './SilenceDetails';
import { Stack } from '@grafana/experimental';
-import { AccessControlAction } from '../../../../../types';
import { Authorize } from '../Authorize';
-import { isGrafanaRulesSource } from '../../utils/datasource';
+import { getInstancesPermissions } from '../../utils/access-control';
export interface SilenceTableItem extends Silence {
silencedAlerts: AlertmanagerAlert[];
@@ -38,7 +37,7 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo
const styles = useStyles2(getStyles);
const [queryParams] = useQueryParams();
const filteredSilences = useFilteredSilences(silences);
- const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
+ const permissions = getInstancesPermissions(alertManagerSourceName);
const { silenceState } = getSilenceFiltersFromUrlParams(queryParams);
@@ -65,14 +64,7 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo
{!!silences.length && (
<>
-
+
@@ -178,15 +170,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
function useColumns(alertManagerSourceName: string) {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
- const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
+ const permissions = getInstancesPermissions(alertManagerSourceName);
return useMemo((): SilenceTableColumnProps[] => {
const handleExpireSilenceClick = (id: string) => {
dispatch(expireSilenceAction(alertManagerSourceName, id));
};
- const showActions = contextSrv.hasAccess(
- isExternalAM ? AccessControlAction.AlertingInstancesExternalWrite : AccessControlAction.AlertingInstanceUpdate,
- contextSrv.isEditor
- );
+ const showActions = contextSrv.hasAccess(permissions.update, contextSrv.isEditor);
const columns: SilenceTableColumnProps[] = [
{
id: 'state',
@@ -262,7 +251,7 @@ function useColumns(alertManagerSourceName: string) {
});
}
return columns;
- }, [alertManagerSourceName, dispatch, styles, isExternalAM]);
+ }, [alertManagerSourceName, dispatch, styles, permissions]);
}
export default SilencesTable;
diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts
new file mode 100644
index 00000000000..7d18ec3c7fb
--- /dev/null
+++ b/public/app/features/alerting/unified/utils/access-control.ts
@@ -0,0 +1,78 @@
+import { AccessControlAction } from 'app/types';
+import { isGrafanaRulesSource } from './datasource';
+import { contextSrv } from 'app/core/services/context_srv';
+
+function getAMversion(alertManagerSourceName: string) {
+ return isGrafanaRulesSource(alertManagerSourceName) ? 'grafana' : 'external';
+}
+
+export function getInstancesPermissions(alertManagerSourceName: string) {
+ const amVersion = getAMversion(alertManagerSourceName);
+
+ const permissions = {
+ read: {
+ grafana: AccessControlAction.AlertingInstanceRead,
+ external: AccessControlAction.AlertingInstancesExternalRead,
+ },
+ create: {
+ grafana: AccessControlAction.AlertingInstanceCreate,
+ external: AccessControlAction.AlertingInstancesExternalWrite,
+ },
+ update: {
+ grafana: AccessControlAction.AlertingInstanceUpdate,
+ external: AccessControlAction.AlertingInstancesExternalWrite,
+ },
+ delete: {
+ grafana: AccessControlAction.AlertingInstanceUpdate,
+ external: AccessControlAction.AlertingInstancesExternalWrite,
+ },
+ viewSource: {
+ grafana: AccessControlAction.AlertingInstanceRead,
+ external: AccessControlAction.DataSourcesExplore,
+ },
+ };
+
+ return {
+ read: permissions.read[amVersion],
+ create: permissions.create[amVersion],
+ update: permissions.update[amVersion],
+ delete: permissions.delete[amVersion],
+ viewSource: permissions.viewSource[amVersion],
+ };
+}
+
+export function getNotificationsPermissions(alertManagerSourceName: string) {
+ const amVersion = getAMversion(alertManagerSourceName);
+
+ const permissions = {
+ read: {
+ grafana: AccessControlAction.AlertingNotificationsRead,
+ external: AccessControlAction.AlertingNotificationsExternalRead,
+ },
+ create: {
+ grafana: AccessControlAction.AlertingNotificationsCreate,
+ external: AccessControlAction.AlertingNotificationsExternalWrite,
+ },
+ update: {
+ grafana: AccessControlAction.AlertingNotificationsUpdate,
+ external: AccessControlAction.AlertingNotificationsExternalWrite,
+ },
+ delete: {
+ grafana: AccessControlAction.AlertingNotificationsDelete,
+ external: AccessControlAction.AlertingNotificationsExternalWrite,
+ },
+ };
+
+ return {
+ read: permissions.read[amVersion],
+ create: permissions.create[amVersion],
+ update: permissions.update[amVersion],
+ delete: permissions.delete[amVersion],
+ };
+}
+
+export function evaluateAccess(actions: AccessControlAction[], fallBackUserRoles: string[]) {
+ return () => {
+ return contextSrv.evaluatePermission(() => fallBackUserRoles, actions);
+ };
+}
From 38c842cf4ae13e9a83ded413095fc1fe28e26141 Mon Sep 17 00:00:00 2001
From: Russ <8377044+rdubrock@users.noreply.github.com>
Date: Wed, 6 Apr 2022 09:11:33 -0800
Subject: [PATCH 04/55] UI: Export sparkline from @grafana/ui and fix a broken
storybook page (#47280)
* Chore: export sparkline from @grafana/ui
* fix: add missing config to BigValue story
* chore: tag Sparkline internal
---
packages/grafana-ui/src/components/BigValue/BigValue.story.tsx | 1 +
packages/grafana-ui/src/components/Sparkline/Sparkline.tsx | 1 +
packages/grafana-ui/src/components/index.ts | 1 +
3 files changed, 3 insertions(+)
diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx
index 95013d5f14d..7b1ee1b665b 100644
--- a/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx
+++ b/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx
@@ -71,6 +71,7 @@ export const Basic: Story = ({
name: '',
values: new ArrayVector([1, 2, 3, 4, 3]),
type: FieldType.number,
+ state: { range: { min: 1, max: 4, delta: 3 } },
config: {},
},
};
diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
index 90f7aa89935..dcd271f5d44 100755
--- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
+++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
@@ -43,6 +43,7 @@ const defaultConfig: GraphFieldConfig = {
axisPlacement: AxisPlacement.Hidden,
};
+/** @internal */
export class Sparkline extends PureComponent {
constructor(props: SparklineProps) {
super(props);
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 3deb48b17f1..3d04933efe2 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -84,6 +84,7 @@ export {
BigValueJustifyMode,
BigValueTextMode,
} from './BigValue/BigValue';
+export { Sparkline } from './Sparkline/Sparkline';
export { Gauge } from './Gauge/Gauge';
export { Graph } from './Graph/Graph';
From b2f24e9d5b1c4e2f165607a4ffc33c31882f1f78 Mon Sep 17 00:00:00 2001
From: Melori Arellano
Date: Wed, 6 Apr 2022 07:23:51 -1000
Subject: [PATCH 05/55] DOCS: Update Query variable section (#46132)
* DOCS: Update Query variable section
Add that the terms query expects a keyword field type. Updated the field names to remove the @ symbol that could mislead a user into thinking that is part of the query. I left one query example using the field name only and one using the fieldname.keyword syntax. Both are valid depending on the field mapping in the index.
* Update docs/sources/datasources/elasticsearch.md
Co-authored-by: Giordano Ricci
* Update docs/sources/datasources/elasticsearch.md
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
* Update docs/sources/datasources/elasticsearch.md
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
Co-authored-by: Giordano Ricci
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
---
docs/sources/datasources/elasticsearch.md | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/docs/sources/datasources/elasticsearch.md b/docs/sources/datasources/elasticsearch.md
index 07a46386c94..767beee6109 100644
--- a/docs/sources/datasources/elasticsearch.md
+++ b/docs/sources/datasources/elasticsearch.md
@@ -143,23 +143,23 @@ types of template variables.
### Query variable
-The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string.
+The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
| Query | Description |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
-| `{"find": "terms", "field": "@hostname", "size": 1000}` | Returns a list of values for a field using term aggregation. Query will use current dashboard time range as time range for query. |
-| `{"find": "terms", "field": "@hostname", "query": ''}` | Returns a list of values for a field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
+| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
+| `{"find": "terms", "field": "hostname", "query": ''}` | Returns a list of values for a keyword field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
There is a default size limit of 500 on terms queries. Set the size property in your query to set a custom limit.
You can use other variables inside the query. Example query definition for a variable named `$host`.
```
-{"find": "terms", "field": "@hostname", "query": "@source:$source"}
+{"find": "terms", "field": "hostname", "query": "source:$source"}
```
-In the above example, we use another variable named `$source` inside the query definition. Whenever you change, via the dropdown, the current value of the ` $source` variable, it will trigger an update of the `$host` variable so it now only contains hostnames filtered by in this case the
-`@source` document property.
+In the above example, we use another variable named `$source` inside the query definition. Whenever you change, via the dropdown, the current value of the `$source` variable, it will trigger an update of the `$host` variable so it now only contains hostnames filtered by in this case the
+`source` document property.
These queries by default return results in term order (which can then be sorted alphabetically or numerically as for any variable).
To produce a list of terms sorted by doc count (a top-N values list), add an `orderBy` property of "doc_count".
@@ -167,22 +167,22 @@ This automatically selects a descending sort; using "asc" with doc_count (a bott
To keep terms in the doc count order, set the variable's Sort dropdown to **Disabled**; you might alternatively still want to use e.g. **Alphabetical** to re-sort them.
```
-{"find": "terms", "field": "@hostname", "orderBy": "doc_count"}
+{"find": "terms", "field": "hostname", "orderBy": "doc_count"}
```
### Using variables in queries
There are two syntaxes:
-- `$` Example: @hostname:$hostname
-- `[[varname]]` Example: @hostname:[[hostname]]
+- `$` Example: hostname:$hostname
+- `[[varname]]` Example: hostname:[[hostname]]
Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the _Multi-value_ or _Include all value_
options are enabled, Grafana converts the labels from plain text to a lucene compatible condition.

-In the above example, we have a lucene query that filters documents based on the `@hostname` property using a variable named `$hostname`. It is also using
+In the above example, we have a lucene query that filters documents based on the `hostname` property using a variable named `$hostname`. It is also using
a variable in the _Terms_ group by field input box. This allows you to use a variable to quickly change how the data is grouped.
Example dashboard:
From 0b897b631876fcfb657434726b8d85be9dec31aa Mon Sep 17 00:00:00 2001
From: achatterjee-grafana
Date: Wed, 6 Apr 2022 14:34:26 -0400
Subject: [PATCH 06/55] Updated exemplar specific data source configuration
properties.
---
docs/sources/datasources/prometheus.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/sources/datasources/prometheus.md b/docs/sources/datasources/prometheus.md
index 8768966321e..3699855bc98 100644
--- a/docs/sources/datasources/prometheus.md
+++ b/docs/sources/datasources/prometheus.md
@@ -29,10 +29,10 @@ To access Prometheus settings, hover your mouse over the **Configuration** (gear
| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
-| `Label name` | Add the name of the field in the label object. |
-| `URL` | If the link is external, then enter the full link URL. You can interpolate the value from the field with `${__value.raw }` macro. |
-| `URL Label` | (Optional) Set a custom display label for the link URL. The link label defaults to the full external URL or the name of datasource and is overridden by this setting. |
-| `Internal link` | Select if the link is internal or external. In the case of an internal link, a data source selector allows you to select the target data source. Supports tracing data sources only. |
+| `Label name` | (Exemplar configuration) Add a name for the exemplar traceID property. |
+| `URL` | (Exemplar configuration) For external link, enter the full URL. You can interpolate the value from the field with `${__value.raw }` macro. |
+| `URL Label` | (Exemplar configuration) Optionally, add a custom display label to override the value of the `Label name` field. |
+| `Internal link` | (Exemplar configuration) Enable this option if the link is internal. For an internal link, the data source selector allows you to select the tracing data source. |
## Prometheus query editor
From 8665745a146e9982fda65f5d1fbb9138a37bd858 Mon Sep 17 00:00:00 2001
From: achatterjee-grafana
Date: Wed, 6 Apr 2022 14:43:36 -0400
Subject: [PATCH 07/55] Few more changes. Also ran prettier to fix spacing
issues.
---
docs/sources/datasources/elasticsearch.md | 10 +++----
docs/sources/datasources/prometheus.md | 35 ++++++++++++-----------
2 files changed, 23 insertions(+), 22 deletions(-)
diff --git a/docs/sources/datasources/elasticsearch.md b/docs/sources/datasources/elasticsearch.md
index 767beee6109..24e0d7ef3df 100644
--- a/docs/sources/datasources/elasticsearch.md
+++ b/docs/sources/datasources/elasticsearch.md
@@ -143,12 +143,12 @@ types of template variables.
### Query variable
-The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
+The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
-| Query | Description |
-| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
-| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
+| Query | Description |
+| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
+| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
| `{"find": "terms", "field": "hostname", "query": ''}` | Returns a list of values for a keyword field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
There is a default size limit of 500 on terms queries. Set the size property in your query to set a custom limit.
diff --git a/docs/sources/datasources/prometheus.md b/docs/sources/datasources/prometheus.md
index 3699855bc98..c46fd3848d0 100644
--- a/docs/sources/datasources/prometheus.md
+++ b/docs/sources/datasources/prometheus.md
@@ -16,23 +16,24 @@ Grafana includes built-in support for Prometheus. This topic explains options, v
To access Prometheus settings, hover your mouse over the **Configuration** (gear) icon, then click **Data Sources**, and then click the Prometheus data source.
-| Name | Description |
-| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
-| `Default` | Default data source that is pre-selected for new panels. |
-| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
-| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
-| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
-| `User` | User name for basic authentication. |
-| `Password` | Password for basic authentication. |
-| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
-| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
-| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
-| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
-| `Label name` | (Exemplar configuration) Add a name for the exemplar traceID property. |
-| `URL` | (Exemplar configuration) For external link, enter the full URL. You can interpolate the value from the field with `${__value.raw }` macro. |
-| `URL Label` | (Exemplar configuration) Optionally, add a custom display label to override the value of the `Label name` field. |
-| `Internal link` | (Exemplar configuration) Enable this option if the link is internal. For an internal link, the data source selector allows you to select the tracing data source. |
+| Name | Description |
+| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
+| `Default` | Default data source that is pre-selected for new panels. |
+| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
+| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
+| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
+| `User` | User name for basic authentication. |
+| `Password` | Password for basic authentication. |
+| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
+| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
+| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
+| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
+| **Examplar configuration** | |
+| `Label name` | Add a name for the exemplar traceID property. |
+| `URL` | For external link, enter the full URL. You can interpolate the value from the field with `${__value.raw }` macro. |
+| `URL Label` | Optionally, add a custom display label to override the value of the `Label name` field. |
+| `Internal link` | Enable this option for an internal link. The data source selector that can see now allows you to select the tracing data source for exemplar data. |
## Prometheus query editor
From c621cdb986489514803b2ab2114bdefab71e7be7 Mon Sep 17 00:00:00 2001
From: achatterjee-grafana
Date: Wed, 6 Apr 2022 14:46:55 -0400
Subject: [PATCH 08/55] Revert "Few more changes. Also ran prettier to fix
spacing issues."
This reverts commit 8665745a146e9982fda65f5d1fbb9138a37bd858.
---
docs/sources/datasources/elasticsearch.md | 10 +++----
docs/sources/datasources/prometheus.md | 35 +++++++++++------------
2 files changed, 22 insertions(+), 23 deletions(-)
diff --git a/docs/sources/datasources/elasticsearch.md b/docs/sources/datasources/elasticsearch.md
index 24e0d7ef3df..767beee6109 100644
--- a/docs/sources/datasources/elasticsearch.md
+++ b/docs/sources/datasources/elasticsearch.md
@@ -143,12 +143,12 @@ types of template variables.
### Query variable
-The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
+The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
-| Query | Description |
-| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
-| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
+| Query | Description |
+| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
+| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
| `{"find": "terms", "field": "hostname", "query": ''}` | Returns a list of values for a keyword field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
There is a default size limit of 500 on terms queries. Set the size property in your query to set a custom limit.
diff --git a/docs/sources/datasources/prometheus.md b/docs/sources/datasources/prometheus.md
index c46fd3848d0..3699855bc98 100644
--- a/docs/sources/datasources/prometheus.md
+++ b/docs/sources/datasources/prometheus.md
@@ -16,24 +16,23 @@ Grafana includes built-in support for Prometheus. This topic explains options, v
To access Prometheus settings, hover your mouse over the **Configuration** (gear) icon, then click **Data Sources**, and then click the Prometheus data source.
-| Name | Description |
-| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
-| `Default` | Default data source that is pre-selected for new panels. |
-| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
-| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
-| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
-| `User` | User name for basic authentication. |
-| `Password` | Password for basic authentication. |
-| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
-| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
-| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
-| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
-| **Examplar configuration** | |
-| `Label name` | Add a name for the exemplar traceID property. |
-| `URL` | For external link, enter the full URL. You can interpolate the value from the field with `${__value.raw }` macro. |
-| `URL Label` | Optionally, add a custom display label to override the value of the `Label name` field. |
-| `Internal link` | Enable this option for an internal link. The data source selector that can see now allows you to select the tracing data source for exemplar data. |
+| Name | Description |
+| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
+| `Default` | Default data source that is pre-selected for new panels. |
+| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
+| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
+| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
+| `User` | User name for basic authentication. |
+| `Password` | Password for basic authentication. |
+| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
+| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
+| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
+| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
+| `Label name` | (Exemplar configuration) Add a name for the exemplar traceID property. |
+| `URL` | (Exemplar configuration) For external link, enter the full URL. You can interpolate the value from the field with `${__value.raw }` macro. |
+| `URL Label` | (Exemplar configuration) Optionally, add a custom display label to override the value of the `Label name` field. |
+| `Internal link` | (Exemplar configuration) Enable this option if the link is internal. For an internal link, the data source selector allows you to select the tracing data source. |
## Prometheus query editor
From ee91561bf6250d2adadfff1934eba95da86c8dee Mon Sep 17 00:00:00 2001
From: achatterjee-grafana
Date: Wed, 6 Apr 2022 14:47:19 -0400
Subject: [PATCH 09/55] Revert "Updated exemplar specific data source
configuration properties."
This reverts commit 0b897b631876fcfb657434726b8d85be9dec31aa.
---
docs/sources/datasources/prometheus.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/sources/datasources/prometheus.md b/docs/sources/datasources/prometheus.md
index 3699855bc98..8768966321e 100644
--- a/docs/sources/datasources/prometheus.md
+++ b/docs/sources/datasources/prometheus.md
@@ -29,10 +29,10 @@ To access Prometheus settings, hover your mouse over the **Configuration** (gear
| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
-| `Label name` | (Exemplar configuration) Add a name for the exemplar traceID property. |
-| `URL` | (Exemplar configuration) For external link, enter the full URL. You can interpolate the value from the field with `${__value.raw }` macro. |
-| `URL Label` | (Exemplar configuration) Optionally, add a custom display label to override the value of the `Label name` field. |
-| `Internal link` | (Exemplar configuration) Enable this option if the link is internal. For an internal link, the data source selector allows you to select the tracing data source. |
+| `Label name` | Add the name of the field in the label object. |
+| `URL` | If the link is external, then enter the full link URL. You can interpolate the value from the field with `${__value.raw }` macro. |
+| `URL Label` | (Optional) Set a custom display label for the link URL. The link label defaults to the full external URL or the name of datasource and is overridden by this setting. |
+| `Internal link` | Select if the link is internal or external. In the case of an internal link, a data source selector allows you to select the target data source. Supports tracing data sources only. |
## Prometheus query editor
From 916749897cc650de13fb0fccc80365c06f668034 Mon Sep 17 00:00:00 2001
From: achatterjee-grafana
<70489351+achatterjee-grafana@users.noreply.github.com>
Date: Wed, 6 Apr 2022 15:56:47 -0400
Subject: [PATCH 10/55] Updated exemplar configuration details (#47411)
* Updated exemplar configuration details
* Added a new property, rearranged existing properties, and ran prettier.
---
docs/sources/datasources/elasticsearch.md | 10 +++----
docs/sources/datasources/prometheus.md | 36 ++++++++++++-----------
2 files changed, 24 insertions(+), 22 deletions(-)
diff --git a/docs/sources/datasources/elasticsearch.md b/docs/sources/datasources/elasticsearch.md
index 767beee6109..24e0d7ef3df 100644
--- a/docs/sources/datasources/elasticsearch.md
+++ b/docs/sources/datasources/elasticsearch.md
@@ -143,12 +143,12 @@ types of template variables.
### Query variable
-The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
+The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
-| Query | Description |
-| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
-| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
+| Query | Description |
+| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
+| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
| `{"find": "terms", "field": "hostname", "query": ''}` | Returns a list of values for a keyword field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
There is a default size limit of 500 on terms queries. Set the size property in your query to set a custom limit.
diff --git a/docs/sources/datasources/prometheus.md b/docs/sources/datasources/prometheus.md
index 8768966321e..0af9f91187c 100644
--- a/docs/sources/datasources/prometheus.md
+++ b/docs/sources/datasources/prometheus.md
@@ -16,23 +16,25 @@ Grafana includes built-in support for Prometheus. This topic explains options, v
To access Prometheus settings, hover your mouse over the **Configuration** (gear) icon, then click **Data Sources**, and then click the Prometheus data source.
-| Name | Description |
-| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
-| `Default` | Default data source that is pre-selected for new panels. |
-| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
-| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
-| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
-| `User` | User name for basic authentication. |
-| `Password` | Password for basic authentication. |
-| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
-| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
-| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
-| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
-| `Label name` | Add the name of the field in the label object. |
-| `URL` | If the link is external, then enter the full link URL. You can interpolate the value from the field with `${__value.raw }` macro. |
-| `URL Label` | (Optional) Set a custom display label for the link URL. The link label defaults to the full external URL or the name of datasource and is overridden by this setting. |
-| `Internal link` | Select if the link is internal or external. In the case of an internal link, a data source selector allows you to select the target data source. Supports tracing data sources only. |
+| Name | Description |
+| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
+| `Default` | Default data source that is pre-selected for new panels. |
+| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
+| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
+| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
+| `User` | User name for basic authentication. |
+| `Password` | Password for basic authentication. |
+| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
+| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
+| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
+| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
+| **Exemplars configuration** | |
+| `Internal link` | Enable this option is you have an internal link. When you enable this option, you will see a data source selector. Select the backend tracing data store for your exemplar data. |
+| `Data source` | You will see this option only if you enable `Internal link` option. Select the backend tracing data store for your exemplar data. |
+| `URL` | You will see this option only if the `Internal link` option is disabled. Enter the full URL of the external link. You can interpolate the value from the field with `${__value.raw }` macro. |
+| `URL Label` | (Optional) add a custom display label to override the value of the `Label name` field. |
+| `Label name` | Add a name for the exemplar traceID property. |
## Prometheus query editor
From 015a31644c37f3949bd48be5a78aeda5c51382e0 Mon Sep 17 00:00:00 2001
From: Nathan Marrs
Date: Wed, 6 Apr 2022 16:59:24 -0700
Subject: [PATCH 11/55] Search: clean up styling (#47221)
---
.../app/features/search/page/table/Table.tsx | 65 +++++++++----------
.../features/search/page/table/columns.tsx | 49 +++++++++-----
2 files changed, 64 insertions(+), 50 deletions(-)
diff --git a/public/app/features/search/page/table/Table.tsx b/public/app/features/search/page/table/Table.tsx
index b91e96f67d2..e28e17fa222 100644
--- a/public/app/features/search/page/table/Table.tsx
+++ b/public/app/features/search/page/table/Table.tsx
@@ -1,11 +1,11 @@
import React, { useMemo } from 'react';
-import { useTable, useBlockLayout, Column, TableOptions, Cell } from 'react-table';
+import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table';
import { DataFrame, DataFrameType, DataFrameView, DataSourceRef, Field, GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
-import { useStyles2 } from '@grafana/ui';
import { FixedSizeList } from 'react-window';
import { TableCell } from '@grafana/ui/src/components/Table/TableCell';
import { getTableStyles } from '@grafana/ui/src/components/Table/styles';
+import { useStyles2 } from '@grafana/ui';
import { LocationInfo } from '../../service';
import { generateColumns } from './columns';
@@ -66,7 +66,7 @@ export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange
[memoizedColumns, memoizedData]
);
- const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useBlockLayout);
+ const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useAbsoluteLayout);
const RenderRow = React.useCallback(
({ index: rowIndex, style }) => {
@@ -78,35 +78,15 @@ export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange
return (
{row.cells.map((cell: Cell, index: number) => {
- if (
- cell.column.id === 'column-checkbox' ||
- cell.column.id === 'column-tags' ||
- cell.column.id === 'column-datasource'
- ) {
- return (
-
- );
- }
-
return (
-
-
+
+
);
})}
@@ -178,7 +158,12 @@ const getStyles = (theme: GrafanaTheme2) => {
align-items: center;
`,
cellWrapper: css`
- display: flex;
+ div {
+ border-right: none;
+ &:hover {
+ box-shadow: none;
+ }
+ }
`,
headerCell: css`
padding-top: 2px;
@@ -190,17 +175,30 @@ const getStyles = (theme: GrafanaTheme2) => {
align-items: center;
`,
rowContainer: css`
+ label: row;
&:hover {
background-color: ${rowHoverBg};
}
`,
typeIcon: css`
+ margin-left: 5px;
margin-right: 9.5px;
vertical-align: middle;
display: inline-block;
margin-bottom: ${theme.v1.spacing.xxs};
fill: ${theme.colors.text.secondary};
`,
+ datasourceItem: css`
+ span {
+ &:hover {
+ color: ${theme.colors.text.link};
+ }
+ }
+ `,
+ invalidDatasourceItem: css`
+ color: ${theme.colors.error.main};
+ text-decoration: line-through;
+ `,
typeText: css`
color: ${theme.colors.text.secondary};
`,
@@ -218,13 +216,14 @@ const getStyles = (theme: GrafanaTheme2) => {
margin-top: 5px;
`,
infoWrap: css`
+ color: ${theme.colors.text.secondary};
span {
margin-right: 10px;
}
`,
tagList: css`
justify-content: flex-start;
- pointer-events: auto;
+ flex-wrap: nowrap;
`,
};
};
diff --git a/public/app/features/search/page/table/columns.tsx b/public/app/features/search/page/table/columns.tsx
index a88634fdf91..5edab606248 100644
--- a/public/app/features/search/page/table/columns.tsx
+++ b/public/app/features/search/page/table/columns.tsx
@@ -65,6 +65,7 @@ export const generateColumns = (
const DATASOURCE_COLUMN_WIDTH = 200;
const INFO_COLUMN_WIDTH = 100;
const LOCATION_COLUMN_WIDTH = 200;
+ const TAGS_COLUMN_WIDTH = 200;
width = TYPE_COLUMN_WIDTH;
if (isDashboardList) {
@@ -92,12 +93,28 @@ export const generateColumns = (
// Show datasources if we have any
if (access.datasource && hasFieldValue(access.datasource)) {
width = DATASOURCE_COLUMN_WIDTH;
- columns.push(makeDataSourceColumn(access.datasource, width, styles.typeIcon, onDatasourceChange));
+ columns.push(
+ makeDataSourceColumn(
+ access.datasource,
+ width,
+ styles.typeIcon,
+ styles.datasourceItem,
+ styles.invalidDatasourceItem,
+ onDatasourceChange
+ )
+ );
+ availableWidth -= width;
+ }
+
+ // Show tags if we have any
+ if (access.tags && hasFieldValue(access.tags)) {
+ width = TAGS_COLUMN_WIDTH;
+ columns.push(makeTagsColumn(access.tags, width, styles.tagList, tags, onTagFilterChange));
availableWidth -= width;
}
if (isDashboardList) {
- width = INFO_COLUMN_WIDTH;
+ width = Math.max(availableWidth, INFO_COLUMN_WIDTH);
columns.push({
Cell: DefaultCell,
id: `column-info`,
@@ -109,8 +126,8 @@ export const generateColumns = (
},
width: width,
});
- availableWidth -= width;
} else {
+ width = Math.max(availableWidth, LOCATION_COLUMN_WIDTH);
columns.push({
Cell: DefaultCell,
id: `column-location`,
@@ -138,15 +155,8 @@ export const generateColumns = (
}
return null;
},
- width: LOCATION_COLUMN_WIDTH,
+ width: width,
});
- availableWidth -= width;
- }
-
- // Show tags if we have any
- if (access.tags && hasFieldValue(access.tags)) {
- width = Math.max(availableWidth, 250);
- columns.push(makeTagsColumn(access.tags, width, styles.tagList, tags, onTagFilterChange));
}
return columns;
@@ -176,6 +186,8 @@ function makeDataSourceColumn(
field: Field
,
width: number,
iconClass: string,
+ datasourceItemClass: string,
+ invalidDatasourceItemClass: string,
onDatasourceChange: (datasource?: string) => void
): TableColumn {
return {
@@ -188,27 +200,30 @@ function makeDataSourceColumn(
if (dslist?.length) {
const srv = getDataSourceSrv();
return (
-
+
);
From 0845ba1abb0ec7274727070ad9ed24c17f4388d4 Mon Sep 17 00:00:00 2001
From: Dimitris Sotirakis
Date: Thu, 7 Apr 2022 09:30:09 +0300
Subject: [PATCH 12/55] Make gsutil command don't fail if dir doesn't exist
(#47394)
---
.drone.yml | 14 +++++++-------
scripts/drone/steps/lib.star | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/.drone.yml b/.drone.yml
index a64dddc89aa..a3de2242549 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -901,8 +901,8 @@ steps:
- commands:
- printenv GCP_KEY | base64 -d > /tmp/gcpkey.json
- gcloud auth activate-service-account --key-file=/tmp/gcpkey.json
- - gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary && gsutil
- -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary
+ - gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary || true
+ && gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary
depends_on:
- build-storybook
- end-to-end-tests-dashboards-suite
@@ -1594,10 +1594,10 @@ steps:
- commands:
- printenv GCP_KEY | base64 -d > /tmp/gcpkey.json
- gcloud auth activate-service-account --key-file=/tmp/gcpkey.json
- - gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest && gsutil
- -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest
- - gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG} &&
- gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG}
+ - gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest || true
+ && gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest
+ - gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG} ||
+ true && gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG}
depends_on:
- build-storybook
- end-to-end-tests-dashboards-suite
@@ -4448,6 +4448,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
-hmac: ec763e70ae08f79845dd4a59c56e6d08ef8e6b39a1f59663aabcc6d9fe758287
+hmac: 0fbba9c5099f1a3585f866b624f198058332624a4de32193a5326450a39cb8f3
...
diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star
index f373ccf00ca..2eea9aa70d3 100644
--- a/scripts/drone/steps/lib.star
+++ b/scripts/drone/steps/lib.star
@@ -273,7 +273,7 @@ def store_storybook_step(edition, ver_mode, trigger=None):
'printenv GCP_KEY | base64 -d > /tmp/gcpkey.json',
'gcloud auth activate-service-account --key-file=/tmp/gcpkey.json',
] + [
- 'gsutil -m rm -r gs://$${{PRERELEASE_BUCKET}}/artifacts/storybook/{} && gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${{PRERELEASE_BUCKET}}/artifacts/storybook/{}'.format(
+ 'gsutil -m rm -r gs://$${{PRERELEASE_BUCKET}}/artifacts/storybook/{} || true && gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${{PRERELEASE_BUCKET}}/artifacts/storybook/{}'.format(
c, c)
for c in channels
])
From 0639ccbdff1f4cc65d4b25004cf0887abc76ff5c Mon Sep 17 00:00:00 2001
From: Victor Marin <36818606+mdvictor@users.noreply.github.com>
Date: Thu, 7 Apr 2022 09:56:23 +0300
Subject: [PATCH 13/55] Histogram Panel: Take decimal into consideration
(#47330)
---
.../src/transformations/transformers/histogram.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts
index eef3698c90e..a48f00c1861 100644
--- a/packages/grafana-data/src/transformations/transformers/histogram.ts
+++ b/packages/grafana-data/src/transformations/transformers/histogram.ts
@@ -159,7 +159,9 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
for (const frame of frames) {
for (const field of frame.fields) {
if (field.type === FieldType.number) {
- allValues = allValues.concat(field.values.toArray());
+ allValues = allValues.concat(
+ field.values.toArray().map((val: number) => Number(val.toFixed(field.config.decimals ?? 0)))
+ );
}
}
}
@@ -217,7 +219,7 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
unit: undefined,
},
});
- if (!config && field.config.unit) {
+ if (!config && Object.keys(field.config).length) {
config = field.config;
}
}
From 6e0826a9866a348ef956fabd068e4e99f4e0644b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Farkas?=
Date: Thu, 7 Apr 2022 09:44:04 +0200
Subject: [PATCH 14/55] loki: do not convert NaN to null (#45389)
---
public/app/plugins/datasource/loki/result_transformer.test.ts | 2 +-
public/app/plugins/datasource/loki/result_transformer.ts | 4 ----
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts
index f0e64b5e8ef..396386e6419 100644
--- a/public/app/plugins/datasource/loki/result_transformer.test.ts
+++ b/public/app/plugins/datasource/loki/result_transformer.test.ts
@@ -306,7 +306,7 @@ describe('enhanceDataFrame', () => {
[1, 1000],
[0, 2000],
[1, 4000],
- [null, 7000],
+ [NaN, 7000],
[Infinity, 8000],
[-Infinity, 9000],
]);
diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts
index 34aa2e5900b..79fde9b62b2 100644
--- a/public/app/plugins/datasource/loki/result_transformer.ts
+++ b/public/app/plugins/datasource/loki/result_transformer.ts
@@ -209,10 +209,6 @@ export function lokiPointsToTimeseriesPoints(data: Array<[number, string]>): Tim
for (const [time, value] of data) {
let datapointValue: TimeSeriesValue = parsePrometheusFormatSampleValue(value);
- if (isNaN(datapointValue)) {
- datapointValue = null;
- }
-
const timestamp = time * 1000;
datapoints.push([datapointValue, timestamp]);
From 25235b3167eda8d2a1ea1ac8c31df8dbd4400fd2 Mon Sep 17 00:00:00 2001
From: Timur Olzhabayev
Date: Thu, 7 Apr 2022 09:54:27 +0200
Subject: [PATCH 15/55] Removing plugin list panel (#46914)
The core plugin named plugin list panel is removed
---
devenv/dev-dashboards/all-panels.json | 11 --
.../manager/manager_integration_test.go | 1 -
.../devenvgoldenfiles/all-panels.json | 21 ---
.../testdata/devdash-all-panels-info.json | 5 -
.../api/plugins/data/expectedListResp.json | 36 -----
.../app/features/plugins/built_in_plugins.ts | 2 -
.../plugins/panel/pluginlist/PluginList.tsx | 151 ------------------
public/app/plugins/panel/pluginlist/README.md | 3 -
.../components/UpdatePluginModal.tsx | 59 -------
.../pluginlist/img/icn-dashlist-panel.svg | 1 -
.../app/plugins/panel/pluginlist/module.tsx | 4 -
.../app/plugins/panel/pluginlist/plugin.json | 19 ---
12 files changed, 313 deletions(-)
delete mode 100644 public/app/plugins/panel/pluginlist/PluginList.tsx
delete mode 100644 public/app/plugins/panel/pluginlist/README.md
delete mode 100644 public/app/plugins/panel/pluginlist/components/UpdatePluginModal.tsx
delete mode 100644 public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg
delete mode 100644 public/app/plugins/panel/pluginlist/module.tsx
delete mode 100644 public/app/plugins/panel/pluginlist/plugin.json
diff --git a/devenv/dev-dashboards/all-panels.json b/devenv/dev-dashboards/all-panels.json
index 6e4cf7cd21d..a76161c64f2 100644
--- a/devenv/dev-dashboards/all-panels.json
+++ b/devenv/dev-dashboards/all-panels.json
@@ -374,17 +374,6 @@
"title": "Dashboard list",
"type": "dashlist"
},
- {
- "gridPos": {
- "h": 8,
- "w": 12,
- "x": 0,
- "y": 29
- },
- "id": 30,
- "title": "Panel list",
- "type": "pluginlist"
- },
{
"gridPos": {
"h": 8,
diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go
index 3a74f718648..cf6eec40fac 100644
--- a/pkg/plugins/manager/manager_integration_test.go
+++ b/pkg/plugins/manager/manager_integration_test.go
@@ -127,7 +127,6 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
"news": {},
"nodeGraph": {},
"piechart": {},
- "pluginlist": {},
"stat": {},
"state-timeline": {},
"status-history": {},
diff --git a/pkg/schema/testdata/devenvgoldenfiles/all-panels.json b/pkg/schema/testdata/devenvgoldenfiles/all-panels.json
index 305b3608691..c86ce253881 100644
--- a/pkg/schema/testdata/devenvgoldenfiles/all-panels.json
+++ b/pkg/schema/testdata/devenvgoldenfiles/all-panels.json
@@ -444,27 +444,6 @@
"overrides": []
}
},
- {
- "gridPos": {
- "h": 8,
- "w": 12,
- "x": 0,
- "y": 29
- },
- "id": 30,
- "title": "Panel list",
- "type": "pluginlist",
- "transparent": false,
- "repeatDirection": "h",
- "transformations": [],
- "options": {},
- "fieldConfig": {
- "defaults": {
- "custom": {}
- },
- "overrides": []
- }
- },
{
"gridPos": {
"h": 8,
diff --git a/pkg/services/searchV2/extract/testdata/devdash-all-panels-info.json b/pkg/services/searchV2/extract/testdata/devdash-all-panels-info.json
index 1818176ca66..26e3218ec73 100644
--- a/pkg/services/searchV2/extract/testdata/devdash-all-panels-info.json
+++ b/pkg/services/searchV2/extract/testdata/devdash-all-panels-info.json
@@ -55,11 +55,6 @@
"type": "dashlist",
"pluginVersion": "8.1.0-pre"
},
- {
- "id": 30,
- "title": "Panel list",
- "type": "pluginlist"
- },
{
"id": 6,
"title": "Alert list",
diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json
index e83c9ab8367..2c012c8a7a5 100644
--- a/pkg/tests/api/plugins/data/expectedListResp.json
+++ b/pkg/tests/api/plugins/data/expectedListResp.json
@@ -1034,42 +1034,6 @@
"signatureType": "",
"signatureOrg": ""
},
- {
- "name": "Plugin list",
- "type": "panel",
- "id": "pluginlist",
- "enabled": true,
- "pinned": false,
- "info": {
- "author": {
- "name": "Grafana Labs",
- "url": "https://grafana.com"
- },
- "description": "Plugin List for Grafana",
- "links": null,
- "logos": {
- "small": "public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg",
- "large": "public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg"
- },
- "build": {},
- "screenshots": null,
- "version": "",
- "updated": ""
- },
- "dependencies": {
- "grafanaDependency": "",
- "grafanaVersion": "*",
- "plugins": []
- },
- "latestVersion": "",
- "hasUpdate": false,
- "defaultNavUrl": "/plugins/pluginlist/",
- "category": "",
- "state": "",
- "signature": "internal",
- "signatureType": "",
- "signatureOrg": ""
- },
{
"name": "PostgreSQL",
"type": "datasource",
diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts
index ebe372a6fff..3128e26f815 100644
--- a/public/app/features/plugins/built_in_plugins.ts
+++ b/public/app/features/plugins/built_in_plugins.ts
@@ -47,7 +47,6 @@ import * as statusHistoryPanel from 'app/plugins/panel/status-history/module';
import * as candlestickPanel from 'app/plugins/panel/candlestick/module';
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
-import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
import * as annoListPanel from 'app/plugins/panel/annolist/module';
import * as heatmapPanelNG from 'app/plugins/panel/heatmap-new/module';
@@ -110,7 +109,6 @@ const builtInPlugins: any = {
'app/plugins/panel/canvas/module': canvasPanel,
'app/plugins/panel/icon/module': iconPanel,
'app/plugins/panel/dashlist/module': dashListPanel,
- 'app/plugins/panel/pluginlist/module': pluginsListPanel,
'app/plugins/panel/alertlist/module': alertListPanel,
'app/plugins/panel/annolist/module': annoListPanel,
'app/plugins/panel/heatmap/module': heatmapPanel,
diff --git a/public/app/plugins/panel/pluginlist/PluginList.tsx b/public/app/plugins/panel/pluginlist/PluginList.tsx
deleted file mode 100644
index c3af90ac740..00000000000
--- a/public/app/plugins/panel/pluginlist/PluginList.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import React from 'react';
-import { useAsync } from 'react-use';
-import { css, cx } from '@emotion/css';
-import { GrafanaTheme, PanelProps, PluginMeta, PluginType } from '@grafana/data';
-import { CustomScrollbar, ModalsController, stylesFactory, Tooltip, useStyles } from '@grafana/ui';
-import { contextSrv } from 'app/core/services/context_srv';
-import { getBackendSrv } from 'app/core/services/backend_srv';
-import { UpdatePluginModal } from './components/UpdatePluginModal';
-
-export function PluginList(props: PanelProps) {
- const pluginState = useAsync(async () => {
- const plugins: PluginMeta[] = await getBackendSrv().get('api/plugins', { embedded: 0, core: 0 });
- return [
- { header: 'Installed Apps', list: plugins.filter((p) => p.type === PluginType.app), type: PluginType.app },
- { header: 'Installed Panels', list: plugins.filter((p) => p.type === PluginType.panel), type: PluginType.panel },
- {
- header: 'Installed Datasources',
- list: plugins.filter((p) => p.type === PluginType.datasource),
- type: PluginType.datasource,
- },
- ];
- }, []);
-
- const styles = useStyles(getStyles);
- const isAdmin = contextSrv.user.isGrafanaAdmin;
-
- if (pluginState.loading || pluginState.value === undefined) {
- return null;
- }
-
- return (
-
-
- {pluginState.value.map((category) => (
-
- ))}
-
-
- );
-}
-
-const getStyles = stylesFactory((theme: GrafanaTheme) => ({
- pluginList: css`
- display: flex;
- flex-direction: column;
- `,
- section: css`
- display: flex;
- flex-direction: column;
- &:not(:last-of-type) {
- margin-bottom: 16px;
- }
- `,
- sectionHeader: css`
- color: ${theme.colors.textWeak};
- margin-bottom: ${theme.spacing.d};
- `,
- image: css`
- width: 17px;
- margin-right: ${theme.spacing.xxs};
- `,
- title: css`
- margin-right: calc(${theme.spacing.d} / 3);
- `,
- version: css`
- font-size: ${theme.typography.size.sm};
- color: ${theme.colors.textWeak};
- `,
- item: css`
- display: flex;
- justify-content: flex-start;
- align-items: center;
- cursor: pointer;
- margin: ${theme.spacing.xxs};
- padding: ${theme.spacing.sm};
- background: ${theme.colors.dashboardBg};
- border-radius: ${theme.border.radius.md};
- `,
- message: css`
- margin-left: auto;
- font-size: ${theme.typography.size.sm};
- `,
- messageEnable: css`
- color: ${theme.colors.linkExternal};
- &:hover {
- border-bottom: ${theme.border.width.sm} solid ${theme.colors.linkExternal};
- }
- `,
- messageUpdate: css`
- &:hover {
- border-bottom: ${theme.border.width.sm} solid ${theme.colors.text};
- }
- `,
- messageNoUpdate: css`
- color: ${theme.colors.textWeak};
- `,
- noneInstalled: css`
- color: ${theme.colors.textWeak};
- font-size: ${theme.typography.size.sm};
- `,
- emphasis: css`
- font-weight: ${theme.typography.weight.semibold};
- font-style: normal;
- color: ${theme.colors.textWeak};
- `,
-}));
diff --git a/public/app/plugins/panel/pluginlist/README.md b/public/app/plugins/panel/pluginlist/README.md
deleted file mode 100644
index 2e0d851d7e5..00000000000
--- a/public/app/plugins/panel/pluginlist/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Plugin List Panel - Native Plugin
-
-The Plugin List plans shows the installed plugins for your Grafana instance and is **included** with Grafana. It is used on the default Home dashboard.
diff --git a/public/app/plugins/panel/pluginlist/components/UpdatePluginModal.tsx b/public/app/plugins/panel/pluginlist/components/UpdatePluginModal.tsx
deleted file mode 100644
index b48d99529eb..00000000000
--- a/public/app/plugins/panel/pluginlist/components/UpdatePluginModal.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import { Modal, stylesFactory, useStyles } from '@grafana/ui';
-import { GrafanaTheme } from '@grafana/data';
-import { css } from '@emotion/css';
-
-interface Props {
- pluginName: string;
- pluginID: string;
- onConfirm?: () => void;
- onDismiss?: () => void;
-}
-
-export function UpdatePluginModal({ pluginName, pluginID, onDismiss }: Props) {
- const styles = useStyles(getStyles);
-
- return (
-
-
-
Type the following on the command line to update {pluginName}.
-
- grafana-cli plugins update {pluginID}
-
-
- Check out {pluginName} on Grafana.com for README and
- changelog. If you do not have access to the command line, ask your Grafana administator.
-
-
-
-
- Pro tip : To update all plugins at once, type{' '}
- grafana-cli plugins update-all on the command line.
-
-
- );
-}
-
-const getStyles = stylesFactory((theme: GrafanaTheme) => ({
- small: css`
- font-size: ${theme.typography.size.sm};
- font-weight: ${theme.typography.weight.regular};
- `,
- codeSmall: css`
- font-size: ${theme.typography.size.xs};
- padding: ${theme.spacing.xxs};
- margin: 0 ${theme.spacing.xxs};
- `,
- container: css`
- margin-bottom: calc(${theme.spacing.d} * 2.5);
- `,
- updateAllTip: css`
- color: ${theme.colors.textWeak};
- font-size: ${theme.typography.size.sm};
- `,
- inlineLogo: css`
- vertical-align: sub;
- margin-right: calc(${theme.spacing.d} / 3);
- width: ${theme.spacing.md};
- `,
-}));
diff --git a/public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg b/public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg
deleted file mode 100644
index bdc6c33039c..00000000000
--- a/public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/app/plugins/panel/pluginlist/module.tsx b/public/app/plugins/panel/pluginlist/module.tsx
deleted file mode 100644
index 86f97e04b5d..00000000000
--- a/public/app/plugins/panel/pluginlist/module.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { PanelPlugin } from '@grafana/data';
-import { PluginList } from './PluginList';
-
-export const plugin = new PanelPlugin(PluginList);
diff --git a/public/app/plugins/panel/pluginlist/plugin.json b/public/app/plugins/panel/pluginlist/plugin.json
deleted file mode 100644
index c2faa3b6dd7..00000000000
--- a/public/app/plugins/panel/pluginlist/plugin.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "type": "panel",
- "name": "Plugin list",
- "id": "pluginlist",
-
- "skipDataQuery": true,
-
- "info": {
- "description": "Plugin List for Grafana",
- "author": {
- "name": "Grafana Labs",
- "url": "https://grafana.com"
- },
- "logos": {
- "small": "img/icn-dashlist-panel.svg",
- "large": "img/icn-dashlist-panel.svg"
- }
- }
-}
From ca286a238d1e9ce1e5357fde8148476a9d618cee Mon Sep 17 00:00:00 2001
From: Giordano Ricci
Date: Thu, 7 Apr 2022 09:26:01 +0100
Subject: [PATCH 16/55] Explore: Add explore2Dashboard feature toggle (#47395)
---
.../grafana-data/src/types/featureToggles.gen.ts | 1 +
pkg/services/featuremgmt/registry.go | 6 ++++++
pkg/services/featuremgmt/toggles_gen.go | 4 ++++
public/app/features/explore/ExploreToolbar.tsx | 15 +++++++++++----
4 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index dcd04ac309e..dc9564e190e 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -52,4 +52,5 @@ export interface FeatureToggles {
alertProvisioning?: boolean;
storageLocalUpload?: boolean;
azureMonitorResourcePickerForMetrics?: boolean;
+ explore2Dashboard?: boolean;
}
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index a55232ecbf3..af7f1baf424 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -201,5 +201,11 @@ var (
RequiresDevMode: true,
FrontendOnly: true,
},
+ {
+ Name: "explore2Dashboard",
+ Description: "Experimental Explore to Dashboard workflow",
+ State: FeatureStateBeta,
+ FrontendOnly: true,
+ },
}
)
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 06c470a7a72..078ecbda7e7 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -150,4 +150,8 @@ const (
// FlagAzureMonitorResourcePickerForMetrics
// New UI for Azure Monitor Metrics Query
FlagAzureMonitorResourcePickerForMetrics = "azureMonitorResourcePickerForMetrics"
+
+ // FlagExplore2Dashboard
+ // Experimental Explore to Dashboard workflow
+ FlagExplore2Dashboard = "explore2Dashboard"
)
diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx
index f38824de47f..af8030b01e9 100644
--- a/public/app/features/explore/ExploreToolbar.tsx
+++ b/public/app/features/explore/ExploreToolbar.tsx
@@ -1,9 +1,9 @@
-import React, { PureComponent, RefObject } from 'react';
+import React, { lazy, PureComponent, RefObject, Suspense } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { ExploreId } from 'app/types/explore';
import { PageToolbar, SetInterval, ToolbarButton, ToolbarButtonRow } from '@grafana/ui';
import { DataSourceInstanceSettings, RawTimeRange } from '@grafana/data';
-import { DataSourcePicker } from '@grafana/runtime';
+import { config, DataSourcePicker } from '@grafana/runtime';
import { StoreState } from 'app/types/store';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { changeDatasource } from './state/datasource';
@@ -18,7 +18,10 @@ import { LiveTailControls } from './useLiveTailControls';
import { cancelQueries, runQueries } from './state/query';
import { isSplit } from './state/selectors';
import { DashNavButton } from '../dashboard/components/DashNav/DashNavButton';
-import { AddToDashboard } from './AddToDashboard';
+
+const AddToDashboard = lazy(() =>
+ import('./AddToDashboard').then(({ AddToDashboard }) => ({ default: AddToDashboard }))
+);
interface OwnProps {
exploreId: ExploreId;
@@ -116,7 +119,11 @@ class UnConnectedExploreToolbar extends PureComponent {
)}
-
+ {config.featureToggles.explore2Dashboard && (
+
+
+
+ )}
{!isLive && (
Date: Thu, 7 Apr 2022 09:39:54 +0100
Subject: [PATCH 17/55] Library panels: Fix issue where query editor options
wouldn't be updated (#47242)
Closes #47241
---
.../components/PanelEditor/PanelEditor.tsx | 8 +++++++-
public/app/features/panel/state/actions.ts | 8 +++++++-
public/app/features/panel/state/reducers.ts | 13 +++++++++++--
3 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx
index 7939e89f415..9f61c323bf6 100644
--- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx
+++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx
@@ -285,7 +285,13 @@ export class PanelEditorUnconnected extends PureComponent {
aria-label={selectors.components.PanelEditor.DataPane.content}
key="panel-editor-tabs"
>
-
+
,
];
}
diff --git a/public/app/features/panel/state/actions.ts b/public/app/features/panel/state/actions.ts
index 84e12c00199..f3c9d51fa9e 100644
--- a/public/app/features/panel/state/actions.ts
+++ b/public/app/features/panel/state/actions.ts
@@ -2,7 +2,7 @@ import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPlugi
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { loadPanelPlugin } from 'app/features/plugins/admin/state/actions';
import { ThunkResult } from 'app/types';
-import { panelModelAndPluginReady } from './reducers';
+import { changePanelKey, panelModelAndPluginReady } from './reducers';
import { LibraryElementDTO } from 'app/features/library-panels/types';
import { toPanelModelLibraryPanel } from 'app/features/library-panels/utils';
import { PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events';
@@ -114,6 +114,12 @@ export function changeToLibraryPanel(panel: PanelModel, libraryPanel: LibraryEle
panel.generateNewKey();
await dispatch(panelModelAndPluginReady({ key: panel.key, plugin, cleanUpKey: oldKey }));
+ } else {
+ // Even if the plugin is the same, we want to change the key
+ // to force a rerender
+ const oldKey = panel.key;
+ panel.generateNewKey();
+ dispatch(changePanelKey({ oldKey, newKey: panel.key }));
}
panel.configRev = 0;
diff --git a/public/app/features/panel/state/reducers.ts b/public/app/features/panel/state/reducers.ts
index 164f0e46610..4fe2f8dce70 100644
--- a/public/app/features/panel/state/reducers.ts
+++ b/public/app/features/panel/state/reducers.ts
@@ -26,6 +26,10 @@ const panelsSlice = createSlice({
plugin: action.payload.plugin,
};
},
+ changePanelKey: (state, action: PayloadAction<{ oldKey: string; newKey: string }>) => {
+ state[action.payload.newKey] = state[action.payload.oldKey];
+ delete state[action.payload.oldKey];
+ },
cleanUpPanelState: (state, action: PayloadAction<{ key: string }>) => {
cleanUpAngularComponent(state[action.payload.key]);
delete state[action.payload.key];
@@ -64,8 +68,13 @@ export interface SetPanelInstanceStatePayload {
value: any;
}
-export const { panelModelAndPluginReady, setPanelAngularComponent, setPanelInstanceState, cleanUpPanelState } =
- panelsSlice.actions;
+export const {
+ panelModelAndPluginReady,
+ setPanelAngularComponent,
+ setPanelInstanceState,
+ cleanUpPanelState,
+ changePanelKey,
+} = panelsSlice.actions;
export const panelsReducer = panelsSlice.reducer;
From c3fe027d5844b5f720ae65b0beb33109733c7400 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 7 Apr 2022 11:27:58 +0200
Subject: [PATCH 18/55] Update dependency @reduxjs/toolkit to v1.8.0 (#45950)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Update dependency @reduxjs/toolkit to v1.8.0
* Fix type errors
* simplified code
Co-authored-by: Renovate Bot
Co-authored-by: kay delaney
Co-authored-by: Piotr Jamróz
Co-authored-by: Gábor Farkas
---
package.json | 2 +-
public/app/features/explore/spec/helper/setup.tsx | 3 +--
public/app/store/configureStore.ts | 10 +++-------
public/test/core/redux/reduxTester.ts | 2 +-
yarn.lock | 10 +++++-----
5 files changed, 11 insertions(+), 16 deletions(-)
diff --git a/package.json b/package.json
index 74ddc4c7a4d..e3b8ddcaad1 100644
--- a/package.json
+++ b/package.json
@@ -276,7 +276,7 @@
"@react-stately/collections": "3.3.7",
"@react-stately/menu": "3.2.6",
"@react-stately/tree": "3.2.3",
- "@reduxjs/toolkit": "1.7.2",
+ "@reduxjs/toolkit": "1.8.0",
"@sentry/browser": "6.19.1",
"@sentry/types": "6.19.1",
"@sentry/utils": "6.19.1",
diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx
index 2d3b243c8a2..ef2930b6717 100644
--- a/public/app/features/explore/spec/helper/setup.tsx
+++ b/public/app/features/explore/spec/helper/setup.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { within } from '@testing-library/dom';
-import { EnhancedStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import { Route, Router } from 'react-router-dom';
import { fromPairs } from 'lodash';
@@ -31,7 +30,7 @@ type SetupOptions = {
export function setupExplore(options?: SetupOptions): {
datasources: { [name: string]: DataSourceApi };
- store: EnhancedStore;
+ store: ReturnType;
unmount: () => void;
container: HTMLElement;
} {
diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts
index 851c4d883e3..03275629e9b 100644
--- a/public/app/store/configureStore.ts
+++ b/public/app/store/configureStore.ts
@@ -1,10 +1,10 @@
-import { configureStore as reduxConfigureStore } from '@reduxjs/toolkit';
+import { configureStore as reduxConfigureStore, MiddlewareArray } from '@reduxjs/toolkit';
import { setStore } from './store';
import { StoreState } from 'app/types/store';
import { addReducer, createRootReducer } from '../core/reducers/root';
import { buildInitialState } from '../core/reducers/navModel';
-import { ThunkMiddlewareFor } from '@reduxjs/toolkit/src/getDefaultMiddleware';
import { AnyAction } from 'redux';
+import { ThunkMiddleware } from 'redux-thunk';
export function addRootReducer(reducers: any) {
// this is ok now because we add reducers before configureStore is called
@@ -14,11 +14,7 @@ export function addRootReducer(reducers: any) {
}
export function configureStore(initialState?: Partial) {
- const store = reduxConfigureStore<
- StoreState,
- AnyAction,
- ReadonlyArray>
- >({
+ const store = reduxConfigureStore]>>({
reducer: createRootReducer(),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }),
diff --git a/public/test/core/redux/reduxTester.ts b/public/test/core/redux/reduxTester.ts
index cd334b7d4c8..c730b6f96a6 100644
--- a/public/test/core/redux/reduxTester.ts
+++ b/public/test/core/redux/reduxTester.ts
@@ -56,7 +56,7 @@ export const reduxTester = (args?: ReduxTesterArguments): ReduxTes
const preloadedState = args?.preloadedState ?? ({} as unknown as PreloadedState>>);
const debug = args?.debug ?? false;
- let store: EnhancedStore | null = null;
+ let store: EnhancedStore | null = null;
const defaultMiddleware = getDefaultMiddleware({
thunk: false,
diff --git a/yarn.lock b/yarn.lock
index ace1dddad49..98530e7faef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7219,9 +7219,9 @@ __metadata:
languageName: node
linkType: hard
-"@reduxjs/toolkit@npm:1.7.2":
- version: 1.7.2
- resolution: "@reduxjs/toolkit@npm:1.7.2"
+"@reduxjs/toolkit@npm:1.8.0":
+ version: 1.8.0
+ resolution: "@reduxjs/toolkit@npm:1.8.0"
dependencies:
immer: ^9.0.7
redux: ^4.1.2
@@ -7235,7 +7235,7 @@ __metadata:
optional: true
react-redux:
optional: true
- checksum: 41c17c660f7c95d776658b9a101f9803c37589b2c08861e90283abe5fab70f99c37af064fae0c5b7501f67308946fa072504f6a801e272f157b9f7427a5e571e
+ checksum: e229571b80d7f5c4407af2050ac2a26a22472541fef1705d5e8b73b93bfb2884072d79e1cea2cd81d89939cc6b6073ef3ddf1680950aac66ba34cd202a6cf371
languageName: node
linkType: hard
@@ -20454,7 +20454,7 @@ __metadata:
"@react-types/menu": 3.5.2
"@react-types/overlays": 3.5.4
"@react-types/shared": 3.11.2
- "@reduxjs/toolkit": 1.7.2
+ "@reduxjs/toolkit": 1.8.0
"@rtsao/plugin-proposal-class-properties": 7.0.1-patch.1
"@sentry/browser": 6.19.1
"@sentry/types": 6.19.1
From 41642fd5f4e1b13733101aecadbb99068836400b Mon Sep 17 00:00:00 2001
From: Josh Hunt
Date: Thu, 7 Apr 2022 10:59:49 +0100
Subject: [PATCH 19/55] Modify the Server Error scenario to return frontend
errors (#47157)
---
pkg/tsdb/testdatasource/scenarios.go | 23 +++++++++---
.../datasource/testdata/QueryEditor.tsx | 5 +++
.../testdata/components/ErrorEditor.tsx | 36 +++++++++++++++++++
.../plugins/datasource/testdata/datasource.ts | 31 +++++++++++++++-
.../app/plugins/datasource/testdata/types.ts | 2 ++
5 files changed, 92 insertions(+), 5 deletions(-)
create mode 100644 public/app/plugins/datasource/testdata/components/ErrorEditor.tsx
diff --git a/pkg/tsdb/testdatasource/scenarios.go b/pkg/tsdb/testdatasource/scenarios.go
index 1c4e5b4d26f..e49d760cef2 100644
--- a/pkg/tsdb/testdatasource/scenarios.go
+++ b/pkg/tsdb/testdatasource/scenarios.go
@@ -177,9 +177,12 @@ Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means
})
s.registerScenario(&Scenario{
- ID: string(serverError500Query),
- Name: "Server Error (500)",
- handler: s.handleServerError500Scenario,
+ // Is no longer strictly a _server_ error scenario, but ID is kept for legacy :)
+ ID: string(serverError500Query),
+ Name: "Conditional Error",
+ handler: s.handleServerError500Scenario,
+ StringInput: "1,20,90,30,5,0",
+ Description: "Returns an error when the String Input field is empty",
})
s.registerScenario(&Scenario{
@@ -449,7 +452,19 @@ func (s *Service) handlePredictablePulseScenario(ctx context.Context, req *backe
}
func (s *Service) handleServerError500Scenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
- panic("Test Data Panic!")
+ for _, q := range req.Queries {
+ model, err := simplejson.NewJson(q.JSON)
+ if err != nil {
+ continue
+ }
+
+ stringInput := model.Get("stringInput").MustString()
+ if stringInput == "" {
+ panic("Test Data Panic!")
+ }
+ }
+
+ return s.handleCSVMetricValuesScenario(ctx, req)
}
func (s *Service) handleClientSideScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx
index 1c762c22730..ae68e35ffb8 100644
--- a/public/app/plugins/datasource/testdata/QueryEditor.tsx
+++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx
@@ -21,6 +21,7 @@ import { defaultStreamQuery } from './runStreams';
import { CSVFileEditor } from './components/CSVFileEditor';
import { CSVContentEditor } from './components/CSVContentEditor';
import { USAQueryEditor, usaQueryModes } from './components/USAQueryEditor';
+import ErrorEditor from './components/ErrorEditor';
const showLabelsFor = ['random_walk', 'predictable_pulse'];
const endpoints = [
@@ -69,6 +70,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
[scenarioList, query]
);
const scenarioId = currentScenario?.id;
+ const description = currentScenario?.description;
const onScenarioChange = (item: SelectableValue) => {
const scenario = scenarioList?.find((sc) => sc.id === item.value);
@@ -287,6 +289,9 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
{scenarioId === 'node_graph' && (
onChange({ ...query, nodes: val })} query={query} />
)}
+ {scenarioId === 'server_error_500' && }
+
+ {description && {description}
}
>
);
};
diff --git a/public/app/plugins/datasource/testdata/components/ErrorEditor.tsx b/public/app/plugins/datasource/testdata/components/ErrorEditor.tsx
new file mode 100644
index 00000000000..a8447118a88
--- /dev/null
+++ b/public/app/plugins/datasource/testdata/components/ErrorEditor.tsx
@@ -0,0 +1,36 @@
+import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
+import React from 'react';
+import { EditorProps } from '../QueryEditor';
+
+const ERROR_OPTIONS = [
+ {
+ label: 'Server panic',
+ value: 'server_panic',
+ },
+ {
+ label: 'Frontend exception',
+ value: 'frontend_exception',
+ },
+ {
+ label: 'Frontend observable',
+ value: 'frontend_observable',
+ },
+];
+
+const FrontendErrorQueryEditor: React.FC = ({ query, onChange }) => {
+ return (
+
+
+ {
+ onChange({ ...query, errorType: v.value });
+ }}
+ />
+
+
+ );
+};
+
+export default FrontendErrorQueryEditor;
diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts
index 09b8cc133b8..a92f578133f 100644
--- a/public/app/plugins/datasource/testdata/datasource.ts
+++ b/public/app/plugins/datasource/testdata/datasource.ts
@@ -1,4 +1,4 @@
-import { from, merge, Observable, of } from 'rxjs';
+import { from, merge, Observable, of, throwError } from 'rxjs';
import { delay } from 'rxjs/operators';
import {
@@ -68,6 +68,12 @@ export class TestDataDataSource extends DataSourceWithBackend {
case 'raw_frame':
streams.push(this.rawFrameQuery(target, options));
break;
+ case 'server_error_500':
+ // this now has an option where it can return/throw an error from the frontend.
+ // if it doesn't, send it to the backend where it might panic there :)
+ const query = this.serverErrorQuery(target, options);
+ query ? streams.push(query) : backendQueries.push(target);
+ break;
// Unusable since 7, removed in 8
case 'manual_entry': {
let csvContent = 'Time,Value\n';
@@ -202,6 +208,29 @@ export class TestDataDataSource extends DataSourceWithBackend {
return of({ data: [], error: ex }).pipe(delay(100));
}
}
+
+ serverErrorQuery(
+ target: TestDataQuery,
+ options: DataQueryRequest
+ ): Observable | null {
+ const { errorType } = target;
+ console.log("we're here!", target);
+
+ if (errorType === 'server_panic') {
+ return null;
+ }
+
+ const stringInput = target.stringInput ?? '';
+ if (stringInput === '') {
+ if (errorType === 'frontend_exception') {
+ throw new Error('Scenario threw an exception in the frontend because the input was empty.');
+ } else {
+ return throwError(() => new Error('Scenario returned an error because the input was empty.'));
+ }
+ }
+
+ return null;
+ }
}
function runGrafanaAPI(target: TestDataQuery, req: DataQueryRequest): Observable {
diff --git a/public/app/plugins/datasource/testdata/types.ts b/public/app/plugins/datasource/testdata/types.ts
index f34442fa571..0f23a903de6 100644
--- a/public/app/plugins/datasource/testdata/types.ts
+++ b/public/app/plugins/datasource/testdata/types.ts
@@ -4,6 +4,7 @@ export interface Scenario {
id: string;
name: string;
stringInput: string;
+ description?: string;
}
export interface TestDataQuery extends DataQuery {
@@ -22,6 +23,7 @@ export interface TestDataQuery extends DataQuery {
csvContent?: string;
rawFrameContent?: string;
usa?: USAQuery;
+ errorType?: 'server_panic' | 'frontend_exception' | 'frontend_observable';
}
export interface NodesQuery {
From af71e77e6ce013c2959d2baea1b129c8467133c2 Mon Sep 17 00:00:00 2001
From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com>
Date: Thu, 7 Apr 2022 04:38:38 -0700
Subject: [PATCH 20/55] ReleaseNotes: Updated changelog and release notes for
8.5.0-beta1 (#47433)
* ReleaseNotes: Updated changelog and release notes for 8.5.0-beta1
* Update release notes
* Update changelog
Co-authored-by: Tania
---
CHANGELOG.md | 27 +++++++++++++++++
docs/sources/release-notes/_index.md | 1 +
.../release-notes-8-5-0-beta1.md | 30 +++++++++++++++++++
3 files changed, 58 insertions(+)
create mode 100644 docs/sources/release-notes/release-notes-8-5-0-beta1.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7f97bec4e9..db42f22ca94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,30 @@
+
+
+# 8.5.0-beta1 (2022-04-06)
+
+### Features and enhancements
+
+- Add config option to enable/disable reporting. (Enterprise)
+- **Alerting:** Accurately set value for prom-compatible APIs. [#47216](https://github.com/grafana/grafana/pull/47216), [@gotjosh](https://github.com/gotjosh)
+- **Alerting:** Provisioning API - Notification Policies. [#46755](https://github.com/grafana/grafana/pull/46755), [@alexweav](https://github.com/alexweav)
+- **Analytics:** Enable grafana and plugin update checks to be operated independently. [#46352](https://github.com/grafana/grafana/pull/46352), [@wbrowne](https://github.com/wbrowne)
+- **Azure Monitor:** Add support for multiple template variables in resource picker. [#46215](https://github.com/grafana/grafana/pull/46215), [@sarahzinger](https://github.com/sarahzinger)
+- **Caching:** Add separate TTL for resources cache. (Enterprise)
+- **Caching:** add support for TLS configuration for Redis Cluster. (Enterprise)
+- **NewsPanel:** Remove Use Proxy option and update documentation with recommendations. [#47189](https://github.com/grafana/grafana/pull/47189), [@joshhunt](https://github.com/joshhunt)
+- **OAuth:** Sync GitHub OAuth user name to Grafana if it's set. [#45438](https://github.com/grafana/grafana/pull/45438), [@pallxk](https://github.com/pallxk)
+
+### Bug fixes
+
+- **Plugins:** Fix Default Nav URL for dashboard includes. [#47143](https://github.com/grafana/grafana/pull/47143), [@wbrowne](https://github.com/wbrowne)
+
+### Breaking changes
+
+When user is using Github OAuth, GitHub login is showed as both Grafana login and name. Now the GitHub name is showed as Grafana name, and GitHub login is showed as Grafana Login. Issue [#45438](https://github.com/grafana/grafana/issues/45438)
+
+The meaning of the default data source has now changed from being a persisted property in a panel. Before when you selected the default data source for a panel and later changed the default data source to another data source it would change all panels who were configured to use the default data source. From now on the default data source is just the default for new panels and changing the default will not impact any currently saved dashboards. Issue [#45132](https://github.com/grafana/grafana/issues/45132)
+
+
# 8.4.5 (2022-03-31)
diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md
index 7eb143ae827..0c8f2f85b44 100644
--- a/docs/sources/release-notes/_index.md
+++ b/docs/sources/release-notes/_index.md
@@ -8,6 +8,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
+- [Release notes for 8.5.0-beta1]({{< relref "release-notes-8-5-0-beta1" >}})
- [Release notes for 8.4.5]({{< relref "release-notes-8-4-5" >}})
- [Release notes for 8.4.4]({{< relref "release-notes-8-4-4" >}})
- [Release notes for 8.4.3]({{< relref "release-notes-8-4-3" >}})
diff --git a/docs/sources/release-notes/release-notes-8-5-0-beta1.md b/docs/sources/release-notes/release-notes-8-5-0-beta1.md
new file mode 100644
index 00000000000..24d703a0725
--- /dev/null
+++ b/docs/sources/release-notes/release-notes-8-5-0-beta1.md
@@ -0,0 +1,30 @@
++++
+title = "Release notes for Grafana 8.5.0-beta1"
+hide_menu = true
++++
+
+
+
+# Release notes for Grafana 8.5.0-beta1
+
+### Features and enhancements
+
+- Add config option to enable/disable reporting. (Enterprise)
+- **Alerting:** Accurately set value for prom-compatible APIs. [#47216](https://github.com/grafana/grafana/pull/47216), [@gotjosh](https://github.com/gotjosh)
+- **Alerting:** Provisioning API - Notification Policies. [#46755](https://github.com/grafana/grafana/pull/46755), [@alexweav](https://github.com/alexweav)
+- **Analytics:** Enable grafana and plugin update checks to be operated independently. [#46352](https://github.com/grafana/grafana/pull/46352), [@wbrowne](https://github.com/wbrowne)
+- **Azure Monitor:** Add support for multiple template variables in resource picker. [#46215](https://github.com/grafana/grafana/pull/46215), [@sarahzinger](https://github.com/sarahzinger)
+- **Caching:** Add separate TTL for resources cache. (Enterprise)
+- **Caching:** add support for TLS configuration for Redis Cluster. (Enterprise)
+- **NewsPanel:** Remove Use Proxy option and update documentation with recommendations. [#47189](https://github.com/grafana/grafana/pull/47189), [@joshhunt](https://github.com/joshhunt)
+- **OAuth:** Sync GitHub OAuth user name to Grafana if it's set. [#45438](https://github.com/grafana/grafana/pull/45438), [@pallxk](https://github.com/pallxk)
+
+### Bug fixes
+
+- **Plugins:** Fix Default Nav URL for dashboard includes. [#47143](https://github.com/grafana/grafana/pull/47143), [@wbrowne](https://github.com/wbrowne)
+
+### Breaking changes
+
+When user is using Github OAuth, GitHub login is showed as both Grafana login and name. Now the GitHub name is showed as Grafana name, and GitHub login is showed as Grafana Login. Issue [#45438](https://github.com/grafana/grafana/issues/45438)
+
+The meaning of the default data source has now changed from being a persisted property in a panel. Before when you selected the default data source for a panel and later changed the default data source to another data source it would change all panels who were configured to use the default data source. From now on the default data source is just the default for new panels and changing the default will not impact any currently saved dashboards. Issue [#45132](https://github.com/grafana/grafana/issues/45132)
From d95782845d819b54131aa11f0d1343b1928cd290 Mon Sep 17 00:00:00 2001
From: Ashley Harrison
Date: Thu, 7 Apr 2022 13:14:01 +0100
Subject: [PATCH 21/55] Navigation: add aria-label to NavBarToggle (#47434)
* add aria-label to NavBarToggle
* use open/close instead
---
public/app/core/components/NavBar/Next/NavBarToggle.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/app/core/components/NavBar/Next/NavBarToggle.tsx b/public/app/core/components/NavBar/Next/NavBarToggle.tsx
index fc830d0fe0f..617958cc046 100644
--- a/public/app/core/components/NavBar/Next/NavBarToggle.tsx
+++ b/public/app/core/components/NavBar/Next/NavBarToggle.tsx
@@ -16,6 +16,7 @@ export const NavBarToggle = ({ className, isExpanded, onClick }: Props) => {
return (
Date: Thu, 7 Apr 2022 14:14:18 +0200
Subject: [PATCH 22/55] Update latest json (#47437)
---
latest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/latest.json b/latest.json
index 68649300d2e..f1c9e890e90 100644
--- a/latest.json
+++ b/latest.json
@@ -1,4 +1,4 @@
{
"stable": "8.4.5",
- "testing": "8.4.5"
+ "testing": "8.5.0-beta1"
}
From 1c13bfdd1b810fdfd19b1ff879ceed5ab4512ce4 Mon Sep 17 00:00:00 2001
From: Tania
Date: Thu, 7 Apr 2022 14:32:37 +0200
Subject: [PATCH 23/55] Chore: Update pre version (#47438)
---
lerna.json | 2 +-
package.json | 2 +-
packages/grafana-data/package.json | 4 +-
packages/grafana-e2e-selectors/package.json | 2 +-
packages/grafana-e2e/package.json | 4 +-
packages/grafana-runtime/package.json | 8 ++--
packages/grafana-schema/package.json | 2 +-
packages/grafana-toolkit/package.json | 6 +--
packages/grafana-ui/package.json | 8 ++--
packages/jaeger-ui-components/package.json | 6 +--
.../internal/input-datasource/package.json | 8 ++--
yarn.lock | 40 +++++++++----------
12 files changed, 46 insertions(+), 46 deletions(-)
diff --git a/lerna.json b/lerna.json
index a4f8c6aa934..9a6406e5399 100644
--- a/lerna.json
+++ b/lerna.json
@@ -2,5 +2,5 @@
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
- "version": "8.5.0-pre"
+ "version": "9.0.0-pre"
}
diff --git a/package.json b/package.json
index e3b8ddcaad1..2a2b2de50d9 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"repository": "github:grafana/grafana",
"scripts": {
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json
index bda9b762e7e..d8838af5cd3 100644
--- a/packages/grafana-data/package.json
+++ b/packages/grafana-data/package.json
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -22,7 +22,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "6.0.0",
- "@grafana/schema": "8.5.0-pre",
+ "@grafana/schema": "9.0.0-pre",
"@types/d3-interpolate": "^1.4.0",
"d3-interpolate": "1.4.0",
"date-fns": "2.28.0",
diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json
index 571ea8dd91d..776d09e5500 100644
--- a/packages/grafana-e2e-selectors/package.json
+++ b/packages/grafana-e2e-selectors/package.json
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",
diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json
index 10aeec505b3..9f892d541c6 100644
--- a/packages/grafana-e2e/package.json
+++ b/packages/grafana-e2e/package.json
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -48,7 +48,7 @@
"@babel/core": "7.17.8",
"@babel/preset-env": "7.16.11",
"@cypress/webpack-preprocessor": "5.11.1",
- "@grafana/e2e-selectors": "8.5.0-pre",
+ "@grafana/e2e-selectors": "9.0.0-pre",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "8.2.4",
diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json
index 26039a58f38..55f9ffa0ff2 100644
--- a/packages/grafana-runtime/package.json
+++ b/packages/grafana-runtime/package.json
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -22,9 +22,9 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@grafana/data": "8.5.0-pre",
- "@grafana/e2e-selectors": "8.5.0-pre",
- "@grafana/ui": "8.5.0-pre",
+ "@grafana/data": "9.0.0-pre",
+ "@grafana/e2e-selectors": "9.0.0-pre",
+ "@grafana/ui": "9.0.0-pre",
"@sentry/browser": "6.19.1",
"history": "4.10.1",
"lodash": "4.17.21",
diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json
index 30654d6f4fa..16db7c725b7 100644
--- a/packages/grafana-schema/package.json
+++ b/packages/grafana-schema/package.json
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"description": "Grafana Schema Library",
"keywords": [
"typescript"
diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json
index d5c458cca80..467a23cf417 100644
--- a/packages/grafana-toolkit/package.json
+++ b/packages/grafana-toolkit/package.json
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -28,10 +28,10 @@
"dependencies": {
"@babel/core": "7.13.14",
"@babel/preset-env": "7.13.12",
- "@grafana/data": "8.5.0-pre",
+ "@grafana/data": "9.0.0-pre",
"@grafana/eslint-config": "2.5.2",
"@grafana/tsconfig": "^1.2.0-rc1",
- "@grafana/ui": "8.5.0-pre",
+ "@grafana/ui": "9.0.0-pre",
"@jest/core": "26.6.3",
"@rushstack/eslint-patch": "1.0.6",
"@types/command-exists": "^1.2.0",
diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json
index b10a5ba1d76..7238cf849fc 100644
--- a/packages/grafana-ui/package.json
+++ b/packages/grafana-ui/package.json
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -33,9 +33,9 @@
"@emotion/css": "11.7.1",
"@emotion/react": "11.8.2",
"@grafana/aws-sdk": "0.0.35",
- "@grafana/data": "8.5.0-pre",
- "@grafana/e2e-selectors": "8.5.0-pre",
- "@grafana/schema": "8.5.0-pre",
+ "@grafana/data": "9.0.0-pre",
+ "@grafana/e2e-selectors": "9.0.0-pre",
+ "@grafana/schema": "9.0.0-pre",
"@grafana/slate-react": "0.22.10-grafana",
"@monaco-editor/react": "4.3.1",
"@popperjs/core": "2.11.4",
diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json
index 992888ce8ad..b69f93aad83 100644
--- a/packages/jaeger-ui-components/package.json
+++ b/packages/jaeger-ui-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@jaegertracing/jaeger-ui-components",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@emotion/css": "11.7.1",
- "@grafana/data": "8.5.0-pre",
- "@grafana/ui": "8.5.0-pre",
+ "@grafana/data": "9.0.0-pre",
+ "@grafana/ui": "9.0.0-pre",
"chance": "^1.0.10",
"classnames": "^2.2.5",
"combokeys": "^3.0.0",
diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json
index ce4d3ebc8aa..df76caba401 100644
--- a/plugins-bundled/internal/input-datasource/package.json
+++ b/plugins-bundled/internal/input-datasource/package.json
@@ -1,6 +1,6 @@
{
"name": "@grafana-plugins/input-datasource",
- "version": "8.5.0-pre",
+ "version": "9.0.0-pre",
"description": "Input Datasource",
"private": true,
"repository": {
@@ -24,9 +24,9 @@
"webpack": "5.58.1"
},
"dependencies": {
- "@grafana/data": "8.5.0-pre",
- "@grafana/toolkit": "8.5.0-pre",
- "@grafana/ui": "8.5.0-pre",
+ "@grafana/data": "9.0.0-pre",
+ "@grafana/toolkit": "9.0.0-pre",
+ "@grafana/ui": "9.0.0-pre",
"jquery": "3.5.1",
"react": "17.0.1",
"react-dom": "17.0.1",
diff --git a/yarn.lock b/yarn.lock
index 98530e7faef..4590dfba6d7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3939,9 +3939,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource"
dependencies:
- "@grafana/data": 8.5.0-pre
- "@grafana/toolkit": 8.5.0-pre
- "@grafana/ui": 8.5.0-pre
+ "@grafana/data": 9.0.0-pre
+ "@grafana/toolkit": 9.0.0-pre
+ "@grafana/ui": 9.0.0-pre
"@types/jest": 26.0.15
"@types/lodash": 4.14.149
"@types/react": 17.0.30
@@ -3982,12 +3982,12 @@ __metadata:
languageName: node
linkType: hard
-"@grafana/data@8.5.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data":
+"@grafana/data@9.0.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data":
version: 0.0.0-use.local
resolution: "@grafana/data@workspace:packages/grafana-data"
dependencies:
"@braintree/sanitize-url": 6.0.0
- "@grafana/schema": 8.5.0-pre
+ "@grafana/schema": 9.0.0-pre
"@grafana/tsconfig": ^1.2.0-rc1
"@rollup/plugin-commonjs": 21.0.2
"@rollup/plugin-json": 4.1.0
@@ -4040,7 +4040,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@grafana/e2e-selectors@8.5.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors":
+"@grafana/e2e-selectors@9.0.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors":
version: 0.0.0-use.local
resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors"
dependencies:
@@ -4064,7 +4064,7 @@ __metadata:
"@babel/core": 7.17.8
"@babel/preset-env": 7.16.11
"@cypress/webpack-preprocessor": 5.11.1
- "@grafana/e2e-selectors": 8.5.0-pre
+ "@grafana/e2e-selectors": 9.0.0-pre
"@grafana/tsconfig": ^1.2.0-rc1
"@mochajs/json-file-reporter": ^1.2.0
"@rollup/plugin-commonjs": 21.0.2
@@ -4171,10 +4171,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "@grafana/runtime@workspace:packages/grafana-runtime"
dependencies:
- "@grafana/data": 8.5.0-pre
- "@grafana/e2e-selectors": 8.5.0-pre
+ "@grafana/data": 9.0.0-pre
+ "@grafana/e2e-selectors": 9.0.0-pre
"@grafana/tsconfig": ^1.2.0-rc1
- "@grafana/ui": 8.5.0-pre
+ "@grafana/ui": 9.0.0-pre
"@rollup/plugin-commonjs": 21.0.2
"@rollup/plugin-node-resolve": 13.1.3
"@sentry/browser": 6.19.1
@@ -4203,7 +4203,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@grafana/schema@8.5.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
+"@grafana/schema@9.0.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
version: 0.0.0-use.local
resolution: "@grafana/schema@workspace:packages/grafana-schema"
dependencies:
@@ -4250,16 +4250,16 @@ __metadata:
languageName: node
linkType: hard
-"@grafana/toolkit@8.5.0-pre, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit":
+"@grafana/toolkit@9.0.0-pre, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit":
version: 0.0.0-use.local
resolution: "@grafana/toolkit@workspace:packages/grafana-toolkit"
dependencies:
"@babel/core": 7.13.14
"@babel/preset-env": 7.13.12
- "@grafana/data": 8.5.0-pre
+ "@grafana/data": 9.0.0-pre
"@grafana/eslint-config": 2.5.2
"@grafana/tsconfig": ^1.2.0-rc1
- "@grafana/ui": 8.5.0-pre
+ "@grafana/ui": 9.0.0-pre
"@jest/core": 26.6.3
"@rushstack/eslint-patch": 1.0.6
"@types/command-exists": ^1.2.0
@@ -4343,7 +4343,7 @@ __metadata:
languageName: node
linkType: hard
-"@grafana/ui@8.5.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui":
+"@grafana/ui@9.0.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui":
version: 0.0.0-use.local
resolution: "@grafana/ui@workspace:packages/grafana-ui"
dependencies:
@@ -4351,9 +4351,9 @@ __metadata:
"@emotion/css": 11.7.1
"@emotion/react": 11.8.2
"@grafana/aws-sdk": 0.0.35
- "@grafana/data": 8.5.0-pre
- "@grafana/e2e-selectors": 8.5.0-pre
- "@grafana/schema": 8.5.0-pre
+ "@grafana/data": 9.0.0-pre
+ "@grafana/e2e-selectors": 9.0.0-pre
+ "@grafana/schema": 9.0.0-pre
"@grafana/slate-react": 0.22.10-grafana
"@grafana/tsconfig": ^1.2.0-rc1
"@mdx-js/react": 1.6.22
@@ -4589,9 +4589,9 @@ __metadata:
resolution: "@jaegertracing/jaeger-ui-components@workspace:packages/jaeger-ui-components"
dependencies:
"@emotion/css": 11.7.1
- "@grafana/data": 8.5.0-pre
+ "@grafana/data": 9.0.0-pre
"@grafana/tsconfig": ^1.2.0-rc1
- "@grafana/ui": 8.5.0-pre
+ "@grafana/ui": 9.0.0-pre
"@types/classnames": ^2.2.7
"@types/deep-freeze": ^0.1.1
"@types/grafana__slate-react": "npm:@types/slate-react@0.22.5"
From fba31c2708944c33ca683028c9a4bbaf1a1e6da8 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 7 Apr 2022 14:25:16 +0100
Subject: [PATCH 24/55] Update dependency @testing-library/dom to v8.13.0
(#47423)
Co-authored-by: Renovate Bot
---
package.json | 2 +-
packages/grafana-data/package.json | 2 +-
packages/grafana-runtime/package.json | 2 +-
packages/grafana-ui/package.json | 2 +-
yarn.lock | 16 ++++++++--------
5 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/package.json b/package.json
index 2a2b2de50d9..e5a9937695c 100644
--- a/package.json
+++ b/package.json
@@ -109,7 +109,7 @@
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
"@swc/core": "1.2.160",
"@swc/helpers": "0.3.8",
- "@testing-library/dom": "8.12.0",
+ "@testing-library/dom": "8.13.0",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "7.0.2",
diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json
index d8838af5cd3..2f761e9a3f3 100644
--- a/packages/grafana-data/package.json
+++ b/packages/grafana-data/package.json
@@ -47,7 +47,7 @@
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "13.1.3",
"@swc/helpers": "0.3.8",
- "@testing-library/dom": "8.12.0",
+ "@testing-library/dom": "8.13.0",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "7.0.2",
diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json
index 55f9ffa0ff2..5f22ae36d8c 100644
--- a/packages/grafana-runtime/package.json
+++ b/packages/grafana-runtime/package.json
@@ -38,7 +38,7 @@
"@grafana/tsconfig": "^1.2.0-rc1",
"@rollup/plugin-commonjs": "21.0.2",
"@rollup/plugin-node-resolve": "13.1.3",
- "@testing-library/dom": "8.12.0",
+ "@testing-library/dom": "8.13.0",
"@testing-library/react": "12.1.4",
"@testing-library/user-event": "^13.5.0",
"@types/angular": "1.8.4",
diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json
index 7238cf849fc..03daae61a61 100644
--- a/packages/grafana-ui/package.json
+++ b/packages/grafana-ui/package.json
@@ -115,7 +115,7 @@
"@storybook/react": "6.4.15",
"@storybook/theming": "6.4.15",
"@swc/helpers": "0.3.8",
- "@testing-library/dom": "8.12.0",
+ "@testing-library/dom": "8.13.0",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "7.0.2",
diff --git a/yarn.lock b/yarn.lock
index 4590dfba6d7..4b3b5d24640 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3993,7 +3993,7 @@ __metadata:
"@rollup/plugin-json": 4.1.0
"@rollup/plugin-node-resolve": 13.1.3
"@swc/helpers": 0.3.8
- "@testing-library/dom": 8.12.0
+ "@testing-library/dom": 8.13.0
"@testing-library/jest-dom": 5.16.2
"@testing-library/react": 12.1.4
"@testing-library/react-hooks": 7.0.2
@@ -4178,7 +4178,7 @@ __metadata:
"@rollup/plugin-commonjs": 21.0.2
"@rollup/plugin-node-resolve": 13.1.3
"@sentry/browser": 6.19.1
- "@testing-library/dom": 8.12.0
+ "@testing-library/dom": 8.13.0
"@testing-library/react": 12.1.4
"@testing-library/user-event": ^13.5.0
"@types/angular": 1.8.4
@@ -4384,7 +4384,7 @@ __metadata:
"@storybook/react": 6.4.15
"@storybook/theming": 6.4.15
"@swc/helpers": 0.3.8
- "@testing-library/dom": 8.12.0
+ "@testing-library/dom": 8.13.0
"@testing-library/jest-dom": 5.16.2
"@testing-library/react": 12.1.4
"@testing-library/react-hooks": 7.0.2
@@ -9032,9 +9032,9 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/dom@npm:8.12.0":
- version: 8.12.0
- resolution: "@testing-library/dom@npm:8.12.0"
+"@testing-library/dom@npm:8.13.0":
+ version: 8.13.0
+ resolution: "@testing-library/dom@npm:8.13.0"
dependencies:
"@babel/code-frame": ^7.10.4
"@babel/runtime": ^7.12.5
@@ -9044,7 +9044,7 @@ __metadata:
dom-accessibility-api: ^0.5.9
lz-string: ^1.4.4
pretty-format: ^27.0.2
- checksum: 2bbf5fa5c1e883571c440ccee76c0568fa5153b43c097456dd7146797256687352bfca9db574e0e78a022ce14722a6acaaba5f680ee16b95e12405501713d34d
+ checksum: 880f1872b9949800d4444e3bdbd03df86d6f41ec7c27136dff1da29e87d2df2d7ee904afcdf895ffce351c25bd12119117eae023354d50e707ad56d43b2ed3ed
languageName: node
linkType: hard
@@ -20461,7 +20461,7 @@ __metadata:
"@sentry/utils": 6.19.1
"@swc/core": 1.2.160
"@swc/helpers": 0.3.8
- "@testing-library/dom": 8.12.0
+ "@testing-library/dom": 8.13.0
"@testing-library/jest-dom": 5.16.2
"@testing-library/react": 12.1.4
"@testing-library/react-hooks": 7.0.2
From 29b8d5d295c33205d2bad71fe27409902612000f Mon Sep 17 00:00:00 2001
From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com>
Date: Thu, 7 Apr 2022 15:45:03 +0200
Subject: [PATCH 25/55] remove the parallele to avoid concurrency (#47447)
---
pkg/schema/load/load_test.go | 1 -
1 file changed, 1 deletion(-)
diff --git a/pkg/schema/load/load_test.go b/pkg/schema/load/load_test.go
index e7d3c1e310c..65d7655bac5 100644
--- a/pkg/schema/load/load_test.go
+++ b/pkg/schema/load/load_test.go
@@ -36,7 +36,6 @@ type testfunc func(*testing.T, schema.VersionedCueSchema, []byte, fs.FileInfo, s
// TODO: remove validdir once we can test directly with devenv folder
var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, fn testfunc) func(t *testing.T) {
return func(t *testing.T) {
- t.Parallel()
require.NoError(t, filepath.Walk(validdir, func(path string, d fs.FileInfo, err error) error {
require.NoError(t, err)
From b857f8339ed58a20a7bb6f61c6a3e784164faa1c Mon Sep 17 00:00:00 2001
From: Josh Hunt
Date: Thu, 7 Apr 2022 14:45:14 +0100
Subject: [PATCH 26/55] TestData: Interpolate variables in more fields (#47158)
* TestData: Interpolate variables in more fields
* only interpolate variables if the field is set
* Correctly type scenarioId which can be undefined
---
.../datasource/testdata/QueryEditor.tsx | 2 +-
.../plugins/datasource/testdata/datasource.ts | 30 ++++++++++++++-----
.../app/plugins/datasource/testdata/types.ts | 2 +-
3 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx
index ae68e35ffb8..a10116b875f 100644
--- a/public/app/plugins/datasource/testdata/QueryEditor.tsx
+++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx
@@ -163,7 +163,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
.sort((a, b) => a.label.localeCompare(b.label)),
[scenarioList]
);
- const showLabels = useMemo(() => showLabelsFor.includes(query.scenarioId), [query]);
+ const showLabels = useMemo(() => showLabelsFor.includes(query.scenarioId ?? ''), [query]);
if (loading) {
return null;
diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts
index a92f578133f..b75d23016be 100644
--- a/public/app/plugins/datasource/testdata/datasource.ts
+++ b/public/app/plugins/datasource/testdata/datasource.ts
@@ -87,10 +87,6 @@ export class TestDataDataSource extends DataSourceWithBackend {
}
default:
- if (target.alias) {
- target.alias = this.templateSrv.replace(target.alias, options.scopedVars);
- }
-
backendQueries.push(target);
}
}
@@ -111,7 +107,24 @@ export class TestDataDataSource extends DataSourceWithBackend {
}
resolveTemplateVariables(query: TestDataQuery, scopedVars: ScopedVars) {
- query.labels = this.templateSrv.replace(query.labels!, scopedVars);
+ if (query.labels) {
+ query.labels = this.templateSrv.replace(query.labels, scopedVars);
+ }
+ if (query.alias) {
+ query.alias = this.templateSrv.replace(query.alias, scopedVars);
+ }
+ if (query.scenarioId) {
+ query.scenarioId = this.templateSrv.replace(query.scenarioId, scopedVars);
+ }
+ if (query.stringInput) {
+ query.stringInput = this.templateSrv.replace(query.stringInput, scopedVars);
+ }
+ if (query.csvContent) {
+ query.csvContent = this.templateSrv.replace(query.csvContent, scopedVars);
+ }
+ if (query.rawFrameContent) {
+ query.rawFrameContent = this.templateSrv.replace(query.rawFrameContent, scopedVars);
+ }
}
annotationDataTopicTest(target: TestDataQuery, req: DataQueryRequest): Observable {
@@ -145,10 +158,13 @@ export class TestDataDataSource extends DataSourceWithBackend {
}
getQueryDisplayText(query: TestDataQuery) {
+ const scenario = query.scenarioId ?? 'Default scenario';
+
if (query.alias) {
- return query.scenarioId + ' as ' + query.alias;
+ return scenario + ' as ' + query.alias;
}
- return query.scenarioId;
+
+ return scenario;
}
testDatasource() {
diff --git a/public/app/plugins/datasource/testdata/types.ts b/public/app/plugins/datasource/testdata/types.ts
index 0f23a903de6..56ce9f569f4 100644
--- a/public/app/plugins/datasource/testdata/types.ts
+++ b/public/app/plugins/datasource/testdata/types.ts
@@ -9,7 +9,7 @@ export interface Scenario {
export interface TestDataQuery extends DataQuery {
alias?: string;
- scenarioId: string;
+ scenarioId?: string;
stringInput?: string;
stream?: StreamingQuery;
pulseWave?: PulseWaveQuery;
From 21d82132bb862f5ed0324e27766d1176c1bf77a1 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 7 Apr 2022 15:54:26 +0200
Subject: [PATCH 27/55] Update dependency webpack to v5.72.0 (#47309)
Co-authored-by: Renovate Bot
---
package.json | 4 +--
packages/grafana-e2e/package.json | 2 +-
packages/grafana-ui/package.json | 2 +-
yarn.lock | 45 ++++++++++++++++++++++++++++---
4 files changed, 45 insertions(+), 8 deletions(-)
diff --git a/package.json b/package.json
index e5a9937695c..2383eb001a3 100644
--- a/package.json
+++ b/package.json
@@ -237,7 +237,7 @@
"ts-node": "10.7.0",
"typescript": "4.4.4",
"wait-on": "6.0.1",
- "webpack": "5.70.0",
+ "webpack": "5.72.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.9.2",
"webpack-dev-server": "4.7.4",
@@ -396,7 +396,7 @@
"@rushstack/node-core-library": "3.45.1",
"@rushstack/rig-package": "0.3.8",
"@rushstack/ts-command-line": "4.10.7",
- "@storybook/react/webpack": "5.70.0",
+ "@storybook/react/webpack": "5.72.0",
"node-fetch": "2.6.7"
},
"workspaces": {
diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json
index 9f892d541c6..6fb70e16064 100644
--- a/packages/grafana-e2e/package.json
+++ b/packages/grafana-e2e/package.json
@@ -41,7 +41,7 @@
"rollup-plugin-copy": "3.4.0",
"rollup-plugin-sourcemaps": "0.6.3",
"rollup-plugin-terser": "7.0.2",
- "webpack": "5.70.0"
+ "webpack": "5.72.0"
},
"types": "src/index.ts",
"dependencies": {
diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json
index 03daae61a61..10130b923a7 100644
--- a/packages/grafana-ui/package.json
+++ b/packages/grafana-ui/package.json
@@ -175,7 +175,7 @@
"terser-webpack-plugin": "5.3.1",
"ts-loader": "8.0.11",
"typescript": "4.4.4",
- "webpack": "5.70.0",
+ "webpack": "5.72.0",
"webpack-filter-warnings-plugin": "1.2.1"
},
"types": "src/index.ts"
diff --git a/yarn.lock b/yarn.lock
index 4b3b5d24640..b63315ebeb1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4094,7 +4094,7 @@ __metadata:
tslib: 2.3.1
typescript: 4.4.4
uuid: 8.3.2
- webpack: 5.70.0
+ webpack: 5.72.0
yaml: ^1.8.3
bin:
grafana-e2e: bin/grafana-e2e.js
@@ -4490,7 +4490,7 @@ __metadata:
typescript: 4.4.4
uplot: 1.6.19
uuid: 8.3.2
- webpack: 5.70.0
+ webpack: 5.72.0
webpack-filter-warnings-plugin: 1.2.1
languageName: unknown
linkType: soft
@@ -20695,7 +20695,7 @@ __metadata:
vendor: "link:./public/vendor"
visjs-network: 4.25.0
wait-on: 6.0.1
- webpack: 5.70.0
+ webpack: 5.72.0
webpack-bundle-analyzer: 4.5.0
webpack-cli: 4.9.2
webpack-dev-server: 4.7.4
@@ -36984,7 +36984,44 @@ __metadata:
languageName: node
linkType: hard
-"webpack@npm:5.70.0, webpack@npm:^5.9.0":
+"webpack@npm:5.72.0":
+ version: 5.72.0
+ resolution: "webpack@npm:5.72.0"
+ dependencies:
+ "@types/eslint-scope": ^3.7.3
+ "@types/estree": ^0.0.51
+ "@webassemblyjs/ast": 1.11.1
+ "@webassemblyjs/wasm-edit": 1.11.1
+ "@webassemblyjs/wasm-parser": 1.11.1
+ acorn: ^8.4.1
+ acorn-import-assertions: ^1.7.6
+ browserslist: ^4.14.5
+ chrome-trace-event: ^1.0.2
+ enhanced-resolve: ^5.9.2
+ es-module-lexer: ^0.9.0
+ eslint-scope: 5.1.1
+ events: ^3.2.0
+ glob-to-regexp: ^0.4.1
+ graceful-fs: ^4.2.9
+ json-parse-better-errors: ^1.0.2
+ loader-runner: ^4.2.0
+ mime-types: ^2.1.27
+ neo-async: ^2.6.2
+ schema-utils: ^3.1.0
+ tapable: ^2.1.1
+ terser-webpack-plugin: ^5.1.3
+ watchpack: ^2.3.1
+ webpack-sources: ^3.2.3
+ peerDependenciesMeta:
+ webpack-cli:
+ optional: true
+ bin:
+ webpack: bin/webpack.js
+ checksum: 8365f1466d0f7adbf80ebc9b780f263a28eeeabcd5fb515249bfd9a56ab7fe8d29ea53df3d9364d0732ab39ae774445eb28abce694ed375b13882a6b2fe93ffc
+ languageName: node
+ linkType: hard
+
+"webpack@npm:^5.9.0":
version: 5.70.0
resolution: "webpack@npm:5.70.0"
dependencies:
From 59388bf546fbb70c1794e5a43e64824dfd4aae61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Agn=C3=A8s=20Toulet?=
<35176601+AgnesToulet@users.noreply.github.com>
Date: Thu, 7 Apr 2022 16:17:37 +0200
Subject: [PATCH 28/55] Config: fix buildInfo initialization (#47402)
* Config: fix buildInfo initialization
* change default to be parseable
* apply default values for buildInfo
---
packages/grafana-runtime/src/config.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts
index 6a0e015626b..f2599a42894 100644
--- a/packages/grafana-runtime/src/config.ts
+++ b/packages/grafana-runtime/src/config.ts
@@ -119,7 +119,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
this.theme2 = createTheme({ colors: { mode } });
this.theme = this.theme2.v1;
this.bootData = options.bootData;
- this.buildInfo = options.buildInfo;
const defaults = {
datasources: {},
@@ -131,7 +130,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
appUrl: '',
appSubUrl: '',
buildInfo: {
- version: 'v1.0',
+ version: '1.0',
commit: '1',
env: 'production',
},
@@ -142,6 +141,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
merge(this, defaults, options);
+ this.buildInfo = options.buildInfo || defaults.buildInfo;
+
if (this.dateFormats) {
systemDateFormats.update(this.dateFormats);
}
From 94f72acbb38722b7a942262c1513dc14032ffde2 Mon Sep 17 00:00:00 2001
From: gotjosh
Date: Thu, 7 Apr 2022 15:24:26 +0100
Subject: [PATCH 29/55] Alerting: Introduce an internal changelog (#47390)
* Alerting: Introduce an internal changelog
Please note that this is not intented to replace Grafana's "add to changelog" label. It is _mostly_ for internal consumption of the Alerting team that owns this part of Grafana.
* Fix markdown formatting
* Fix changelog entry
---
pkg/services/ngalert/CHANGELOG.md | 49 +++++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)
create mode 100644 pkg/services/ngalert/CHANGELOG.md
diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md
new file mode 100644
index 00000000000..0b6fff8a661
--- /dev/null
+++ b/pkg/services/ngalert/CHANGELOG.md
@@ -0,0 +1,49 @@
+# Changelog
+
+**Please note that this is not a direct replacement of Grafana's "add to changelog" label. It is _mostly_ for internal consumption of the Alerting team that owns this part of Grafana.**
+
+## Scope Glossary
+
+### `[ADMIN]`
+The ADMIN scope denotes a change that affect the structure and layout of this repository. This includes updates to the following:
+
+- CODEOWNERS
+- README
+- DotFiles (.gitignore, .git-attributes, etc)
+
+Anything that a developer working on this repo should be aware of from a standards and practice perspective.
+
+### `[BUGFIX]`
+
+The BUGFIX scope denotes a change that fixes an issue with the project in question. A BUGFIX should align the behaviour of the service with the current expected behaviour of the service. If a BUGFIX introduces new unexpected behaviour to ameliorate the issue, a corresponding FEATURE or ENHANCEMENT scope should also be added to the changelog.
+
+### `[CHANGE]`
+
+The CHANGE scope denotes a change that changes the expected behavior of the project while not adding new functionality or fixing an underling issue. This commonly occurs when renaming things to make them more consistent or to accommodate updated versions of vendored dependencies.
+
+### `[FEATURE]`
+
+The FEATURE scope denotes a change that adds new functionality to the project/service.
+
+### `[ENHANCEMENT]`
+
+The ENHANCEMENT scope denotes a change that improves upon the current functionality of the project/service. Generally, an enhancement is something that improves upon something that is already present. Either by making it simpler, more powerful, or more performant. For Example:
+
+An optimization on a particular process in a service that makes it more performant
+Simpler syntax for setting a configuration value, like allowing 1m instead of 60 for a duration setting.
+
+## Order
+
+Scopes must have an order to ensure consistency and ease of search, this helps us identify which section do we need to look for what. The order must be:
+
+1. `[CHANGE]`
+2. `[FEATURE]`
+3. `[BUGFIX]`
+4. `[ENHANCEMENT]`
+5. `[ADMIN]`
+
+
+## Grafana Alerting - main / unreleased
+
+- [CHANGE] Prometheus Compatible API: Use float-like values for `api/prometheus/grafana/api/v1/alerts` and `api/prometheus/grafana/api/v1/rules` instead of the evaluation string #47216
+- [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971
\ No newline at end of file
From 74a5650156287e7f1c368f616d78137641f65519 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 7 Apr 2022 18:23:22 +0200
Subject: [PATCH 30/55] Update dependency jest-junit to v13.1.0 (#47446)
Co-authored-by: Renovate Bot
---
package.json | 2 +-
yarn.lock | 16 ++++++++++++++--
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index 2383eb001a3..37f76582ca8 100644
--- a/package.json
+++ b/package.json
@@ -202,7 +202,7 @@
"jest-canvas-mock": "2.3.1",
"jest-date-mock": "1.0.8",
"jest-fail-on-console": "2.3.0",
- "jest-junit": "13.0.0",
+ "jest-junit": "13.1.0",
"jest-matcher-utils": "27.5.1",
"jest-mock-console": "1.2.3",
"lerna": "^4.0.0",
diff --git a/yarn.lock b/yarn.lock
index b63315ebeb1..568fe87e119 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -20589,7 +20589,7 @@ __metadata:
jest-canvas-mock: 2.3.1
jest-date-mock: 1.0.8
jest-fail-on-console: 2.3.0
- jest-junit: 13.0.0
+ jest-junit: 13.1.0
jest-matcher-utils: 27.5.1
jest-mock-console: 1.2.3
jquery: 3.6.0
@@ -23417,7 +23417,19 @@ __metadata:
languageName: node
linkType: hard
-"jest-junit@npm:13.0.0, jest-junit@npm:^13.0.0":
+"jest-junit@npm:13.1.0":
+ version: 13.1.0
+ resolution: "jest-junit@npm:13.1.0"
+ dependencies:
+ mkdirp: ^1.0.4
+ strip-ansi: ^6.0.1
+ uuid: ^8.3.2
+ xml: ^1.0.1
+ checksum: a12ec522012cf9d0e97d4c665d8ca05b97dc3d25ef3cd0dbba2b095da4b68e1cabac9751a2718a9593b55c620a2b980e0b6c7dd7d33dfc4b73193bc67b9c9ae0
+ languageName: node
+ linkType: hard
+
+"jest-junit@npm:^13.0.0":
version: 13.0.0
resolution: "jest-junit@npm:13.0.0"
dependencies:
From 883f9f718f4a648eeb48872532adc4a8a21f5745 Mon Sep 17 00:00:00 2001
From: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
Date: Thu, 7 Apr 2022 11:25:58 -0500
Subject: [PATCH 31/55] Docs: corrects typos (#47464)
---
docs/sources/enterprise/license/license-restrictions.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/sources/enterprise/license/license-restrictions.md b/docs/sources/enterprise/license/license-restrictions.md
index 2e4b068a450..080803aac4c 100644
--- a/docs/sources/enterprise/license/license-restrictions.md
+++ b/docs/sources/enterprise/license/license-restrictions.md
@@ -29,7 +29,7 @@ Your Grafana license includes a maximum number of _Viewer_ and _Editor/Admin_ ac
- An _active user_ is a user who has signed in to Grafana within the last 30 days. This is a rolling window that is updated daily.
- When you reach the number of maximum active viewers or editor/admins, only currently active users can sign in; new users and non-active users cannot sign in when you reach the limit.
-- Grafana applies sign-in restrictions separately for viewers and editor/admins. If your Grafana license reaches its limit of active viewers but not its limit of active editor/ddmins, new editors and admins can still sign in.
+- Grafana applies sign-in restrictions separately for viewers and editor/admins. If your Grafana license reaches its limit of active viewers but not its limit of active editor/admins, new editors and admins can still sign in.
- The number of dashboards that a user can view or edit, and the number of organizations that they can access does not affect the active user count. A user with editor permissions for many dashboards across many different organizations counts as one editor.
- A license limit banner appears to administrators when Grafana reaches its active user limit; editors and viewers do not see the banner.
To change user roles to make better use of your licenses, refer to [Optimize your tiered license](#optimize-your-tiered-license).
@@ -123,7 +123,7 @@ After you apply the token, Grafana Enterprise resets your license and updates th
> If you are running Grafana Enterprise 8.2 or earlier, the license grants you the total number of licensed users _for each user type_.
-For example, if your current license includes 60 viewers and 40 dditor/admins, the new license includes 100 viewers and 100 editor/admins. Grafana Enterprise 8.3 removes the distinction between viewers and editor/admins as shown on the **Utilization** panel.
+For example, if your current license includes 60 viewers and 40 editor/admins, the new license includes 100 viewers and 100 editor/admins. Grafana Enterprise 8.3 removes the distinction between viewers and editor/admins as shown on the **Utilization** panel.
Before you upgrade to Grafana 8.3, ensure that the total number of active users in Grafana does not exceed the number of users in your combined license. If it does, then new users cannot sign in to Grafana 8.3 until the active user count returns below the licensed limit.
From 222325c7f192ec8644b46c417bc2d09946181291 Mon Sep 17 00:00:00 2001
From: Stephanie Closson
Date: Thu, 7 Apr 2022 13:49:54 -0300
Subject: [PATCH 32/55] Fix: Parse exemplars before parsing heatmap data
(#47463)
* parse exemplars before parsing the heatmap. Exemplars are overlayed. Not part of the main heatmap
* added tests
---
.../prometheus/result_transformer.test.ts | 65 +++++++++++++++++++
.../prometheus/result_transformer.ts | 21 +++---
2 files changed, 76 insertions(+), 10 deletions(-)
diff --git a/public/app/plugins/datasource/prometheus/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts
index d61dee2fbad..b7087c11d1e 100644
--- a/public/app/plugins/datasource/prometheus/result_transformer.test.ts
+++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts
@@ -251,6 +251,71 @@ describe('Prometheus Result Transformer', () => {
expect(series.data[0].fields[2].values.toArray()).toEqual([10, 0, 30]);
expect(series.data[0].fields[3].values.toArray()).toEqual([10, 0, 10]);
});
+
+ it('Retains exemplar frames when data returned is a heatmap', () => {
+ const options = {
+ targets: [
+ {
+ format: 'heatmap',
+ refId: 'A',
+ },
+ ],
+ } as unknown as DataQueryRequest;
+ const response = {
+ state: 'Done',
+ data: [
+ new MutableDataFrame({
+ refId: 'A',
+ fields: [
+ { name: 'Time', type: FieldType.time, values: [6, 5, 4] },
+ {
+ name: 'Value',
+ type: FieldType.number,
+ values: [10, 10, 0],
+ labels: { le: '1' },
+ },
+ ],
+ }),
+ new MutableDataFrame({
+ refId: 'A',
+ name: 'exemplar',
+ meta: {
+ custom: {
+ resultType: 'exemplar',
+ },
+ },
+ fields: [
+ { name: 'Time', type: FieldType.time, values: [6, 5, 4, 3, 2, 1] },
+ {
+ name: 'Value',
+ type: FieldType.number,
+ values: [30, 10, 40, 90, 14, 21],
+ labels: { le: '6' },
+ },
+ {
+ name: 'Test',
+ type: FieldType.string,
+ values: ['hello', 'doctor', 'name', 'continue', 'yesterday', 'tomorrow'],
+ labels: { le: '6' },
+ },
+ ],
+ }),
+ ],
+ } as unknown as DataQueryResponse;
+
+ const series = transformV2(response, options, {});
+ expect(series.data[0].fields.length).toEqual(2);
+ expect(series.data.length).toEqual(2);
+ expect(series.data[1].fields[2].values.toArray()).toEqual([
+ 'hello',
+ 'doctor',
+ 'name',
+ 'continue',
+ 'yesterday',
+ 'tomorrow',
+ ]);
+ expect(series.data[1].fields.length).toEqual(3);
+ });
});
describe('transformDFToTable', () => {
it('transforms dataFrame with response length 1 to table dataFrame', () => {
diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts
index d3d5b1f9595..62b8d2f3422 100644
--- a/public/app/plugins/datasource/prometheus/result_transformer.ts
+++ b/public/app/plugins/datasource/prometheus/result_transformer.ts
@@ -72,16 +72,8 @@ export function transformV2(
const [tableFrames, framesWithoutTable] = partition(response.data, (df) => isTableResult(df, request));
const processedTableFrames = transformDFToTable(tableFrames);
- const [heatmapResults, framesWithoutTableAndHeatmaps] = partition(framesWithoutTable, (df) =>
- isHeatmapResult(df, request)
- );
-
- const processedHeatmapFrames = mergeHeatmapFrames(
- transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel))
- );
-
- const [exemplarFrames, framesWithoutTableHeatmapsAndExemplars] = partition(
- framesWithoutTableAndHeatmaps,
+ const [exemplarFrames, framesWithoutTableAndExemplars] = partition(
+ framesWithoutTable,
(df) => df.meta?.custom?.resultType === 'exemplar'
);
@@ -103,6 +95,15 @@ export function transformV2(
return { ...dataFrame, meta: { ...dataFrame.meta, dataTopic: DataTopic.Annotations } };
});
+ const [heatmapResults, framesWithoutTableHeatmapsAndExemplars] = partition(
+ framesWithoutTableAndExemplars,
+ (df) => isHeatmapResult(df, request)
+ );
+
+ const processedHeatmapFrames = mergeHeatmapFrames(
+ transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel))
+ );
+
// Everything else is processed as time_series result and graph preferredVisualisationType
const otherFrames = framesWithoutTableHeatmapsAndExemplars.map((dataFrame) => {
const df = {
From c3ad36ba72c5c581737d45ef86206156200c46e4 Mon Sep 17 00:00:00 2001
From: Alexander Weaver
Date: Thu, 7 Apr 2022 12:52:00 -0500
Subject: [PATCH 33/55] Temporarily skip intermittent test (#47471)
---
pkg/services/ngalert/notifier/alertmanager_test.go | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/pkg/services/ngalert/notifier/alertmanager_test.go b/pkg/services/ngalert/notifier/alertmanager_test.go
index 94ec8a365fb..a3b0ebece1d 100644
--- a/pkg/services/ngalert/notifier/alertmanager_test.go
+++ b/pkg/services/ngalert/notifier/alertmanager_test.go
@@ -346,6 +346,10 @@ func TestPutAlert(t *testing.T) {
// implement a custom maintenance function for silences, because we snapshot
// our data differently, so we test that functionality.
func TestSilenceCleanup(t *testing.T) {
+ // TODO: This test intermittently fails. Un-skip me!
+ // https://github.com/grafana/grafana/issues/47470
+ t.Skip("intermittent test")
+
require := require.New(t)
oldRetention := retentionNotificationsAndSilences
From 4a4d87dbdcaf4dca753bcf29205ac036b6c59862 Mon Sep 17 00:00:00 2001
From: Jeff Levin
Date: Thu, 7 Apr 2022 10:30:19 -0800
Subject: [PATCH 34/55] add public dashboards feature flag (#47364)
---
packages/grafana-data/src/types/featureToggles.gen.ts | 1 +
pkg/services/featuremgmt/registry.go | 6 ++++++
pkg/services/featuremgmt/toggles_gen.go | 4 ++++
3 files changed, 11 insertions(+)
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index dc9564e190e..23e997e4dd6 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -41,6 +41,7 @@ export interface FeatureToggles {
showFeatureFlagsInUI?: boolean;
disable_http_request_histogram?: boolean;
validatedQueries?: boolean;
+ publicDashboards?: boolean;
lokiLive?: boolean;
swaggerUi?: boolean;
featureHighlights?: boolean;
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index af7f1baf424..60b46d46b6a 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -143,6 +143,12 @@ var (
State: FeatureStateAlpha,
RequiresDevMode: true,
},
+ {
+ Name: "publicDashboards",
+ Description: "enables public access to dashboards",
+ State: FeatureStateAlpha,
+ RequiresDevMode: true,
+ },
{
Name: "lokiLive",
Description: "support websocket streaming for loki (early prototype)",
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 078ecbda7e7..7548ed76643 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -107,6 +107,10 @@ const (
// only execute the query saved in a panel
FlagValidatedQueries = "validatedQueries"
+ // FlagPublicDashboards
+ // enables public access to dashboards
+ FlagPublicDashboards = "publicDashboards"
+
// FlagLokiLive
// support websocket streaming for loki (early prototype)
FlagLokiLive = "lokiLive"
From 631dd718a228823e6570cd02e71cfb910a822c3c Mon Sep 17 00:00:00 2001
From: Joe Blubaugh
Date: Fri, 8 Apr 2022 14:52:08 +0800
Subject: [PATCH 35/55] 47470: Add additional delay to silences in test.
(#47482)
This test of silence cleanup was flaky because of its use of real wall
time. In CI environments with slow execution, delays could cause the
test to fail. This change mitigates the problem by increasing the end time of
silences in the test.
After Prometheus merges this PR: https://github.com/prometheus/alertmanager/pull/2867
we can make the test fully deterministic by using a fake clock.
Fixes #47470
Signed-off-by: Joe Blubaugh
---
pkg/services/ngalert/notifier/alertmanager_test.go | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/pkg/services/ngalert/notifier/alertmanager_test.go b/pkg/services/ngalert/notifier/alertmanager_test.go
index a3b0ebece1d..5e2f10e7420 100644
--- a/pkg/services/ngalert/notifier/alertmanager_test.go
+++ b/pkg/services/ngalert/notifier/alertmanager_test.go
@@ -346,10 +346,6 @@ func TestPutAlert(t *testing.T) {
// implement a custom maintenance function for silences, because we snapshot
// our data differently, so we test that functionality.
func TestSilenceCleanup(t *testing.T) {
- // TODO: This test intermittently fails. Un-skip me!
- // https://github.com/grafana/grafana/issues/47470
- t.Skip("intermittent test")
-
require := require.New(t)
oldRetention := retentionNotificationsAndSilences
@@ -390,10 +386,10 @@ func TestSilenceCleanup(t *testing.T) {
makeSilence("", "tests", dt(now.Add(5*time.Hour)), dt(now.Add(6*time.Hour)), matchers),
// Active now
makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(6*time.Hour)), matchers),
- // Expiring soon
- makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(2*time.Second)), matchers),
+ // Expiring soon.
+ makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(5*time.Second)), matchers),
// Expiring *very* soon
- makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(20*time.Millisecond)), matchers),
+ makeSilence("", "tests", dt(now.Add(-5*time.Hour)), dt(now.Add(2*time.Second)), matchers),
}
for _, s := range silences {
@@ -407,12 +403,12 @@ func TestSilenceCleanup(t *testing.T) {
found, err := am.ListSilences(nil)
require.NoError(err)
return len(found) == 3
- }, 1500*time.Millisecond, 150*time.Millisecond)
+ }, 3*time.Second, 150*time.Millisecond)
// Wait again for another silence to expire.
require.Eventually(func() bool {
found, err := am.ListSilences(nil)
require.NoError(err)
return len(found) == 2
- }, 2*time.Second, 150*time.Millisecond)
+ }, 6*time.Second, 150*time.Millisecond)
}
From 7ab910af480497b6e102fe5c59a968b18968e1ee Mon Sep 17 00:00:00 2001
From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
Date: Fri, 8 Apr 2022 09:35:34 +0200
Subject: [PATCH 36/55] Query history: Search in comments (#46761)
---
pkg/services/queryhistory/queryhistory_search_test.go | 2 +-
pkg/services/queryhistory/queryhistory_test.go | 6 ++++++
pkg/services/queryhistory/writers.go | 4 ++--
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/pkg/services/queryhistory/queryhistory_search_test.go b/pkg/services/queryhistory/queryhistory_search_test.go
index f459cd4fa7e..9d3db5ca328 100644
--- a/pkg/services/queryhistory/queryhistory_search_test.go
+++ b/pkg/services/queryhistory/queryhistory_search_test.go
@@ -111,7 +111,7 @@ func TestGetQueriesFromQueryHistory(t *testing.T) {
err := json.Unmarshal(resp.Body(), &response)
require.NoError(t, err)
require.Equal(t, 200, resp.Status())
- require.Equal(t, 1, response.Result.TotalCount)
+ require.Equal(t, 2, response.Result.TotalCount)
require.Equal(t, true, response.Result.QueryHistory[0].Starred)
})
}
diff --git a/pkg/services/queryhistory/queryhistory_test.go b/pkg/services/queryhistory/queryhistory_test.go
index fe9dfecedeb..b88c3161309 100644
--- a/pkg/services/queryhistory/queryhistory_test.go
+++ b/pkg/services/queryhistory/queryhistory_test.go
@@ -115,6 +115,12 @@ func testScenarioWithMultipleQueriesInQueryHistory(t *testing.T, desc string, fn
resp1 := sc.service.createHandler(sc.reqContext)
sc.initialResult = validateAndUnMarshalResponse(t, resp1)
+ // Add comment
+ cmd := PatchQueryCommentInQueryHistoryCommand{Comment: "test comment 2"}
+ sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
+ sc.reqContext.Req.Body = mockRequestBody(cmd)
+ sc.service.patchCommentHandler(sc.reqContext)
+
time.Sleep(1 * time.Second)
command2 := CreateQueryInQueryHistoryCommand{
DatasourceUID: testDsUID1,
diff --git a/pkg/services/queryhistory/writers.go b/pkg/services/queryhistory/writers.go
index 20f94ab1705..034a055ecfb 100644
--- a/pkg/services/queryhistory/writers.go
+++ b/pkg/services/queryhistory/writers.go
@@ -23,12 +23,12 @@ func writeStarredSQL(query SearchInQueryHistoryQuery, sqlStore *sqlstore.SQLStor
}
func writeFiltersSQL(query SearchInQueryHistoryQuery, user *models.SignedInUser, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
- params := []interface{}{user.OrgId, user.UserId, "%" + query.SearchString + "%"}
+ params := []interface{}{user.OrgId, user.UserId, "%" + query.SearchString + "%", "%" + query.SearchString + "%"}
for _, uid := range query.DatasourceUIDs {
params = append(params, uid)
}
var sql bytes.Buffer
- sql.WriteString(" WHERE query_history.org_id = ? AND query_history.created_by = ? AND query_history.queries " + sqlStore.Dialect.LikeStr() + " ? AND query_history.datasource_uid IN (? " + strings.Repeat(",?", len(query.DatasourceUIDs)-1) + ") ")
+ sql.WriteString(" WHERE query_history.org_id = ? AND query_history.created_by = ? AND (query_history.queries " + sqlStore.Dialect.LikeStr() + " ? OR query_history.comment " + sqlStore.Dialect.LikeStr() + " ?) AND query_history.datasource_uid IN (? " + strings.Repeat(",?", len(query.DatasourceUIDs)-1) + ") ")
builder.Write(sql.String(), params...)
}
From e1438990aebe18debaa697bcafddf4ea1554a9dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Farkas?=
Date: Fri, 8 Apr 2022 09:57:06 +0200
Subject: [PATCH 37/55] Loki: use the same dataframe-format for both live and
normal queries (#47153)
* loki: use single dataframe-format for both live and normal queries
* better comment
* better function-name
* simplified code
* logs_model: added test
---
public/app/core/logs_model.test.ts | 146 +++++++++++++++++-
public/app/core/logs_model.ts | 75 ++++++---
.../app/plugins/datasource/loki/datasource.ts | 55 +++----
.../plugins/datasource/loki/live_streams.ts | 6 +-
.../loki/result_transformer.test.ts | 53 +++----
.../datasource/loki/result_transformer.ts | 73 ++++-----
6 files changed, 290 insertions(+), 118 deletions(-)
diff --git a/public/app/core/logs_model.test.ts b/public/app/core/logs_model.test.ts
index 06d81158942..e09b6205849 100644
--- a/public/app/core/logs_model.test.ts
+++ b/public/app/core/logs_model.test.ts
@@ -332,7 +332,10 @@ describe('dataFrameToLogsModel', () => {
expect(logsModel.meta).toHaveLength(2);
expect(logsModel.meta![0]).toMatchObject({
label: COMMON_LABELS,
- value: series[0].fields[1].labels,
+ value: {
+ filename: '/var/log/grafana/grafana.log',
+ job: 'grafana',
+ },
kind: LogsMetaKind.LabelsMap,
});
expect(logsModel.meta![1]).toMatchObject({
@@ -342,6 +345,147 @@ describe('dataFrameToLogsModel', () => {
});
});
+ it('given one series with labels-field should return expected logs model', () => {
+ const series: DataFrame[] = [
+ new MutableDataFrame({
+ fields: [
+ {
+ name: 'labels',
+ type: FieldType.other,
+ values: [
+ {
+ filename: '/var/log/grafana/grafana.log',
+ job: 'grafana',
+ },
+ {
+ filename: '/var/log/grafana/grafana.log',
+ job: 'grafana',
+ },
+ ],
+ },
+ {
+ name: 'time',
+ type: FieldType.time,
+ values: ['2019-04-26T09:28:11.352440161Z', '2019-04-26T14:42:50.991981292Z'],
+ },
+ {
+ name: 'message',
+ type: FieldType.string,
+ values: [
+ 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
+ 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
+ ],
+ },
+ {
+ name: 'id',
+ type: FieldType.string,
+ values: ['foo', 'bar'],
+ },
+ ],
+ meta: {
+ limit: 1000,
+ },
+ }),
+ ];
+ const logsModel = dataFrameToLogsModel(series, 1);
+ expect(logsModel.hasUniqueLabels).toBeFalsy();
+ expect(logsModel.rows).toHaveLength(2);
+ expect(logsModel.rows).toMatchObject([
+ {
+ entry: 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
+ labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
+ logLevel: 'info',
+ uniqueLabels: {},
+ uid: 'foo',
+ },
+ {
+ entry: 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
+ labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
+ logLevel: 'error',
+ uniqueLabels: {},
+ uid: 'bar',
+ },
+ ]);
+
+ expect(logsModel.series).toHaveLength(2);
+ expect(logsModel.series).toMatchObject([
+ {
+ name: 'info',
+ fields: [
+ { type: 'time', values: new ArrayVector([1556270891000, 1556289770000]) },
+ { type: 'number', values: new ArrayVector([1, 0]) },
+ ],
+ },
+ {
+ name: 'error',
+ fields: [
+ { type: 'time', values: new ArrayVector([1556289770000]) },
+ { type: 'number', values: new ArrayVector([1]) },
+ ],
+ },
+ ]);
+ expect(logsModel.meta).toHaveLength(2);
+ expect(logsModel.meta![0]).toMatchObject({
+ label: COMMON_LABELS,
+ value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
+ kind: LogsMetaKind.LabelsMap,
+ });
+ expect(logsModel.meta![1]).toMatchObject({
+ label: LIMIT_LABEL,
+ value: `1000 (2 returned)`,
+ kind: LogsMetaKind.String,
+ });
+ });
+
+ it('given one series with labels-field it should work regardless the label-fields position', () => {
+ const labels = {
+ name: 'labels',
+ type: FieldType.other,
+ values: [
+ {
+ node: 'first',
+ mode: 'slow',
+ },
+ ],
+ };
+
+ const time = {
+ name: 'time',
+ type: FieldType.time,
+ values: ['2019-04-26T09:28:11.352440161Z'],
+ };
+
+ const line = {
+ name: 'line',
+ type: FieldType.string,
+ values: ['line1'],
+ };
+
+ const frame1 = new MutableDataFrame({
+ fields: [labels, time, line],
+ });
+
+ const frame2 = new MutableDataFrame({
+ fields: [time, labels, line],
+ });
+
+ const frame3 = new MutableDataFrame({
+ fields: [time, line, labels],
+ });
+
+ const logsModel1 = dataFrameToLogsModel([frame1], 1);
+ expect(logsModel1.rows).toHaveLength(1);
+ expect(logsModel1.rows[0].labels).toStrictEqual({ mode: 'slow', node: 'first' });
+
+ const logsModel2 = dataFrameToLogsModel([frame2], 1);
+ expect(logsModel2.rows).toHaveLength(1);
+ expect(logsModel2.rows[0].labels).toStrictEqual({ mode: 'slow', node: 'first' });
+
+ const logsModel3 = dataFrameToLogsModel([frame3], 1);
+ expect(logsModel3.rows).toHaveLength(1);
+ expect(logsModel3.rows[0].labels).toStrictEqual({ mode: 'slow', node: 'first' });
+ });
+
it('given one series with error should return expected logs model', () => {
const series: DataFrame[] = [
new MutableDataFrame({
diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts
index 3bee4f2b5d3..36316d6a85e 100644
--- a/public/app/core/logs_model.ts
+++ b/public/app/core/logs_model.ts
@@ -303,11 +303,38 @@ interface LogFields {
timeField: FieldWithIndex;
stringField: FieldWithIndex;
+ labelsField?: FieldWithIndex;
timeNanosecondField?: FieldWithIndex;
logLevelField?: FieldWithIndex;
idField?: FieldWithIndex;
}
+function getAllLabels(fields: LogFields): Labels[] {
+ // there are two types of dataframes we handle:
+ // 1. labels are in a separate field (more efficient when labels change by every log-row)
+ // 2. labels are in in the string-field's `.labels` attribute
+
+ const { stringField, labelsField } = fields;
+
+ const fieldLabels = stringField.labels !== undefined ? [stringField.labels] : [];
+
+ const labelsFieldLabels: Labels[] = labelsField !== undefined ? labelsField.values.toArray() : [];
+
+ return [...fieldLabels, ...labelsFieldLabels];
+}
+
+function getLabelsForFrameRow(fields: LogFields, index: number): Labels {
+ // there are two types of dataframes we handle.
+ // either labels-on-the-string-field, or labels-in-the-labels-field
+
+ const { stringField, labelsField } = fields;
+
+ return {
+ ...stringField.labels,
+ ...labelsField?.values.get(index),
+ };
+}
+
/**
* Converts dataFrames into LogsModel. This involves merging them into one list, sorting them and computing metadata
* like common labels.
@@ -316,7 +343,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
if (logSeries.length === 0) {
return undefined;
}
- const allLabels: Labels[] = [];
+ const allLabels: Labels[][] = [];
// Find the fields we care about and collect all labels
let allSeries: LogFields[] = [];
@@ -330,43 +357,39 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
const fieldCache = new FieldCache(series);
const stringField = fieldCache.getFirstFieldOfType(FieldType.string);
const timeField = fieldCache.getFirstFieldOfType(FieldType.time);
+ const labelsField = fieldCache.getFieldByName('labels');
if (stringField !== undefined && timeField !== undefined) {
- if (stringField?.labels) {
- allLabels.push(stringField.labels);
- }
-
- allSeries.push({
+ const info = {
series,
timeField,
+ labelsField,
timeNanosecondField: fieldCache.hasFieldWithNameAndType('tsNs', FieldType.time)
? fieldCache.getFieldByName('tsNs')
: undefined,
stringField,
logLevelField: fieldCache.getFieldByName('level'),
idField: getIdField(fieldCache),
- });
+ };
+
+ allSeries.push(info);
+
+ const labels = getAllLabels(info);
+ if (labels.length > 0) {
+ allLabels.push(labels);
+ }
}
});
}
- const commonLabels = allLabels.length > 0 ? findCommonLabels(allLabels) : {};
+ const flatAllLabels = allLabels.flat();
+ const commonLabels = flatAllLabels.length > 0 ? findCommonLabels(flatAllLabels) : {};
const rows: LogRowModel[] = [];
let hasUniqueLabels = false;
for (const info of allSeries) {
const { timeField, timeNanosecondField, stringField, logLevelField, idField, series } = info;
- const labels = stringField.labels;
- const uniqueLabels = findUniqueLabels(labels, commonLabels);
- if (Object.keys(uniqueLabels).length > 0) {
- hasUniqueLabels = true;
- }
-
- let seriesLogLevel: LogLevel | undefined = undefined;
- if (labels && Object.keys(labels).indexOf('level') !== -1) {
- seriesLogLevel = getLogLevelFromKey(labels['level']);
- }
for (let j = 0; j < series.length; j++) {
const ts = timeField.values.get(j);
@@ -386,14 +409,20 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : [];
const entry = hasAnsi ? ansicolor.strip(message) : message;
+ const labels = getLabelsForFrameRow(info, j);
+ const uniqueLabels = findUniqueLabels(labels, commonLabels);
+ if (Object.keys(uniqueLabels).length > 0) {
+ hasUniqueLabels = true;
+ }
+
let logLevel = LogLevel.unknown;
- if (logLevelField && logLevelField.values.get(j)) {
- logLevel = getLogLevelFromKey(logLevelField.values.get(j));
- } else if (seriesLogLevel) {
- logLevel = seriesLogLevel;
+ const logLevelKey = (logLevelField && logLevelField.values.get(j)) || (labels && labels['level']);
+ if (logLevelKey) {
+ logLevel = getLogLevelFromKey(logLevelKey);
} else {
logLevel = getLogLevel(entry);
}
+
rows.push({
entryFieldIndex: stringField.index,
rowIndex: j,
@@ -410,7 +439,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
searchWords,
entry,
raw: message,
- labels: stringField.labels || {},
+ labels: labels || {},
uid: idField ? idField.values.get(j) : j.toString(),
});
}
diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts
index f8439136914..1259918bdfc 100644
--- a/public/app/plugins/datasource/loki/datasource.ts
+++ b/public/app/plugins/datasource/loki/datasource.ts
@@ -40,8 +40,8 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { convertToWebSocketUrl } from 'app/core/utils/explore';
import {
lokiResultsToTableModel,
- lokiStreamResultToDataFrame,
lokiStreamsToDataFrames,
+ lokiStreamsToRawDataFrame,
processRangeQueryResponse,
} from './result_transformer';
import { transformBackendResult } from './backendResultTransformer';
@@ -54,7 +54,6 @@ import {
LokiRangeQueryRequest,
LokiResultType,
LokiStreamResponse,
- LokiStreamResult,
} from './types';
import { LiveStreams, LokiLiveTarget } from './live_streams';
import LanguageProvider from './language_provider';
@@ -538,9 +537,7 @@ export class LokiDatasource
}),
switchMap((res) =>
of({
- data: res.data
- ? res.data.data.result.map((stream: LokiStreamResult) => lokiStreamResultToDataFrame(stream, reverse))
- : [],
+ data: res.data ? [lokiStreamsToRawDataFrame(res.data.data.result, reverse)] : [],
})
)
)
@@ -667,33 +664,31 @@ export class LokiDatasource
const splitKeys: string[] = tagKeys.split(',').filter((v: string) => v !== '');
for (const frame of data) {
- const labels: { [key: string]: string } = {};
- for (const field of frame.fields) {
- if (field.labels) {
- for (const [key, value] of Object.entries(field.labels)) {
- labels[key] = String(value).trim();
- }
- }
- }
-
- const tags: string[] = [
- ...new Set(
- Object.entries(labels).reduce((acc: string[], [key, val]) => {
- if (val === '') {
- return acc;
- }
- if (splitKeys.length && !splitKeys.includes(key)) {
- return acc;
- }
- acc.push.apply(acc, [val]);
- return acc;
- }, [])
- ),
- ];
-
- const view = new DataFrameView<{ ts: string; line: string }>(frame);
+ const view = new DataFrameView<{ ts: string; line: string; labels: Labels }>(frame);
view.forEach((row) => {
+ const { labels } = row;
+
+ const maybeDuplicatedTags = Object.entries(labels)
+ .map(([key, val]) => [key, val.trim()]) // trim all label-values
+ .filter(([key, val]) => {
+ if (val === '') {
+ // remove empty
+ return false;
+ }
+
+ // if tags are specified, remove label if does not match tags
+ if (splitKeys.length && !splitKeys.includes(key)) {
+ return false;
+ }
+
+ return true;
+ })
+ .map(([key, val]) => val); // keep only the label-value
+
+ // remove duplicates
+ const tags = Array.from(new Set(maybeDuplicatedTags));
+
annotations.push({
time: new Date(row.ts).valueOf(),
title: renderLegendFormat(titleFormat, labels),
diff --git a/public/app/plugins/datasource/loki/live_streams.ts b/public/app/plugins/datasource/loki/live_streams.ts
index be252116786..a2a68ec9fd6 100644
--- a/public/app/plugins/datasource/loki/live_streams.ts
+++ b/public/app/plugins/datasource/loki/live_streams.ts
@@ -30,11 +30,11 @@ export class LiveStreams {
}
const data = new CircularDataFrame({ capacity: target.size });
- data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } });
- data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } });
- data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query);
data.addField({ name: 'labels', type: FieldType.other }); // The labels for each line
+ data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } });
+ data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query);
data.addField({ name: 'id', type: FieldType.string });
+ data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } });
data.meta = { ...data.meta, preferredVisualisationType: 'logs' };
data.refId = target.refId;
diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts
index 396386e6419..db35da689a6 100644
--- a/public/app/plugins/datasource/loki/result_transformer.test.ts
+++ b/public/app/plugins/datasource/loki/result_transformer.test.ts
@@ -64,18 +64,20 @@ describe('loki result transformer', () => {
jest.clearAllMocks();
});
- describe('lokiStreamResultToDataFrame', () => {
+ describe('lokiStreamsToRawDataFrame', () => {
it('converts streams to series', () => {
- const data = streamResult.map((stream) => ResultTransformer.lokiStreamResultToDataFrame(stream));
+ const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResult);
- expect(data.length).toBe(2);
- expect(data[0].fields[1].labels!['foo']).toEqual('bar');
- expect(data[0].fields[0].values.get(0)).toEqual('2020-01-24T09:19:22.021Z');
- expect(data[0].fields[1].values.get(0)).toEqual(streamResult[0].values[0][1]);
- expect(data[0].fields[2].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc');
- expect(data[1].fields[0].values.get(0)).toEqual('2020-01-24T09:19:22.031Z');
- expect(data[1].fields[1].values.get(0)).toEqual(streamResult[1].values[0][1]);
- expect(data[1].fields[2].values.get(0)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1');
+ expect(data.fields[0].values.get(0)).toStrictEqual({ foo: 'bar' });
+ expect(data.fields[1].values.get(0)).toEqual('2020-01-24T09:19:22.021Z');
+ expect(data.fields[2].values.get(0)).toEqual(streamResult[0].values[0][1]);
+ expect(data.fields[3].values.get(0)).toEqual(streamResult[0].values[0][0]);
+ expect(data.fields[4].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc');
+ expect(data.fields[0].values.get(1)).toStrictEqual({ bar: 'foo' });
+ expect(data.fields[1].values.get(1)).toEqual('2020-01-24T09:19:22.031Z');
+ expect(data.fields[2].values.get(1)).toEqual(streamResult[1].values[0][1]);
+ expect(data.fields[3].values.get(1)).toEqual(streamResult[1].values[0][0]);
+ expect(data.fields[4].values.get(1)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1');
});
it('should always generate unique ids for logs', () => {
@@ -100,20 +102,19 @@ describe('loki result transformer', () => {
},
];
- const data = streamResultWithDuplicateLogs.map((stream) => ResultTransformer.lokiStreamResultToDataFrame(stream));
+ const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResultWithDuplicateLogs);
- expect(data[0].fields[2].values.get(0)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa');
- expect(data[0].fields[2].values.get(1)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_1');
- expect(data[0].fields[2].values.get(2)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2');
- expect(data[0].fields[2].values.get(3)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2');
- expect(data[1].fields[2].values.get(0)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_3');
+ expect(data.fields[4].values.get(0)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa');
+ expect(data.fields[4].values.get(1)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_1');
+ expect(data.fields[4].values.get(2)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2');
+ expect(data.fields[4].values.get(3)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2');
+ expect(data.fields[4].values.get(4)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_3');
});
it('should append refId to the unique ids if refId is provided', () => {
- const data = streamResult.map((stream) => ResultTransformer.lokiStreamResultToDataFrame(stream, false, 'B'));
- expect(data.length).toBe(2);
- expect(data[0].fields[2].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc_B');
- expect(data[1].fields[2].values.get(0)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1_B');
+ const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResult, false, 'B');
+ expect(data.fields[4].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc_B');
+ expect(data.fields[4].values.get(1)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1_B');
});
});
@@ -159,11 +160,11 @@ describe('loki result transformer', () => {
};
const data = new CircularDataFrame({ capacity: 1 });
- data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } });
- data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } });
- data.addField({ name: 'line', type: FieldType.string }).labels = { job: 'grafana' };
data.addField({ name: 'labels', type: FieldType.other });
+ data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } });
+ data.addField({ name: 'line', type: FieldType.string }).labels = { job: 'grafana' };
data.addField({ name: 'id', type: FieldType.string });
+ data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } });
ResultTransformer.appendResponseToBufferedData(tailResponse, data);
expect(data.get(0)).toEqual({
@@ -196,11 +197,11 @@ describe('loki result transformer', () => {
};
const data = new CircularDataFrame({ capacity: 6 });
- data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } });
- data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } });
- data.addField({ name: 'line', type: FieldType.string }).labels = { job: 'grafana' };
data.addField({ name: 'labels', type: FieldType.other });
+ data.addField({ name: 'ts', type: FieldType.time, config: { displayName: 'Time' } });
+ data.addField({ name: 'line', type: FieldType.string }).labels = { job: 'grafana' };
data.addField({ name: 'id', type: FieldType.string });
+ data.addField({ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' } });
data.refId = 'C';
ResultTransformer.appendResponseToBufferedData(tailResponse, data);
diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts
index 79fde9b62b2..8270288c32f 100644
--- a/public/app/plugins/datasource/loki/result_transformer.ts
+++ b/public/app/plugins/datasource/loki/result_transformer.ts
@@ -43,15 +43,10 @@ import { renderLegendFormat } from '../prometheus/legend';
const UUID_NAMESPACE = '6ec946da-0f49-47a8-983a-1d76d17e7c92';
/**
- * Transforms LokiStreamResult structure into a dataFrame. Used when doing standard queries and newer version of Loki.
+ * Transforms LokiStreamResult structure into a dataFrame. Used when doing standard queries
*/
-export function lokiStreamResultToDataFrame(stream: LokiStreamResult, reverse?: boolean, refId?: string): DataFrame {
- const labels: Labels = stream.stream;
- const labelsString = Object.entries(labels)
- .map(([key, val]) => `${key}="${val}"`)
- .sort()
- .join('');
-
+export function lokiStreamsToRawDataFrame(streams: LokiStreamResult[], reverse?: boolean, refId?: string): DataFrame {
+ const labels = new ArrayVector<{}>([]);
const times = new ArrayVector([]);
const timesNs = new ArrayVector([]);
const lines = new ArrayVector([]);
@@ -60,12 +55,21 @@ export function lokiStreamResultToDataFrame(stream: LokiStreamResult, reverse?:
// We need to store and track all used uids to ensure that uids are unique
const usedUids: { string?: number } = {};
- for (const [ts, line] of stream.values) {
- // num ns epoch in string, we convert it to iso string here so it matches old format
- times.add(new Date(parseInt(ts.slice(0, -6), 10)).toISOString());
- timesNs.add(ts);
- lines.add(line);
- uids.add(createUid(ts, labelsString, line, usedUids, refId));
+ for (const stream of streams) {
+ const streamLabels: Labels = stream.stream;
+ const labelsString = Object.entries(streamLabels)
+ .map(([key, val]) => `${key}="${val}"`)
+ .sort()
+ .join('');
+
+ for (const [ts, line] of stream.values) {
+ labels.add(streamLabels);
+ // num ns epoch in string, we convert it to iso string here so it matches old format
+ times.add(new Date(parseInt(ts.slice(0, -6), 10)).toISOString());
+ timesNs.add(ts);
+ lines.add(line);
+ uids.add(createUid(ts, labelsString, line, usedUids, refId));
+ }
}
return constructDataFrame(times, timesNs, lines, uids, labels, reverse, refId);
@@ -79,17 +83,18 @@ function constructDataFrame(
timesNs: ArrayVector,
lines: ArrayVector,
uids: ArrayVector,
- labels: Labels,
+ labels: ArrayVector<{}>,
reverse?: boolean,
refId?: string
) {
const dataFrame = {
refId,
fields: [
+ { name: 'labels', type: FieldType.other, config: {}, values: labels },
{ name: 'ts', type: FieldType.time, config: { displayName: 'Time' }, values: times }, // Time
- { name: 'line', type: FieldType.string, config: {}, values: lines, labels }, // Line - needs to be the first field with string type
- { name: 'id', type: FieldType.string, config: {}, values: uids },
+ { name: 'line', type: FieldType.string, config: {}, values: lines }, // Line - needs to be the first field with string type
{ name: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' }, values: timesNs }, // Time
+ { name: 'id', type: FieldType.string, config: {}, values: uids },
],
length: times.length,
};
@@ -128,11 +133,11 @@ export function appendResponseToBufferedData(response: LokiTailResponse, data: M
}
}
- const tsField = data.fields[0];
- const tsNsField = data.fields[1];
+ const labelsField = data.fields[0];
+ const tsField = data.fields[1];
const lineField = data.fields[2];
- const labelsField = data.fields[3];
- const idField = data.fields[4];
+ const idField = data.fields[3];
+ const tsNsField = data.fields[4];
// We are comparing used ids only within the received stream. This could be a problem if the same line + labels + nanosecond timestamp came in 2 separate batches.
// As this is very unlikely, and the result would only affect live-tailing css animation we have decided to not compare all received uids from data param as this would slow down processing.
@@ -346,20 +351,12 @@ export function lokiStreamsToDataFrames(
preferredVisualisationType: 'logs',
};
- const series: DataFrame[] = data.map((stream) => {
- const dataFrame = lokiStreamResultToDataFrame(stream, reverse, target.refId);
- enhanceDataFrame(dataFrame, config);
+ const dataFrame = lokiStreamsToRawDataFrame(data, reverse, target.refId);
+ enhanceDataFrame(dataFrame, config);
- if (meta.custom && dataFrame.fields.some((f) => f.labels && Object.keys(f.labels).some((l) => l === '__error__'))) {
- meta.custom.error = 'Error when parsing some of the logs';
- }
-
- return {
- ...dataFrame,
- refId: target.refId,
- meta,
- };
- });
+ if (meta.custom && dataFrame.fields.some((f) => f.labels && Object.keys(f.labels).some((l) => l === '__error__'))) {
+ meta.custom.error = 'Error when parsing some of the logs';
+ }
if (stats.length && !data.length) {
return [
@@ -372,7 +369,13 @@ export function lokiStreamsToDataFrames(
];
}
- return series;
+ return [
+ {
+ ...dataFrame,
+ refId: target.refId,
+ meta,
+ },
+ ];
}
/**
From ce2a9252c281aab8d1f8f974abd95dbee3cd8be7 Mon Sep 17 00:00:00 2001
From: Dimitris Sotirakis
Date: Fri, 8 Apr 2022 11:06:45 +0300
Subject: [PATCH 38/55] Replace with secret (#47488)
---
.drone.yml | 10 +++++++---
scripts/drone/pipelines/release.star | 3 ++-
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/.drone.yml b/.drone.yml
index a3de2242549..81b2be1109e 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -2934,12 +2934,14 @@ steps:
image: byrnedo/alpine-curl:0.1.8
name: grabpl
- commands:
- - ./bin/grabpl artifacts publish --security --tag ${TAG} --src-bucket grafana-prerelease
+ - ./bin/grabpl artifacts publish --security --tag ${TAG} --src-bucket $${PRERELEASE_BUCKET}
depends_on:
- grabpl
environment:
GCP_KEY:
from_secret: gcp_key
+ PRERELEASE_BUCKET:
+ from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: publish-artifacts
trigger:
@@ -2970,12 +2972,14 @@ steps:
image: byrnedo/alpine-curl:0.1.8
name: grabpl
- commands:
- - ./bin/grabpl artifacts publish --tag ${TAG} --src-bucket grafana-prerelease
+ - ./bin/grabpl artifacts publish --tag ${TAG} --src-bucket $${PRERELEASE_BUCKET}
depends_on:
- grabpl
environment:
GCP_KEY:
from_secret: gcp_key
+ PRERELEASE_BUCKET:
+ from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: publish-artifacts
trigger:
@@ -4448,6 +4452,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
-hmac: 0fbba9c5099f1a3585f866b624f198058332624a4de32193a5326450a39cb8f3
+hmac: 9b87d4bb17d422973023c6ad1a59980c37832ae60ba6bb0d11ee024ea1e35b1a
...
diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star
index de794ca93c1..0e10c9892fa 100644
--- a/scripts/drone/pipelines/release.star
+++ b/scripts/drone/pipelines/release.star
@@ -339,8 +339,9 @@ def publish_artifacts_step(mode):
'image': publish_image,
'environment': {
'GCP_KEY': from_secret('gcp_key'),
+ 'PRERELEASE_BUCKET': from_secret('prerelease_bucket'),
},
- 'commands': ['./bin/grabpl artifacts publish {}--tag ${{TAG}} --src-bucket grafana-prerelease'.format(security)],
+ 'commands': ['./bin/grabpl artifacts publish {}--tag ${{TAG}} --src-bucket $${{PRERELEASE_BUCKET}}'.format(security)],
'depends_on': ['grabpl'],
}
From c8514756be162a4b09061bfc6a859b4e214dfa99 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 8 Apr 2022 09:26:30 +0100
Subject: [PATCH 39/55] Bump actions/checkout from 2 to 3 (#46068)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/backport.yml | 2 +-
.github/workflows/bump-version.yml | 4 ++--
.github/workflows/close-milestone.yml | 2 +-
.github/workflows/codeql-analysis.yml | 2 +-
.github/workflows/commands.yml | 2 +-
.github/workflows/detect-breaking-changes-build.yml | 6 +++---
.github/workflows/detect-breaking-changes-report.yml | 2 +-
.github/workflows/github-release.yml | 2 +-
.github/workflows/metrics-collector.yml | 2 +-
.github/workflows/pr-checks.yml | 2 +-
.github/workflows/pr-codeql-analysis-go.yml | 2 +-
.github/workflows/pr-codeql-analysis-javascript.yml | 2 +-
.github/workflows/pr-codeql-analysis-python.yml | 2 +-
.github/workflows/pr-commands.yml | 2 +-
.github/workflows/publish.yml | 2 +-
.github/workflows/remove-milestone.yml | 2 +-
.github/workflows/update-changelog.yml | 2 +-
17 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index 55dc5c8da02..97042213f2c 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml
index f0aface7844..619ac66bd71 100644
--- a/.github/workflows/bump-version.yml
+++ b/.github/workflows/bump-version.yml
@@ -50,7 +50,7 @@ jobs:
example: 7.4.3 or 7.4.3-beta1"
exit 1
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set intermedia variables
id: intermedia
@@ -69,7 +69,7 @@ jobs:
exit 1
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/close-milestone.yml b/.github/workflows/close-milestone.yml
index a495eab7a8b..e798f0cd4bf 100644
--- a/.github/workflows/close-milestone.yml
+++ b/.github/workflows/close-milestone.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index c1578b4840f..6fd5101abc8 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 7b5c492590f..ac5a03f9d5c 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/detect-breaking-changes-build.yml b/.github/workflows/detect-breaking-changes-build.yml
index 87096eaf159..8e76bbe441b 100644
--- a/.github/workflows/detect-breaking-changes-build.yml
+++ b/.github/workflows/detect-breaking-changes-build.yml
@@ -11,7 +11,7 @@ jobs:
working-directory: './pr'
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
path: './pr'
@@ -51,7 +51,7 @@ jobs:
working-directory: './base'
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
path: './base'
ref: ${{ github.event.pull_request.base.ref }}
@@ -92,7 +92,7 @@ jobs:
GITHUB_STEP_NUMBER: 7
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Get built packages from pr
uses: actions/download-artifact@v2
diff --git a/.github/workflows/detect-breaking-changes-report.yml b/.github/workflows/detect-breaking-changes-report.yml
index fe5f2f1f490..fec1f33b93d 100644
--- a/.github/workflows/detect-breaking-changes-report.yml
+++ b/.github/workflows/detect-breaking-changes-report.yml
@@ -14,7 +14,7 @@ jobs:
ARTIFACT_NAME: 'levitate'
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: 'Download artifact'
uses: actions/github-script@v6
diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml
index 9f1f1fe31c5..f0223cfdcca 100644
--- a/.github/workflows/github-release.yml
+++ b/.github/workflows/github-release.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/metrics-collector.yml b/.github/workflows/metrics-collector.yml
index 84dec4e204a..874ccc4c3e7 100644
--- a/.github/workflows/metrics-collector.yml
+++ b/.github/workflows/metrics-collector.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index 16e00f6d094..bf41c507509 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -20,7 +20,7 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/pr-codeql-analysis-go.yml b/.github/workflows/pr-codeql-analysis-go.yml
index 6e6bd524cc7..d80c62f4f5c 100644
--- a/.github/workflows/pr-codeql-analysis-go.yml
+++ b/.github/workflows/pr-codeql-analysis-go.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
diff --git a/.github/workflows/pr-codeql-analysis-javascript.yml b/.github/workflows/pr-codeql-analysis-javascript.yml
index 943bbfc1543..9fc371a61ad 100644
--- a/.github/workflows/pr-codeql-analysis-javascript.yml
+++ b/.github/workflows/pr-codeql-analysis-javascript.yml
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
diff --git a/.github/workflows/pr-codeql-analysis-python.yml b/.github/workflows/pr-codeql-analysis-python.yml
index 4f887526812..d9fc18b4f68 100644
--- a/.github/workflows/pr-codeql-analysis-python.yml
+++ b/.github/workflows/pr-codeql-analysis-python.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
diff --git a/.github/workflows/pr-commands.yml b/.github/workflows/pr-commands.yml
index 7f47cfd8e92..ecd7361c550 100644
--- a/.github/workflows/pr-commands.yml
+++ b/.github/workflows/pr-commands.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cc7d6bf4953..742dc7f9bac 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v3
- run: git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync
- name: generate-packages-docs
uses: actions/setup-node@v2.5.1
diff --git a/.github/workflows/remove-milestone.yml b/.github/workflows/remove-milestone.yml
index ac2504a45f8..78305864e09 100644
--- a/.github/workflows/remove-milestone.yml
+++ b/.github/workflows/remove-milestone.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml
index 9ea0da2388e..fe03bdeed01 100644
--- a/.github/workflows/update-changelog.yml
+++ b/.github/workflows/update-changelog.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
From 2cf88cfec88373a61a22faf62469d50a35d21835 Mon Sep 17 00:00:00 2001
From: Serge Zaitsev
Date: Fri, 8 Apr 2022 10:33:19 +0200
Subject: [PATCH 40/55] Chore: Remove bus from contexthandler (#47458)
* Chore: remove bus from contexthandler
* remove bus from orgredirect
---
pkg/api/common_test.go | 4 +++-
pkg/api/http_server.go | 2 +-
pkg/middleware/middleware_basic_auth_test.go | 6 ++----
pkg/middleware/middleware_test.go | 6 ++++--
pkg/middleware/org_redirect.go | 6 +++---
pkg/middleware/org_redirect_test.go | 4 +---
pkg/middleware/recovery_test.go | 2 +-
pkg/services/contexthandler/auth_jwt.go | 3 +--
pkg/services/contexthandler/auth_proxy_test.go | 9 ++++++++-
pkg/services/contexthandler/contexthandler.go | 10 +++++++---
pkg/services/login/logintest/logintest.go | 10 ++++++++++
pkg/services/sqlstore/mockstore/mockstore.go | 5 +++--
12 files changed, 44 insertions(+), 23 deletions(-)
diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go
index b256c1454d7..f1870cca517 100644
--- a/pkg/api/common_test.go
+++ b/pkg/api/common_test.go
@@ -197,7 +197,9 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, sqlStore)
- ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy)
+ loginService := &logintest.LoginServiceFake{}
+ authenticator := &logintest.AuthenticatorFake{}
+ ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, authenticator)
return ctxHdlr
}
diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go
index 6d026fc5f26..fc87b642ca5 100644
--- a/pkg/api/http_server.go
+++ b/pkg/api/http_server.go
@@ -507,7 +507,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(hs.pluginMetricsEndpoint)
m.Use(hs.ContextHandler.Middleware)
- m.Use(middleware.OrgRedirect(hs.Cfg))
+ m.Use(middleware.OrgRedirect(hs.Cfg, hs.SQLStore))
m.Use(acmiddleware.LoadPermissionsMiddleware(hs.AccessControl))
// needs to be after context handler
diff --git a/pkg/middleware/middleware_basic_auth_test.go b/pkg/middleware/middleware_basic_auth_test.go
index 7588d2172e1..dda1b609269 100644
--- a/pkg/middleware/middleware_basic_auth_test.go
+++ b/pkg/middleware/middleware_basic_auth_test.go
@@ -88,6 +88,7 @@ func TestMiddlewareBasicAuth(t *testing.T) {
}, configure)
middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {
+ sc.mockSQLStore.ExpectedError = models.ErrUserNotFound
sc.fakeReq("GET", "/")
sc.req.SetBasicAuth("user", "password")
sc.exec()
@@ -100,10 +101,7 @@ func TestMiddlewareBasicAuth(t *testing.T) {
}, configure)
middlewareScenario(t, "Should return error if user & password do not match", func(t *testing.T, sc *scenarioContext) {
- bus.AddHandler("user-query", func(ctx context.Context, loginUserQuery *models.GetUserByLoginQuery) error {
- return nil
- })
-
+ sc.mockSQLStore.ExpectedError = models.ErrUserNotFound
sc.fakeReq("GET", "/")
sc.req.SetBasicAuth("killa", "gorilla")
sc.exec()
diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go
index fe645b87cd2..b52083dbeed 100644
--- a/pkg/middleware/middleware_test.go
+++ b/pkg/middleware/middleware_test.go
@@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/login/loginservice"
+ "github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
@@ -602,7 +603,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
sc.sqlStore = ctxHdlr.SQLStore
sc.contextHandler = ctxHdlr
sc.m.Use(ctxHdlr.Middleware)
- sc.m.Use(OrgRedirect(sc.cfg))
+ sc.m.Use(OrgRedirect(sc.cfg, sc.mockSQLStore))
sc.userAuthTokenService = ctxHdlr.AuthTokenService.(*auth.FakeUserAuthTokenService)
sc.jwtAuthService = ctxHdlr.JWTAuthService.(*models.FakeJWTService)
@@ -644,8 +645,9 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *mockstore.S
authJWTSvc := models.NewFakeJWTService()
tracer, err := tracing.InitializeTracerForTest()
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, mockSQLStore)
+ authenticator := &logintest.AuthenticatorFake{ExpectedUser: &models.User{}}
require.NoError(t, err)
- return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy)
+ return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, authenticator)
}
type fakeRenderService struct {
diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go
index e6f8b638f52..ecf0d0800b0 100644
--- a/pkg/middleware/org_redirect.go
+++ b/pkg/middleware/org_redirect.go
@@ -6,16 +6,16 @@ import (
"strconv"
"strings"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
// OrgRedirect changes org and redirects users if the
// querystring `orgId` doesn't match the active org.
-func OrgRedirect(cfg *setting.Cfg) web.Handler {
+func OrgRedirect(cfg *setting.Cfg, store sqlstore.Store) web.Handler {
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
orgIdValue := req.URL.Query().Get("orgId")
orgId, err := strconv.ParseInt(orgIdValue, 10, 64)
@@ -34,7 +34,7 @@ func OrgRedirect(cfg *setting.Cfg) web.Handler {
}
cmd := models.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId}
- if err := bus.Dispatch(ctx.Req.Context(), &cmd); err != nil {
+ if err := store.SetUsingOrg(ctx.Req.Context(), &cmd); err != nil {
if ctx.IsApiRequest() {
ctx.JsonApiErr(404, "Not found", nil)
} else {
diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go
index 5917e9a5377..22450ec880e 100644
--- a/pkg/middleware/org_redirect_test.go
+++ b/pkg/middleware/org_redirect_test.go
@@ -68,9 +68,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
middlewareScenario(t, "when setting an invalid org for user", func(t *testing.T, sc *scenarioContext) {
sc.withTokenSessionCookie("token")
- bus.AddHandler("test", func(ctx context.Context, query *models.SetUsingOrgCommand) error {
- return fmt.Errorf("")
- })
+ sc.mockSQLStore.ExpectedSetUsingOrgError = fmt.Errorf("")
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 1, UserId: 12}
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go
index 51b1c6c1c4d..7e3e4a5939f 100644
--- a/pkg/middleware/recovery_test.go
+++ b/pkg/middleware/recovery_test.go
@@ -73,7 +73,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
contextHandler := getContextHandler(t, nil, nil, nil)
sc.m.Use(contextHandler.Middleware)
// mock out gc goroutine
- sc.m.Use(OrgRedirect(cfg))
+ sc.m.Use(OrgRedirect(cfg, sc.mockSQLStore))
sc.defaultHandler = func(c *models.ReqContext) {
sc.context = c
diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go
index d29b4b07d65..cda763f2c37 100644
--- a/pkg/services/contexthandler/auth_jwt.go
+++ b/pkg/services/contexthandler/auth_jwt.go
@@ -3,7 +3,6 @@ package contexthandler
import (
"errors"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
)
@@ -67,7 +66,7 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
SignupAllowed: h.Cfg.JWTAuthAutoSignUp,
ExternalUser: extUser,
}
- if err := bus.Dispatch(ctx.Req.Context(), upsert); err != nil {
+ if err := h.loginService.UpsertUser(ctx.Req.Context(), upsert); err != nil {
ctx.Logger.Error("Failed to upsert JWT user", "error", err)
return false
}
diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go
index 82e6245c428..48fea6227bb 100644
--- a/pkg/services/contexthandler/auth_proxy_test.go
+++ b/pkg/services/contexthandler/auth_proxy_test.go
@@ -84,8 +84,9 @@ func getContextHandler(t *testing.T) *ContextHandler {
loginService := loginservice.LoginServiceMock{ExpectedUser: &models.User{Id: userID}}
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, &FakeGetSignUserStore{})
+ authenticator := &fakeAuthenticator{}
- return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy)
+ return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, authenticator)
}
type FakeGetSignUserStore struct {
@@ -103,3 +104,9 @@ func (f *FakeGetSignUserStore) GetSignedInUser(ctx context.Context, query *model
}
return nil
}
+
+type fakeAuthenticator struct{}
+
+func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error {
+ return nil
+}
diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go
index 99111ac9194..407b89fa326 100644
--- a/pkg/services/contexthandler/contexthandler.go
+++ b/pkg/services/contexthandler/contexthandler.go
@@ -9,12 +9,12 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
+ loginpkg "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
@@ -36,7 +36,7 @@ const ServiceName = "ContextHandler"
func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, jwtService models.JWTService,
remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore sqlstore.Store,
- tracer tracing.Tracer, authProxy *authproxy.AuthProxy) *ContextHandler {
+ tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service, authenticator loginpkg.Authenticator) *ContextHandler {
return &ContextHandler{
Cfg: cfg,
AuthTokenService: tokenService,
@@ -46,6 +46,8 @@ func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, jwtS
SQLStore: sqlStore,
tracer: tracer,
authProxy: authProxy,
+ authenticator: authenticator,
+ loginService: loginService,
}
}
@@ -59,6 +61,8 @@ type ContextHandler struct {
SQLStore sqlstore.Store
tracer tracing.Tracer
authProxy *authproxy.AuthProxy
+ authenticator loginpkg.Authenticator
+ loginService login.Service
// GetTime returns the current time.
// Stubbable by tests.
GetTime func() time.Time
@@ -291,7 +295,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext,
Password: password,
Cfg: h.Cfg,
}
- if err := bus.Dispatch(reqContext.Req.Context(), &authQuery); err != nil {
+ if err := h.authenticator.AuthenticateUser(reqContext.Req.Context(), &authQuery); err != nil {
reqContext.Logger.Debug(
"Failed to authorize the user",
"username", username,
diff --git a/pkg/services/login/logintest/logintest.go b/pkg/services/login/logintest/logintest.go
index 6197e8d5ce9..020714632e8 100644
--- a/pkg/services/login/logintest/logintest.go
+++ b/pkg/services/login/logintest/logintest.go
@@ -49,3 +49,13 @@ func (a *AuthInfoServiceFake) GetExternalUserInfoByLogin(ctx context.Context, qu
query.Result = a.ExpectedExternalUser
return a.ExpectedError
}
+
+type AuthenticatorFake struct {
+ ExpectedUser *models.User
+ ExpectedError error
+}
+
+func (a *AuthenticatorFake) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error {
+ query.User = a.ExpectedUser
+ return a.ExpectedError
+}
diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go
index 2094f0100da..f66b16b723e 100644
--- a/pkg/services/sqlstore/mockstore/mockstore.go
+++ b/pkg/services/sqlstore/mockstore/mockstore.go
@@ -44,7 +44,8 @@ type SQLStoreMock struct {
ExpectedUserStars map[int64]bool
ExpectedLoginAttempts int64
- ExpectedError error
+ ExpectedError error
+ ExpectedSetUsingOrgError error
}
func NewSQLStoreMock() *SQLStoreMock {
@@ -178,7 +179,7 @@ func (m *SQLStoreMock) UpdateUserLastSeenAt(ctx context.Context, cmd *models.Upd
}
func (m *SQLStoreMock) SetUsingOrg(ctx context.Context, cmd *models.SetUsingOrgCommand) error {
- return m.ExpectedError
+ return m.ExpectedSetUsingOrgError
}
func (m *SQLStoreMock) GetUserProfile(ctx context.Context, query *models.GetUserProfileQuery) error {
From 9574f447bbcf8a60ff1d693041f294d7e9a0aec2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Farkas?=
Date: Fri, 8 Apr 2022 11:00:16 +0200
Subject: [PATCH 41/55] grafana-data: removed DataQueryRequest.reverse (#47487)
---
packages/grafana-data/src/types/datasource.ts | 1 -
public/app/plugins/datasource/loki/datasource.ts | 3 +--
public/app/plugins/datasource/loki/result_transformer.ts | 5 ++---
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts
index 94276a9114d..69717854e2b 100644
--- a/packages/grafana-data/src/types/datasource.ts
+++ b/packages/grafana-data/src/types/datasource.ts
@@ -481,7 +481,6 @@ export interface DataQueryRequest {
intervalMs: number;
maxDataPoints?: number;
range: TimeRange;
- reverse?: boolean;
scopedVars: ScopedVars;
targets: TQuery[];
timezone: string;
diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts
index 1259918bdfc..944d62bbe07 100644
--- a/public/app/plugins/datasource/loki/datasource.ts
+++ b/public/app/plugins/datasource/loki/datasource.ts
@@ -309,8 +309,7 @@ export class LokiDatasource
responseListLength,
maxDataPoints,
this.instanceSettings.jsonData,
- (options as DataQueryRequest).scopedVars,
- (options as DataQueryRequest).reverse
+ (options as DataQueryRequest).scopedVars
)
)
);
diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts
index 8270288c32f..31760ee0d08 100644
--- a/public/app/plugins/datasource/loki/result_transformer.ts
+++ b/public/app/plugins/datasource/loki/result_transformer.ts
@@ -520,13 +520,12 @@ export function processRangeQueryResponse(
responseListLength: number,
limit: number,
config: LokiOptions,
- scopedVars: ScopedVars,
- reverse = false
+ scopedVars: ScopedVars
) {
switch (response.data.resultType) {
case LokiResultType.Stream:
return of({
- data: lokiStreamsToDataFrames(response as LokiStreamResponse, target, limit, config, reverse),
+ data: lokiStreamsToDataFrames(response as LokiStreamResponse, target, limit, config),
key: `${target.refId}_log`,
});
From beed3fd5deab3144377eef615d2275491bd1838d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Farkas?=
Date: Fri, 8 Apr 2022 11:24:09 +0200
Subject: [PATCH 42/55] loki: remove unused reverse-attribute (#47494)
---
public/app/plugins/datasource/loki/types.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts
index c72954e7c38..da887ec18a9 100644
--- a/public/app/plugins/datasource/loki/types.ts
+++ b/public/app/plugins/datasource/loki/types.ts
@@ -35,7 +35,6 @@ export interface LokiQuery extends DataQuery {
expr: string;
query?: string;
format?: string;
- reverse?: boolean;
legendFormat?: string;
valueWithRefId?: boolean;
maxLines?: number;
From 416da59c438b64bcc8f75453c15d34a34e3ccbc9 Mon Sep 17 00:00:00 2001
From: Josh Hunt
Date: Fri, 8 Apr 2022 10:44:17 +0100
Subject: [PATCH 43/55] Routing: Fix links to different port from being treated
as internal links (#45192)
* Add some failing tests that trigger the bug
* start at refactor of stripBaseFromUrl
---
.../grafana-data/src/utils/location.test.ts | 132 +++++++++++-------
packages/grafana-data/src/utils/location.ts | 29 +++-
2 files changed, 108 insertions(+), 53 deletions(-)
diff --git a/packages/grafana-data/src/utils/location.test.ts b/packages/grafana-data/src/utils/location.test.ts
index 174a505601d..b56660bbd01 100644
--- a/packages/grafana-data/src/utils/location.test.ts
+++ b/packages/grafana-data/src/utils/location.test.ts
@@ -3,7 +3,7 @@ import { locationUtil } from './location';
describe('locationUtil', () => {
const { location } = window;
- beforeAll(() => {
+ beforeEach(() => {
// @ts-ignore
delete window.location;
@@ -21,63 +21,97 @@ describe('locationUtil', () => {
};
});
- afterAll(() => {
+ afterEach(() => {
window.location = location;
});
- describe('strip base when appSubUrl configured', () => {
- beforeEach(() => {
- locationUtil.initialize({
- config: { appSubUrl: '/subUrl' } as any,
- getVariablesUrlParams: (() => {}) as any,
- getTimeRangeForUrl: (() => {}) as any,
+ describe('stripBaseFromUrl', () => {
+ describe('when appSubUrl configured', () => {
+ beforeEach(() => {
+ locationUtil.initialize({
+ config: { appSubUrl: '/subUrl' } as any,
+ getVariablesUrlParams: (() => {}) as any,
+ getTimeRangeForUrl: (() => {}) as any,
+ });
});
- });
- test('relative url', () => {
- const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/');
- expect(urlWithoutMaster).toBe('/thisShouldRemain/');
- });
- test('relative url with multiple subUrl in path', () => {
- const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/subUrl/');
- expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
- });
- test('relative url with subdirectory subUrl', () => {
- const urlWithoutMaster = locationUtil.stripBaseFromUrl('/thisShouldRemain/subUrl/');
- expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
- });
- test('absolute url', () => {
- const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/thisShouldRemain/');
- expect(urlWithoutMaster).toBe('/thisShouldRemain/');
- });
- test('absolute url with multiple subUrl in path', () => {
- const urlWithoutMaster = locationUtil.stripBaseFromUrl(
- 'http://www.domain.com:9877/subUrl/thisShouldRemain/subUrl/'
- );
- expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
- });
- test('absolute url with subdirectory subUrl', () => {
- const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/thisShouldRemain/subUrl/');
- expect(urlWithoutMaster).toBe('http://www.domain.com:9877/thisShouldRemain/subUrl/');
- });
- });
-
- describe('strip base when appSubUrl not configured', () => {
- beforeEach(() => {
- locationUtil.initialize({
- config: {} as any,
- getVariablesUrlParams: (() => {}) as any,
- getTimeRangeForUrl: (() => {}) as any,
+ test('relative url', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/');
+ expect(urlWithoutMaster).toBe('/thisShouldRemain/');
+ });
+ test('relative url with multiple subUrl in path', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/subUrl/');
+ expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
+ });
+ test('relative url with subdirectory subUrl', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('/thisShouldRemain/subUrl/');
+ expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
+ });
+ test('absolute url', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/thisShouldRemain/');
+ expect(urlWithoutMaster).toBe('/thisShouldRemain/');
+ });
+ test('absolute url with multiple subUrl in path', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl(
+ 'http://www.domain.com:9877/subUrl/thisShouldRemain/subUrl/'
+ );
+ expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
+ });
+ test('absolute url with subdirectory subUrl', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/thisShouldRemain/subUrl/');
+ expect(urlWithoutMaster).toBe('http://www.domain.com:9877/thisShouldRemain/subUrl/');
});
});
- test('relative url', () => {
- const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
- expect(urlWithoutMaster).toBe('/subUrl/grafana/');
+ describe('when appSubUrl not configured', () => {
+ beforeEach(() => {
+ locationUtil.initialize({
+ config: {} as any,
+ getVariablesUrlParams: (() => {}) as any,
+ getTimeRangeForUrl: (() => {}) as any,
+ });
+ });
+
+ test('relative url', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
+ expect(urlWithoutMaster).toBe('/subUrl/grafana/');
+ });
+
+ test('absolute url', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
+ expect(urlWithoutMaster).toBe('/subUrl/grafana/');
+ });
});
- test('absolute url', () => {
- const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
- expect(urlWithoutMaster).toBe('/subUrl/grafana/');
+ describe('when origin does not have a port in it', () => {
+ beforeEach(() => {
+ window.location = {
+ ...location,
+ hash: '#hash',
+ host: 'www.domain.com',
+ hostname: 'www.domain.com',
+ href: 'http://www.domain.com/path/b?search=a&b=c&d#hash',
+ origin: 'http://www.domain.com',
+ pathname: '/path/b',
+ port: '',
+ protocol: 'http:',
+ search: '?search=a&b=c&d',
+ };
+ });
+
+ test('relative url', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
+ expect(urlWithoutMaster).toBe('/subUrl/grafana/');
+ });
+
+ test('URL with same host, different port', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
+ expect(urlWithoutMaster).toBe('http://www.domain.com:9877/subUrl/grafana/');
+ });
+
+ test('URL of a completely different origin', () => {
+ const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.another-domain.com/subUrl/grafana/');
+ expect(urlWithoutMaster).toBe('http://www.another-domain.com/subUrl/grafana/');
+ });
});
});
diff --git a/packages/grafana-data/src/utils/location.ts b/packages/grafana-data/src/utils/location.ts
index 83f481067bb..7df8796b41b 100644
--- a/packages/grafana-data/src/utils/location.ts
+++ b/packages/grafana-data/src/utils/location.ts
@@ -7,22 +7,43 @@ let grafanaConfig: GrafanaConfig = { appSubUrl: '' } as any;
let getTimeRangeUrlParams: () => RawTimeRange;
let getVariablesUrlParams: (scopedVars?: ScopedVars) => UrlQueryMap;
+const maybeParseUrl = (input: string): URL | undefined => {
+ try {
+ return new URL(input);
+ } catch {
+ return undefined;
+ }
+};
+
/**
*
* @param url
* @internal
*/
-const stripBaseFromUrl = (url: string): string => {
+const stripBaseFromUrl = (urlOrPath: string): string => {
+ // Will only return a URL object if the input is actually a valid URL
+ const parsedUrl = maybeParseUrl(urlOrPath);
+ if (parsedUrl) {
+ // If the input is a URL, and for a different origin that we're on, just bail
+ // and return it. There's no need to strip anything from it
+ if (parsedUrl.origin !== window.location.origin) {
+ return urlOrPath;
+ }
+ }
+
const appSubUrl = grafanaConfig.appSubUrl ?? '';
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
- const isAbsoluteUrl = url.startsWith('http');
+ const isAbsoluteUrl = urlOrPath.startsWith('http');
+
let segmentToStrip = appSubUrl;
- if (!url.startsWith('/') || isAbsoluteUrl) {
+ if (!urlOrPath.startsWith('/') || isAbsoluteUrl) {
segmentToStrip = `${window.location.origin}${appSubUrl}`;
}
- return url.length > 0 && url.indexOf(segmentToStrip) === 0 ? url.slice(segmentToStrip.length - stripExtraChars) : url;
+ return urlOrPath.length > 0 && urlOrPath.indexOf(segmentToStrip) === 0
+ ? urlOrPath.slice(segmentToStrip.length - stripExtraChars)
+ : urlOrPath;
};
/**
From f1a1070d41993c0dabba8162082fc8a22b2ac738 Mon Sep 17 00:00:00 2001
From: Artur Wierzbicki
Date: Fri, 8 Apr 2022 12:42:33 +0200
Subject: [PATCH 44/55] UsageStats: track enabled features (#47407)
* #47127: include enabled features in the usage stats reports
* #47127: convert feature names to snake cased metric names
* #47127: remove dead code
* #47127: lint fix
* #47127: convert GetUsageStats to return `map[string]interface{}`
* #47127: fix testssssssss
* #47127: fix testssssssss
---
pkg/infra/usagestats/service/service.go | 5 +-
pkg/infra/usagestats/service/usage_stats.go | 5 +
.../usagestats/service/usage_stats_test.go | 10 ++
pkg/services/featuremgmt/strcase/snake.go | 118 ++++++++++++++++++
pkg/services/featuremgmt/usage_stats.go | 21 ++++
pkg/services/featuremgmt/usage_stats_test.go | 34 +++++
6 files changed, 192 insertions(+), 1 deletion(-)
create mode 100644 pkg/services/featuremgmt/strcase/snake.go
create mode 100644 pkg/services/featuremgmt/usage_stats.go
create mode 100644 pkg/services/featuremgmt/usage_stats_test.go
diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go
index ceb8a321fbc..486f986f697 100644
--- a/pkg/infra/usagestats/service/service.go
+++ b/pkg/infra/usagestats/service/service.go
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
@@ -22,6 +23,7 @@ type UsageStats struct {
SocialService social.Service
kvStore *kvstore.NamespacedKVStore
RouteRegister routing.RouteRegister
+ features *featuremgmt.FeatureManager
log log.Logger
@@ -33,11 +35,12 @@ type UsageStats struct {
}
func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugins.Store,
- socialService social.Service, kvStore kvstore.KVStore, routeRegister routing.RouteRegister,
+ socialService social.Service, kvStore kvstore.KVStore, routeRegister routing.RouteRegister, features *featuremgmt.FeatureManager,
) *UsageStats {
s := &UsageStats{
Cfg: cfg,
SQLStore: sqlStore,
+ features: features,
oauthProviders: socialService.GetOAuthProviders(),
RouteRegister: routeRegister,
pluginStore: pluginStore,
diff --git a/pkg/infra/usagestats/service/usage_stats.go b/pkg/infra/usagestats/service/usage_stats.go
index b67e1db0aa9..a9e7b4fdf6b 100644
--- a/pkg/infra/usagestats/service/usage_stats.go
+++ b/pkg/infra/usagestats/service/usage_stats.go
@@ -230,6 +230,11 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e
metrics["stats.uptime"] = int64(time.Since(uss.startTime).Seconds())
+ featureUsageStats := uss.features.GetUsageStats(ctx)
+ for name, val := range featureUsageStats {
+ metrics[name] = val
+ }
+
return report, nil
}
diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go
index 13acd1155aa..a150e26d316 100644
--- a/pkg/infra/usagestats/service/usage_stats_test.go
+++ b/pkg/infra/usagestats/service/usage_stats_test.go
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
@@ -405,6 +406,14 @@ func TestMetrics(t *testing.T) {
metric := report.Metrics[metricName]
assert.Equal(t, 1, metric)
})
+
+ t.Run("Should include feature usage stats", func(t *testing.T) {
+ report, err := uss.GetUsageReport(context.Background())
+ require.NoError(t, err, "Expected no error")
+
+ assert.Equal(t, 1, report.Metrics["stats.features.feature_1.count"])
+ assert.Equal(t, 1, report.Metrics["stats.features.feature_2.count"])
+ })
})
t.Run("When registering external metrics", func(t *testing.T) {
@@ -511,5 +520,6 @@ func createService(t *testing.T, cfg setting.Cfg, sqlStore sqlstore.Store, withD
log: log.New("infra.usagestats"),
startTime: time.Now().Add(-1 * time.Minute),
RouteRegister: routing.NewRouteRegister(),
+ features: featuremgmt.WithFeatures("feature1", "feature2"),
}
}
diff --git a/pkg/services/featuremgmt/strcase/snake.go b/pkg/services/featuremgmt/strcase/snake.go
new file mode 100644
index 00000000000..012fdb181d2
--- /dev/null
+++ b/pkg/services/featuremgmt/strcase/snake.go
@@ -0,0 +1,118 @@
+// https://github.com/iancoleman/strcase/blob/master/snake.go
+
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Ian Coleman
+ * Copyright (c) 2018 Ma_124,
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, Subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or Substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package strcase
+
+import (
+ "strings"
+)
+
+// ToSnake converts a string to snake_case
+func ToSnake(s string) string {
+ return ToDelimited(s, '_')
+}
+
+func ToSnakeWithIgnore(s string, ignore string) string {
+ return ToScreamingDelimited(s, '_', ignore, false)
+}
+
+// ToScreamingSnake converts a string to SCREAMING_SNAKE_CASE
+func ToScreamingSnake(s string) string {
+ return ToScreamingDelimited(s, '_', "", true)
+}
+
+// ToKebab converts a string to kebab-case
+func ToKebab(s string) string {
+ return ToDelimited(s, '-')
+}
+
+// ToScreamingKebab converts a string to SCREAMING-KEBAB-CASE
+func ToScreamingKebab(s string) string {
+ return ToScreamingDelimited(s, '-', "", true)
+}
+
+// ToDelimited converts a string to delimited.snake.case
+// (in this case `delimiter = '.'`)
+func ToDelimited(s string, delimiter uint8) string {
+ return ToScreamingDelimited(s, delimiter, "", false)
+}
+
+// ToScreamingDelimited converts a string to SCREAMING.DELIMITED.SNAKE.CASE
+// (in this case `delimiter = '.'; screaming = true`)
+// or delimited.snake.case
+// (in this case `delimiter = '.'; screaming = false`)
+//nolint: gocyclo
+func ToScreamingDelimited(s string, delimiter uint8, ignore string, screaming bool) string {
+ s = strings.TrimSpace(s)
+ n := strings.Builder{}
+ n.Grow(len(s) + 2) // nominal 2 bytes of extra space for inserted delimiters
+ for i, v := range []byte(s) {
+ vIsCap := v >= 'A' && v <= 'Z'
+ vIsLow := v >= 'a' && v <= 'z'
+ if vIsLow && screaming {
+ v += 'A'
+ v -= 'a'
+ } else if vIsCap && !screaming {
+ v += 'a'
+ v -= 'A'
+ }
+
+ // treat acronyms as words, eg for JSONData -> JSON is a whole word
+ if i+1 < len(s) {
+ next := s[i+1]
+ vIsNum := v >= '0' && v <= '9'
+ nextIsCap := next >= 'A' && next <= 'Z'
+ nextIsLow := next >= 'a' && next <= 'z'
+ nextIsNum := next >= '0' && next <= '9'
+ // add underscore if next letter case type is changed
+ if (vIsCap && (nextIsLow || nextIsNum)) || (vIsLow && (nextIsCap || nextIsNum)) || (vIsNum && (nextIsCap || nextIsLow)) {
+ prevIgnore := ignore != "" && i > 0 && strings.ContainsAny(string(s[i-1]), ignore)
+ if !prevIgnore {
+ if vIsCap && nextIsLow {
+ if prevIsCap := i > 0 && s[i-1] >= 'A' && s[i-1] <= 'Z'; prevIsCap {
+ n.WriteByte(delimiter)
+ }
+ }
+ n.WriteByte(v)
+ if vIsLow || vIsNum || nextIsNum {
+ n.WriteByte(delimiter)
+ }
+ continue
+ }
+ }
+ }
+
+ if (v == ' ' || v == '_' || v == '-' || v == '.') && !strings.ContainsAny(string(v), ignore) {
+ // replace space/underscore/hyphen/dot with delimiter
+ n.WriteByte(delimiter)
+ } else {
+ n.WriteByte(v)
+ }
+ }
+
+ return n.String()
+}
diff --git a/pkg/services/featuremgmt/usage_stats.go b/pkg/services/featuremgmt/usage_stats.go
new file mode 100644
index 00000000000..f33e77834a6
--- /dev/null
+++ b/pkg/services/featuremgmt/usage_stats.go
@@ -0,0 +1,21 @@
+package featuremgmt
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/grafana/grafana/pkg/services/featuremgmt/strcase"
+)
+
+func (fm *FeatureManager) GetUsageStats(ctx context.Context) map[string]interface{} {
+ enabled := fm.GetEnabled(ctx)
+ stats := make(map[string]interface{}, len(enabled))
+ for featureName := range enabled {
+ stats[asMetricName(featureName)] = 1
+ }
+ return stats
+}
+
+func asMetricName(featureName string) string {
+ return fmt.Sprintf("stats.features.%s.count", strcase.ToSnake(featureName))
+}
diff --git a/pkg/services/featuremgmt/usage_stats_test.go b/pkg/services/featuremgmt/usage_stats_test.go
new file mode 100644
index 00000000000..1bc5420e4ff
--- /dev/null
+++ b/pkg/services/featuremgmt/usage_stats_test.go
@@ -0,0 +1,34 @@
+package featuremgmt
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestFeatureUsageStats(t *testing.T) {
+ featureManagerWithAllFeatures := WithFeatures(
+ "trimDefaults",
+ "httpclientprovider_azure_auth",
+ "service-accounts",
+ "database_metrics",
+ "dashboardPreviews",
+ "live-config",
+ "showFeatureFlagsInUI",
+ "UPPER_SNAKE_CASE",
+ "feature.with.a.dot",
+ )
+
+ require.Equal(t, map[string]interface{}{
+ "stats.features.trim_defaults.count": 1,
+ "stats.features.httpclientprovider_azure_auth.count": 1,
+ "stats.features.service_accounts.count": 1,
+ "stats.features.database_metrics.count": 1,
+ "stats.features.dashboard_previews.count": 1,
+ "stats.features.live_config.count": 1,
+ "stats.features.show_feature_flags_in_ui.count": 1,
+ "stats.features.upper_snake_case.count": 1,
+ "stats.features.feature_with_a_dot.count": 1,
+ }, featureManagerWithAllFeatures.GetUsageStats(context.Background()))
+}
From 87383b1c8b27a4b0b8cc59714ae97720505bc7b9 Mon Sep 17 00:00:00 2001
From: Konrad Lalik
Date: Fri, 8 Apr 2022 13:34:02 +0200
Subject: [PATCH 45/55] Alerting: FGAC for alert rules page (#47418)
* Apply FGAC on the alert rules list page
* Add tests for edit, delete and silence buttons
* Unify access-control helpers
* Fix import
* Add route permissions for alert groups, unify access control helpers
* Improve buttons with data source explore permission
* Fix test
---
public/app/features/alerting/routes.tsx | 21 ++
.../alerting/unified/AlertGroups.test.tsx | 1 +
.../components/alert-groups/AlertDetails.tsx | 28 ++-
.../rule-viewer/RuleViewerVisualization.tsx | 32 +--
.../components/rules/RuleDetails.test.tsx | 194 ++++++++++++++++++
.../unified/components/rules/RuleDetails.tsx | 12 +-
.../rules/RuleDetailsActionButtons.tsx | 72 ++++---
.../unified/components/rules/RulesGroup.tsx | 6 +-
.../alerting/unified/utils/access-control.ts | 63 ++++--
9 files changed, 351 insertions(+), 78 deletions(-)
create mode 100644 public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx
diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx
index 1e64912cc57..ea93183aa40 100644
--- a/public/app/features/alerting/routes.tsx
+++ b/public/app/features/alerting/routes.tsx
@@ -6,6 +6,7 @@ import { RouteDescriptor } from 'app/core/navigation/types';
import { uniq } from 'lodash';
import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types';
+import { OrgRole } from '@grafana/data';
import { evaluateAccess } from './unified/utils/access-control';
const commonRoutes: RouteDescriptor[] = [
@@ -90,6 +91,10 @@ const unifiedRoutes: RouteDescriptor[] = [
...commonRoutes,
{
path: '/alerting/list',
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead],
+ [OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertRuleListIndex" */ 'app/features/alerting/unified/RuleList')
),
@@ -214,6 +219,10 @@ const unifiedRoutes: RouteDescriptor[] = [
},
{
path: '/alerting/groups/',
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead],
+ [OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups')
),
@@ -221,6 +230,10 @@ const unifiedRoutes: RouteDescriptor[] = [
{
path: '/alerting/new',
pageClass: 'page-alerting',
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite],
+ [OrgRole.Editor, OrgRole.Admin]
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor')
),
@@ -228,6 +241,10 @@ const unifiedRoutes: RouteDescriptor[] = [
{
path: '/alerting/:id/edit',
pageClass: 'page-alerting',
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleExternalWrite],
+ [OrgRole.Editor, OrgRole.Admin]
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor')
),
@@ -235,6 +252,10 @@ const unifiedRoutes: RouteDescriptor[] = [
{
path: '/alerting/:sourceName/:id/view',
pageClass: 'page-alerting',
+ roles: evaluateAccess(
+ [AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead],
+ [OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
+ ),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertingRule"*/ 'app/features/alerting/unified/RuleViewer')
),
diff --git a/public/app/features/alerting/unified/AlertGroups.test.tsx b/public/app/features/alerting/unified/AlertGroups.test.tsx
index ec9704c7ede..765af678612 100644
--- a/public/app/features/alerting/unified/AlertGroups.test.tsx
+++ b/public/app/features/alerting/unified/AlertGroups.test.tsx
@@ -16,6 +16,7 @@ jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
isEditor: true,
hasAccess: () => true,
+ hasPermission: () => true,
},
}));
const mocks = {
diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx
index 53818045141..878620f5fd5 100644
--- a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx
+++ b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx
@@ -3,11 +3,13 @@ import { GrafanaTheme2 } from '@grafana/data';
import { LinkButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
+import { AccessControlAction } from 'app/types';
import React, { FC } from 'react';
+import { getInstancesPermissions } from '../../utils/access-control';
+import { isGrafanaRulesSource } from '../../utils/datasource';
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
import { AnnotationDetailsField } from '../AnnotationDetailsField';
import { Authorize } from '../Authorize';
-import { getInstancesPermissions } from '../../utils/access-control';
interface AmNotificationsAlertDetailsProps {
alertManagerSourceName: string;
@@ -16,11 +18,19 @@ interface AmNotificationsAlertDetailsProps {
export const AlertDetails: FC = ({ alert, alertManagerSourceName }) => {
const styles = useStyles2(getStyles);
- const permissions = getInstancesPermissions(alertManagerSourceName);
+ const instancePermissions = getInstancesPermissions(alertManagerSourceName);
+
+ // For Grafana Managed alerts the Generator URL redirects to the alert rule edit page, so update permission is required
+ // For external alert manager the Generator URL redirects to an external service which we don't control
+ const isGrafanaSource = isGrafanaRulesSource(alertManagerSourceName);
+ const isSeeSourceButtonEnabled = isGrafanaSource
+ ? contextSrv.hasPermission(AccessControlAction.AlertingRuleUpdate)
+ : true;
+
return (
<>
-
+
{alert.status.state === AlertState.Suppressed && (
= ({ alert, aler
)}
-
- {alert.generatorURL && (
-
- See source
-
- )}
-
+ {isSeeSourceButtonEnabled && alert.generatorURL && (
+
+ See source
+
+ )}
{Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => (
diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx
index 26eb3ed2fd0..5d419399fcc 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx
@@ -9,6 +9,8 @@ import { AlertQuery } from 'app/types/unified-alerting-dto';
import AutoSizer from 'react-virtualized-auto-sizer';
import { PanelPluginsButtonGroup, SupportedPanelPlugins } from '../PanelPluginsButtonGroup';
import { TABLE, TIMESERIES } from '../../utils/constants';
+import { Authorize } from '../Authorize';
+import { AccessControlAction } from 'app/types';
type RuleViewerVisualizationProps = {
data?: PanelData;
@@ -91,20 +93,22 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS
/>
) : null}
- {!isExpressionQuery(query.model) && (
- <>
-
-
- View in Explore
-
- >
- )}
+
+ {!isExpressionQuery(query.model) && (
+ <>
+
+
+ View in Explore
+
+ >
+ )}
+
{
+ mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
+
+ describe('Grafana rules action buttons', () => {
+ const grafanaRule = getGrafanaRule({ name: 'Grafana' });
+ it('Should not render Edit button for users without the update permission', () => {
+ // Arrange
+ jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
+
+ // Act
+ renderRuleDetails(grafanaRule);
+
+ // Assert
+ expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
+ });
+
+ it('Should not render Delete button for users without the delete permission', () => {
+ // Arrange
+ jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
+
+ // Act
+ renderRuleDetails(grafanaRule);
+
+ // Assert
+ expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
+ });
+
+ it('Should render Edit button for users with the update permission', () => {
+ // Arrange
+ jest
+ .spyOn(contextSrv, 'hasPermission')
+ .mockImplementation((action) => action === AccessControlAction.AlertingRuleUpdate);
+
+ // Act
+ renderRuleDetails(grafanaRule);
+
+ // Assert
+ expect(ui.actionButtons.edit.query()).toBeInTheDocument();
+ });
+
+ it('Should render Delete button for users with the delete permission', () => {
+ // Arrange
+ jest
+ .spyOn(contextSrv, 'hasPermission')
+ .mockImplementation((action) => action === AccessControlAction.AlertingRuleDelete);
+
+ // Act
+ renderRuleDetails(grafanaRule);
+
+ // Assert
+ expect(ui.actionButtons.delete.query()).toBeInTheDocument();
+ });
+
+ it('Should not render Silence button for users wihout the instance create permission', () => {
+ // Arrange
+ jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
+
+ // Act
+ renderRuleDetails(grafanaRule);
+
+ // Assert
+ expect(ui.actionButtons.silence.query()).not.toBeInTheDocument();
+ });
+
+ it('Should render Silence button for users with the instance create permissions', () => {
+ // Arrange
+ jest
+ .spyOn(contextSrv, 'hasPermission')
+ .mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate);
+
+ // Act
+ renderRuleDetails(grafanaRule);
+
+ // Assert
+ expect(ui.actionButtons.silence.query()).toBeInTheDocument();
+ });
+ });
+
+ describe('Cloud rules action buttons', () => {
+ const cloudRule = getCloudRule({ name: 'Cloud' });
+ it('Should not render Edit button for users without the update permission', () => {
+ // Arrange
+ jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
+
+ // Act
+ renderRuleDetails(cloudRule);
+
+ // Assert
+ expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
+ });
+
+ it('Should not render Delete button for users without the delete permission', () => {
+ // Arrange
+ jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
+
+ // Act
+ renderRuleDetails(cloudRule);
+
+ // Assert
+ expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
+ });
+
+ it('Should render Edit button for users with the update permission', () => {
+ // Arrange
+ jest
+ .spyOn(contextSrv, 'hasPermission')
+ .mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalWrite);
+
+ // Act
+ renderRuleDetails(cloudRule);
+
+ // Assert
+ expect(ui.actionButtons.edit.query()).toBeInTheDocument();
+ });
+
+ it('Should render Delete button for users with the delete permission', () => {
+ // Arrange
+ jest
+ .spyOn(contextSrv, 'hasPermission')
+ .mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalWrite);
+
+ // Act
+ renderRuleDetails(cloudRule);
+
+ // Assert
+ expect(ui.actionButtons.delete.query()).toBeInTheDocument();
+ });
+ });
+});
+
+function renderRuleDetails(rule: CombinedRule) {
+ const store = configureStore();
+
+ render(
+
+
+
+
+
+ );
+}
+
+function getGrafanaRule(override?: Partial) {
+ return mockCombinedRule({
+ namespace: {
+ groups: [],
+ name: 'Grafana',
+ rulesSource: 'grafana',
+ },
+ ...override,
+ });
+}
+
+function getCloudRule(override?: Partial) {
+ return mockCombinedRule({
+ namespace: {
+ groups: [],
+ name: 'Cortex',
+ rulesSource: mockDataSource(),
+ },
+ promRule: mockPromAlertingRule(),
+ rulerRule: mockRulerAlertingRule(),
+ ...override,
+ });
+}
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
index af21b4d4ab2..db8a5fc8e8c 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
@@ -1,15 +1,15 @@
-import { CombinedRule } from 'app/types/unified-alerting';
-import React, { FC } from 'react';
-import { useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
+import { CombinedRule } from 'app/types/unified-alerting';
+import React, { FC } from 'react';
import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField';
import { RuleDetailsActionButtons } from './RuleDetailsActionButtons';
-import { RuleDetailsDataSources } from './RuleDetailsDataSources';
-import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
-import { RuleDetailsExpression } from './RuleDetailsExpression';
import { RuleDetailsAnnotations } from './RuleDetailsAnnotations';
+import { RuleDetailsDataSources } from './RuleDetailsDataSources';
+import { RuleDetailsExpression } from './RuleDetailsExpression';
+import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
interface Props {
rule: CombinedRule;
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx
index 3e6640a24ef..7f26d28ba87 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx
@@ -1,24 +1,25 @@
-import React, { FC, Fragment, useState } from 'react';
-import { useDispatch } from 'react-redux';
-import { useLocation } from 'react-router-dom';
import { css } from '@emotion/css';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { config } from '@grafana/runtime';
-import { Button, ConfirmModal, ClipboardButton, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
-import { contextSrv } from 'app/core/services/context_srv';
+import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
+import { contextSrv } from 'app/core/services/context_srv';
+import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
+import { AccessControlAction } from 'app/types';
+import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
+import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+import React, { FC, Fragment, useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { useLocation } from 'react-router-dom';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
+import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
+import { deleteRuleAction } from '../../state/actions';
+import { getAlertmanagerByUid } from '../../utils/alertmanager';
import { Annotation } from '../../utils/constants';
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
import { createExploreLink, createViewLink, makeRuleBasedSilenceLink } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
-import { deleteRuleAction } from '../../state/actions';
-import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
-import { getAlertmanagerByUid } from '../../utils/alertmanager';
-import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
-import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
import { isFederatedRuleGroup } from '../../utils/rules';
-import { AccessControlAction } from 'app/types';
interface Props {
rule: CombinedRule;
@@ -38,12 +39,18 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => {
const alertmanagerSourceName = isGrafanaRulesSource(rulesSource)
? rulesSource
: getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name;
+ const rulesSourceName = getRulesSourceName(rulesSource);
+
+ const rulesPermissions = getRulesPermissions(rulesSourceName);
+ const hasEditPermission = contextSrv.hasPermission(rulesPermissions.update);
+ const hasDeletePermission = contextSrv.hasPermission(rulesPermissions.delete);
+ const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
const leftButtons: JSX.Element[] = [];
const rightButtons: JSX.Element[] = [];
const isFederated = isFederatedRuleGroup(group);
- const { isEditable } = useIsRuleEditable(getRulesSourceName(rulesSource), rulerRule);
+ const { isEditable } = useIsRuleEditable(rulesSourceName, rulerRule);
const returnTo = location.pathname + location.search;
const isViewMode = inViewMode(location.pathname);
@@ -74,7 +81,7 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => {
// explore does not support grafana rule queries atm
// neither do "federated rules"
- if (isCloudRulesSource(rulesSource) && contextSrv.isEditor && !isFederated) {
+ if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
leftButtons.push(
= ({ rule, rulesSource }) => {
);
}
+ // TODO Maybe there is a way to unify isEditable with FGAC permissions
if (isEditable && rulerRule && !isFederated) {
const sourceName = getRulesSourceName(rulesSource);
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
@@ -210,22 +218,28 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => {
);
}
- rightButtons.push(
-
- Edit
- ,
- setRuleToDelete(rule)}
- >
- Delete
-
- );
+ if (hasEditPermission) {
+ rightButtons.push(
+
+ Edit
+
+ );
+ }
+ if (hasDeletePermission) {
+ rightButtons.push(
+ setRuleToDelete(rule)}
+ >
+ Delete
+
+ );
+ }
}
if (leftButtons.length || rightButtons.length) {
return (
diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
index b534a3e8592..ef668398c0c 100644
--- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
@@ -1,7 +1,9 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
+import { contextSrv } from 'app/core/services/context_srv';
import kbn from 'app/core/utils/kbn';
+import { AccessControlAction } from 'app/types';
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import pluralize from 'pluralize';
import React, { FC, useEffect, useState } from 'react';
@@ -32,6 +34,8 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll }
const [isDeletingGroup, setIsDeletingGroup] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(!expandAll);
+ const canEditCloudRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite);
+
useEffect(() => {
setIsCollapsed(!expandAll);
}, [expandAll]);
@@ -88,7 +92,7 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll }
);
}
}
- } else if (hasRuler(rulesSource)) {
+ } else if (canEditCloudRules && hasRuler(rulesSource)) {
if (!isFederated) {
actionIcons.push(
Date: Fri, 8 Apr 2022 12:41:26 +0100
Subject: [PATCH 46/55] UsageStats: Move stats collection to separate service
(#47381)
* Remove specific stats from usage stats service
* Create statscollector service
* refactor
* Update and move tests
Mostly equivalent tests to before, but they've been divided over the two
services and removed the behavior driven legacy from GoConvey to
reduce the complexity of the tests.
* Collect featuremgmr metrics (copied over from #47407)
I removed the metrics registration from the feature manager in the merge
and re-add them in this commit. Separated to make things easier to
review.
---
pkg/infra/usagestats/mock.go | 5 +-
pkg/infra/usagestats/service/service.go | 89 +--
pkg/infra/usagestats/service/types.go | 10 -
pkg/infra/usagestats/service/usage_stats.go | 258 +-------
.../usagestats/service/usage_stats_test.go | 549 +++++-------------
.../statscollector/concurrent_users.go | 58 ++
.../concurrent_users_test.go} | 80 ++-
.../usagestats/statscollector/service.go | 321 ++++++++++
.../usagestats/statscollector/service_test.go | 362 ++++++++++++
.../backgroundsvcs/background_services.go | 12 +-
pkg/server/wire.go | 3 +
11 files changed, 964 insertions(+), 783 deletions(-)
delete mode 100644 pkg/infra/usagestats/service/types.go
create mode 100644 pkg/infra/usagestats/statscollector/concurrent_users.go
rename pkg/infra/usagestats/{service/usage_stats_service_test.go => statscollector/concurrent_users_test.go} (57%)
create mode 100644 pkg/infra/usagestats/statscollector/service.go
create mode 100644 pkg/infra/usagestats/statscollector/service_test.go
diff --git a/pkg/infra/usagestats/mock.go b/pkg/infra/usagestats/mock.go
index bead572f455..0510233fbb8 100644
--- a/pkg/infra/usagestats/mock.go
+++ b/pkg/infra/usagestats/mock.go
@@ -2,6 +2,7 @@ package usagestats
import (
"context"
+ "strings"
"testing"
"github.com/stretchr/testify/require"
@@ -29,8 +30,8 @@ func (usm *UsageStatsMock) GetUsageReport(ctx context.Context) (Report, error) {
return Report{Metrics: all}, nil
}
-func (usm *UsageStatsMock) ShouldBeReported(_ context.Context, _ string) bool {
- return true
+func (usm *UsageStatsMock) ShouldBeReported(_ context.Context, s string) bool {
+ return !strings.HasPrefix(s, "unknown")
}
func (usm *UsageStatsMock) RegisterSendReportCallback(_ SendReportCallbackFunc) {}
diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go
index 486f986f697..c31385d7d5a 100644
--- a/pkg/infra/usagestats/service/service.go
+++ b/pkg/infra/usagestats/service/service.go
@@ -2,51 +2,35 @@ package service
import (
"context"
- "fmt"
"time"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
- "github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins"
- "github.com/grafana/grafana/pkg/services/featuremgmt"
- "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
type UsageStats struct {
Cfg *setting.Cfg
- SQLStore sqlstore.Store
- pluginStore plugins.Store
- SocialService social.Service
kvStore *kvstore.NamespacedKVStore
RouteRegister routing.RouteRegister
- features *featuremgmt.FeatureManager
+ pluginStore plugins.Store
log log.Logger
- oauthProviders map[string]bool
- externalMetrics []usagestats.MetricsFunc
- concurrentUserStatsCache memoConcurrentUserStats
- startTime time.Time
- sendReportCallbacks []usagestats.SendReportCallbackFunc
+ externalMetrics []usagestats.MetricsFunc
+ sendReportCallbacks []usagestats.SendReportCallbackFunc
}
-func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugins.Store,
- socialService social.Service, kvStore kvstore.KVStore, routeRegister routing.RouteRegister, features *featuremgmt.FeatureManager,
-) *UsageStats {
+func ProvideService(cfg *setting.Cfg, pluginStore plugins.Store, kvStore kvstore.KVStore, routeRegister routing.RouteRegister) *UsageStats {
s := &UsageStats{
- Cfg: cfg,
- SQLStore: sqlStore,
- features: features,
- oauthProviders: socialService.GetOAuthProviders(),
- RouteRegister: routeRegister,
- pluginStore: pluginStore,
- kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"),
- log: log.New("infra.usagestats"),
- startTime: time.Now(),
+ Cfg: cfg,
+ RouteRegister: routeRegister,
+ pluginStore: pluginStore,
+ kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"),
+ log: log.New("infra.usagestats"),
}
s.registerAPIEndpoints()
@@ -55,8 +39,6 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p
}
func (uss *UsageStats) Run(ctx context.Context) error {
- uss.updateTotalStats(ctx)
-
// try to load last sent time from kv store
lastSent := time.Now()
if val, ok, err := uss.kvStore.Get(ctx, "last_sent"); err != nil {
@@ -77,10 +59,8 @@ func (uss *UsageStats) Run(ctx context.Context) error {
}
sendReportTicker := time.NewTicker(nextSendInterval)
- updateStatsTicker := time.NewTicker(time.Minute * 30)
defer sendReportTicker.Stop()
- defer updateStatsTicker.Stop()
for {
select {
@@ -102,54 +82,21 @@ func (uss *UsageStats) Run(ctx context.Context) error {
for _, callback := range uss.sendReportCallbacks {
callback()
}
- case <-updateStatsTicker.C:
- uss.updateTotalStats(ctx)
case <-ctx.Done():
return ctx.Err()
}
}
}
-type memoConcurrentUserStats struct {
- stats *concurrentUsersStats
-
- memoized time.Time
-}
-
-const concurrentUserStatsCacheLifetime = time.Hour
-
-func (uss *UsageStats) GetConcurrentUsersStats(ctx context.Context) (*concurrentUsersStats, error) {
- memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime)
- if !uss.concurrentUserStatsCache.memoized.Before(memoizationPeriod) {
- return uss.concurrentUserStatsCache.stats, nil
- }
-
- uss.concurrentUserStatsCache.stats = &concurrentUsersStats{}
- err := uss.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
- // Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive.
- rawSQL := `
-SELECT
- COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3,
- COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6,
- COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9,
- COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12,
- COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15,
- COUNT(1) AS bucket_le_inf
-FROM (select count(1) as tokens from user_auth_token group by user_id) uat;`
- _, err := sess.SQL(rawSQL).Get(uss.concurrentUserStatsCache.stats)
- if err != nil {
- return err
- }
- return nil
- })
- if err != nil {
- return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err)
- }
-
- uss.concurrentUserStatsCache.memoized = time.Now()
- return uss.concurrentUserStatsCache.stats, nil
-}
-
func (uss *UsageStats) RegisterSendReportCallback(c usagestats.SendReportCallbackFunc) {
uss.sendReportCallbacks = append(uss.sendReportCallbacks, c)
}
+
+func (uss *UsageStats) ShouldBeReported(ctx context.Context, dsType string) bool {
+ ds, exists := uss.pluginStore.Plugin(ctx, dsType)
+ if !exists {
+ return false
+ }
+
+ return ds.Signature.IsValid() || ds.Signature.IsInternal()
+}
diff --git a/pkg/infra/usagestats/service/types.go b/pkg/infra/usagestats/service/types.go
deleted file mode 100644
index c9e2a4d8361..00000000000
--- a/pkg/infra/usagestats/service/types.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package service
-
-type concurrentUsersStats struct {
- BucketLE3 int32 `xorm:"bucket_le_3"`
- BucketLE6 int32 `xorm:"bucket_le_6"`
- BucketLE9 int32 `xorm:"bucket_le_9"`
- BucketLE12 int32 `xorm:"bucket_le_12"`
- BucketLE15 int32 `xorm:"bucket_le_15"`
- BucketLEInf int32 `xorm:"bucket_le_inf"`
-}
diff --git a/pkg/infra/usagestats/service/usage_stats.go b/pkg/infra/usagestats/service/usage_stats.go
index a9e7b4fdf6b..95df118a6c0 100644
--- a/pkg/infra/usagestats/service/usage_stats.go
+++ b/pkg/infra/usagestats/service/usage_stats.go
@@ -4,17 +4,14 @@ import (
"bytes"
"context"
"encoding/json"
- "fmt"
"net/http"
"runtime"
"strings"
"time"
"github.com/google/uuid"
- "github.com/grafana/grafana/pkg/infra/metrics"
+
"github.com/grafana/grafana/pkg/infra/usagestats"
- "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/plugins"
)
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
@@ -38,64 +35,7 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e
UsageStatsId: uss.GetUsageStatsId(ctx),
}
- statsQuery := models.GetSystemStatsQuery{}
- if err := uss.SQLStore.GetSystemStats(ctx, &statsQuery); err != nil {
- uss.log.Error("Failed to get system stats", "error", err)
- return report, err
- }
-
- metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
- metrics["stats.users.count"] = statsQuery.Result.Users
- metrics["stats.admins.count"] = statsQuery.Result.Admins
- metrics["stats.editors.count"] = statsQuery.Result.Editors
- metrics["stats.viewers.count"] = statsQuery.Result.Viewers
- metrics["stats.orgs.count"] = statsQuery.Result.Orgs
- metrics["stats.playlist.count"] = statsQuery.Result.Playlists
- metrics["stats.plugins.apps.count"] = uss.appCount(ctx)
- metrics["stats.plugins.panels.count"] = uss.panelCount(ctx)
- metrics["stats.plugins.datasources.count"] = uss.dataSourceCount(ctx)
- metrics["stats.alerts.count"] = statsQuery.Result.Alerts
- metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
- metrics["stats.active_admins.count"] = statsQuery.Result.ActiveAdmins
- metrics["stats.active_editors.count"] = statsQuery.Result.ActiveEditors
- metrics["stats.active_viewers.count"] = statsQuery.Result.ActiveViewers
- metrics["stats.active_sessions.count"] = statsQuery.Result.ActiveSessions
- metrics["stats.monthly_active_users.count"] = statsQuery.Result.MonthlyActiveUsers
- metrics["stats.daily_active_users.count"] = statsQuery.Result.DailyActiveUsers
- metrics["stats.daily_active_admins.count"] = statsQuery.Result.DailyActiveAdmins
- metrics["stats.daily_active_editors.count"] = statsQuery.Result.DailyActiveEditors
- metrics["stats.daily_active_viewers.count"] = statsQuery.Result.DailyActiveViewers
- metrics["stats.daily_active_sessions.count"] = statsQuery.Result.DailyActiveSessions
- metrics["stats.datasources.count"] = statsQuery.Result.Datasources
- metrics["stats.stars.count"] = statsQuery.Result.Stars
- metrics["stats.folders.count"] = statsQuery.Result.Folders
- metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
- metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
- metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
- metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
- metrics["stats.teams.count"] = statsQuery.Result.Teams
- metrics["stats.total_auth_token.count"] = statsQuery.Result.AuthTokens
- metrics["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions
- metrics["stats.annotations.count"] = statsQuery.Result.Annotations
- metrics["stats.alert_rules.count"] = statsQuery.Result.AlertRules
- metrics["stats.library_panels.count"] = statsQuery.Result.LibraryPanels
- metrics["stats.library_variables.count"] = statsQuery.Result.LibraryVariables
- metrics["stats.dashboards_viewers_can_edit.count"] = statsQuery.Result.DashboardsViewersCanEdit
- metrics["stats.dashboards_viewers_can_admin.count"] = statsQuery.Result.DashboardsViewersCanAdmin
- metrics["stats.folders_viewers_can_edit.count"] = statsQuery.Result.FoldersViewersCanEdit
- metrics["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin
- metrics["stats.api_keys.count"] = statsQuery.Result.APIKeys
-
- ossEditionCount := 1
- enterpriseEditionCount := 0
- if uss.Cfg.IsEnterprise {
- enterpriseEditionCount = 1
- ossEditionCount = 0
- }
- metrics["stats.edition.oss.count"] = ossEditionCount
- metrics["stats.edition.enterprise.count"] = enterpriseEditionCount
-
- uss.registerExternalMetrics(ctx, metrics)
+ uss.gatherMetrics(ctx, metrics)
// must run after registration of external metrics
if v, ok := metrics["stats.valid_license.count"]; ok {
@@ -104,141 +44,10 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e
metrics["stats.valid_license.count"] = 0
}
- userCount := statsQuery.Result.Users
- avgAuthTokensPerUser := statsQuery.Result.AuthTokens
- if userCount != 0 {
- avgAuthTokensPerUser /= userCount
- }
-
- metrics["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser
-
- dsStats := models.GetDataSourceStatsQuery{}
- if err := uss.SQLStore.GetDataSourceStats(ctx, &dsStats); err != nil {
- uss.log.Error("Failed to get datasource stats", "error", err)
- return report, err
- }
-
- // send counters for each data source
- // but ignore any custom data sources
- // as sending that name could be sensitive information
- dsOtherCount := 0
- for _, dsStat := range dsStats.Result {
- if uss.ShouldBeReported(ctx, dsStat.Type) {
- metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
- } else {
- dsOtherCount += dsStat.Count
- }
- }
- metrics["stats.ds.other.count"] = dsOtherCount
-
- esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES}
- if err := uss.SQLStore.GetDataSourcesByType(ctx, &esDataSourcesQuery); err != nil {
- uss.log.Error("Failed to get elasticsearch json data", "error", err)
- return report, err
- }
-
- for _, data := range esDataSourcesQuery.Result {
- esVersion, err := data.JsonData.Get("esVersion").Int()
- if err != nil {
- continue
- }
-
- statName := fmt.Sprintf("stats.ds.elasticsearch.v%d.count", esVersion)
-
- count, _ := metrics[statName].(int64)
-
- metrics[statName] = count + 1
- }
-
- metrics["stats.packaging."+uss.Cfg.Packaging+".count"] = 1
- metrics["stats.distributor."+uss.Cfg.ReportingDistributor+".count"] = 1
-
- // fetch datasource access stats
- dsAccessStats := models.GetDataSourceAccessStatsQuery{}
- if err := uss.SQLStore.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil {
- uss.log.Error("Failed to get datasource access stats", "error", err)
- return report, err
- }
-
- // send access counters for each data source
- // but ignore any custom data sources
- // as sending that name could be sensitive information
- dsAccessOtherCount := make(map[string]int64)
- for _, dsAccessStat := range dsAccessStats.Result {
- if dsAccessStat.Access == "" {
- continue
- }
-
- access := strings.ToLower(dsAccessStat.Access)
-
- if uss.ShouldBeReported(ctx, dsAccessStat.Type) {
- metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
- } else {
- old := dsAccessOtherCount[access]
- dsAccessOtherCount[access] = old + dsAccessStat.Count
- }
- }
-
- for access, count := range dsAccessOtherCount {
- metrics["stats.ds_access.other."+access+".count"] = count
- }
-
- // get stats about alert notifier usage
- anStats := models.GetAlertNotifierUsageStatsQuery{}
- if err := uss.SQLStore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil {
- uss.log.Error("Failed to get alert notification stats", "error", err)
- return report, err
- }
-
- for _, stats := range anStats.Result {
- metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
- }
-
- // Add stats about auth configuration
- authTypes := map[string]bool{}
- authTypes["anonymous"] = uss.Cfg.AnonymousEnabled
- authTypes["basic_auth"] = uss.Cfg.BasicAuthEnabled
- authTypes["ldap"] = uss.Cfg.LDAPEnabled
- authTypes["auth_proxy"] = uss.Cfg.AuthProxyEnabled
-
- for provider, enabled := range uss.oauthProviders {
- authTypes["oauth_"+provider] = enabled
- }
-
- for authType, enabled := range authTypes {
- enabledValue := 0
- if enabled {
- enabledValue = 1
- }
- metrics["stats.auth_enabled."+authType+".count"] = enabledValue
- }
-
- // Get concurrent users stats as histogram
- concurrentUsersStats, err := uss.GetConcurrentUsersStats(ctx)
- if err != nil {
- uss.log.Error("Failed to get concurrent users stats", "error", err)
- return report, err
- }
-
- // Histogram is cumulative and metric name has a postfix of le_""
- metrics["stats.auth_token_per_user_le_3"] = concurrentUsersStats.BucketLE3
- metrics["stats.auth_token_per_user_le_6"] = concurrentUsersStats.BucketLE6
- metrics["stats.auth_token_per_user_le_9"] = concurrentUsersStats.BucketLE9
- metrics["stats.auth_token_per_user_le_12"] = concurrentUsersStats.BucketLE12
- metrics["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15
- metrics["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf
-
- metrics["stats.uptime"] = int64(time.Since(uss.startTime).Seconds())
-
- featureUsageStats := uss.features.GetUsageStats(ctx)
- for name, val := range featureUsageStats {
- metrics[name] = val
- }
-
return report, nil
}
-func (uss *UsageStats) registerExternalMetrics(ctx context.Context, metrics map[string]interface{}) {
+func (uss *UsageStats) gatherMetrics(ctx context.Context, metrics map[string]interface{}) {
for _, fn := range uss.externalMetrics {
fnMetrics, err := fn(ctx)
if err != nil {
@@ -295,55 +104,6 @@ var sendUsageStats = func(uss *UsageStats, data *bytes.Buffer) {
}()
}
-func (uss *UsageStats) updateTotalStats(ctx context.Context) {
- if !uss.Cfg.MetricsEndpointEnabled || uss.Cfg.MetricsEndpointDisableTotalStats {
- return
- }
-
- statsQuery := models.GetSystemStatsQuery{}
- if err := uss.SQLStore.GetSystemStats(ctx, &statsQuery); err != nil {
- uss.log.Error("Failed to get system stats", "error", err)
- return
- }
-
- metrics.MStatTotalDashboards.Set(float64(statsQuery.Result.Dashboards))
- metrics.MStatTotalFolders.Set(float64(statsQuery.Result.Folders))
- metrics.MStatTotalUsers.Set(float64(statsQuery.Result.Users))
- metrics.MStatActiveUsers.Set(float64(statsQuery.Result.ActiveUsers))
- metrics.MStatTotalPlaylists.Set(float64(statsQuery.Result.Playlists))
- metrics.MStatTotalOrgs.Set(float64(statsQuery.Result.Orgs))
- metrics.StatsTotalViewers.Set(float64(statsQuery.Result.Viewers))
- metrics.StatsTotalActiveViewers.Set(float64(statsQuery.Result.ActiveViewers))
- metrics.StatsTotalEditors.Set(float64(statsQuery.Result.Editors))
- metrics.StatsTotalActiveEditors.Set(float64(statsQuery.Result.ActiveEditors))
- metrics.StatsTotalAdmins.Set(float64(statsQuery.Result.Admins))
- metrics.StatsTotalActiveAdmins.Set(float64(statsQuery.Result.ActiveAdmins))
- metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions))
- metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations))
- metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules))
- metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels))
- metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables))
-
- dsStats := models.GetDataSourceStatsQuery{}
- if err := uss.SQLStore.GetDataSourceStats(ctx, &dsStats); err != nil {
- uss.log.Error("Failed to get datasource stats", "error", err)
- return
- }
-
- for _, dsStat := range dsStats.Result {
- metrics.StatsTotalDataSources.WithLabelValues(dsStat.Type).Set(float64(dsStat.Count))
- }
-}
-
-func (uss *UsageStats) ShouldBeReported(ctx context.Context, dsType string) bool {
- ds, exists := uss.pluginStore.Plugin(ctx, dsType)
- if !exists {
- return false
- }
-
- return ds.Signature.IsValid() || ds.Signature.IsInternal()
-}
-
func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string {
anonId, ok, err := uss.kvStore.Get(ctx, "anonymous_id")
if err != nil {
@@ -371,15 +131,3 @@ func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string {
return anonId
}
-
-func (uss *UsageStats) appCount(ctx context.Context) int {
- return len(uss.pluginStore.Plugins(ctx, plugins.App))
-}
-
-func (uss *UsageStats) panelCount(ctx context.Context) int {
- return len(uss.pluginStore.Plugins(ctx, plugins.Panel))
-}
-
-func (uss *UsageStats) dataSourceCount(ctx context.Context) int {
- return len(uss.pluginStore.Plugins(ctx, plugins.DataSource))
-}
diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go
index a150e26d316..7c36eead42e 100644
--- a/pkg/infra/usagestats/service/usage_stats_test.go
+++ b/pkg/infra/usagestats/service/usage_stats_test.go
@@ -3,6 +3,7 @@ package service
import (
"bytes"
"context"
+ "encoding/json"
"errors"
"io/ioutil"
"net/http"
@@ -11,19 +12,16 @@ import (
"testing"
"time"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
"github.com/grafana/grafana/pkg/api/routing"
- "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/kvstore"
- "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
- "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
- "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
// This is to ensure that the interface contract is held by the implementation
@@ -38,419 +36,163 @@ func Test_InterfaceContractValidity(t *testing.T) {
}
func TestMetrics(t *testing.T) {
- t.Run("When sending usage stats", func(t *testing.T) {
- sqlStore := mockstore.NewSQLStoreMock()
- uss := createService(t, setting.Cfg{}, sqlStore, false)
+ const metricName = "stats.test_metric.count"
- sqlStore.ExpectedSystemStats = &models.SystemStats{
- Dashboards: 1,
- Datasources: 2,
- Users: 3,
- Admins: 31,
- Editors: 32,
- Viewers: 33,
- ActiveUsers: 4,
- ActiveAdmins: 21,
- ActiveEditors: 22,
- ActiveViewers: 23,
- ActiveSessions: 24,
- DailyActiveUsers: 25,
- DailyActiveAdmins: 26,
- DailyActiveEditors: 27,
- DailyActiveViewers: 28,
- DailyActiveSessions: 29,
- Orgs: 5,
- Playlists: 6,
- Alerts: 7,
- Stars: 8,
- Folders: 9,
- DashboardPermissions: 10,
- FolderPermissions: 11,
- ProvisionedDashboards: 12,
- Snapshots: 13,
- Teams: 14,
- AuthTokens: 15,
- DashboardVersions: 16,
- Annotations: 17,
- AlertRules: 18,
- LibraryPanels: 19,
- LibraryVariables: 20,
- DashboardsViewersCanAdmin: 3,
- DashboardsViewersCanEdit: 2,
- FoldersViewersCanAdmin: 1,
- FoldersViewersCanEdit: 5,
- APIKeys: 2,
+ sqlStore := mockstore.NewSQLStoreMock()
+ uss := createService(t, setting.Cfg{}, sqlStore, false)
+
+ uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
+ return map[string]interface{}{metricName: 1}, nil
+ })
+
+ err := uss.sendUsageStats(context.Background())
+ require.NoError(t, err)
+
+ t.Run("Given reporting not enabled and sending usage stats", func(t *testing.T) {
+ origSendUsageStats := sendUsageStats
+ t.Cleanup(func() {
+ sendUsageStats = origSendUsageStats
+ })
+ statsSent := false
+ sendUsageStats = func(uss *UsageStats, b *bytes.Buffer) {
+ statsSent = true
}
- setupSomeDataSourcePlugins(t, uss)
+ uss.Cfg.ReportingEnabled = false
+ err := uss.sendUsageStats(context.Background())
+ require.NoError(t, err)
- sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{
- {
- Type: models.DS_ES,
- Count: 9,
- },
- {
- Type: models.DS_PROMETHEUS,
- Count: 10,
- },
- {
- Type: "unknown_ds",
- Count: 11,
- },
- {
- Type: "unknown_ds2",
- Count: 12,
- },
+ require.False(t, statsSent)
+ })
+
+ t.Run("Given reporting enabled, stats should be gathered and sent to HTTP endpoint", func(t *testing.T) {
+ origCfg := uss.Cfg
+ t.Cleanup(func() {
+ uss.Cfg = origCfg
+ })
+ uss.Cfg = &setting.Cfg{
+ ReportingEnabled: true,
+ BuildVersion: "5.0.0",
+ AnonymousEnabled: true,
+ BasicAuthEnabled: true,
+ LDAPEnabled: true,
+ AuthProxyEnabled: true,
+ Packaging: "deb",
+ ReportingDistributor: "hosted-grafana",
}
- sqlStore.ExpectedDataSources = []*models.DataSource{
- {
- JsonData: simplejson.NewFromAny(map[string]interface{}{
- "esVersion": 2,
- }),
- },
- {
- JsonData: simplejson.NewFromAny(map[string]interface{}{
- "esVersion": 2,
- }),
- },
- {
- JsonData: simplejson.NewFromAny(map[string]interface{}{
- "esVersion": 70,
- }),
- },
- }
-
- sqlStore.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{
- {
- Type: models.DS_ES,
- Access: "direct",
- Count: 1,
- },
- {
- Type: models.DS_ES,
- Access: "proxy",
- Count: 2,
- },
- {
- Type: models.DS_PROMETHEUS,
- Access: "proxy",
- Count: 3,
- },
- {
- Type: "unknown_ds",
- Access: "proxy",
- Count: 4,
- },
- {
- Type: "unknown_ds2",
- Access: "",
- Count: 5,
- },
- {
- Type: "unknown_ds3",
- Access: "direct",
- Count: 6,
- },
- {
- Type: "unknown_ds4",
- Access: "direct",
- Count: 7,
- },
- {
- Type: "unknown_ds5",
- Access: "proxy",
- Count: 8,
- },
- }
-
- sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{
- {
- Type: "slack",
- Count: 1,
- },
- {
- Type: "webhook",
- Count: 2,
- },
- }
-
- uss.SQLStore = sqlStore
-
- createConcurrentTokens(t, uss.SQLStore)
-
- uss.oauthProviders = map[string]bool{
- "github": true,
- "gitlab": true,
- "azuread": true,
- "google": true,
- "generic_oauth": true,
- "grafana_com": true,
- }
+ ch := make(chan httpResp)
+ ticker := time.NewTicker(2 * time.Second)
+ ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ buf, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ t.Logf("Fake HTTP handler received an error: %s", err.Error())
+ ch <- httpResp{
+ err: err,
+ }
+ return
+ }
+ require.NoError(t, err, "Failed to read response body, err=%v", err)
+ t.Logf("Fake HTTP handler received a response")
+ ch <- httpResp{
+ responseBuffer: bytes.NewBuffer(buf),
+ req: r,
+ }
+ }))
+ t.Cleanup(ts.Close)
+ t.Cleanup(func() {
+ close(ch)
+ })
+ usageStatsURL = ts.URL
err := uss.sendUsageStats(context.Background())
require.NoError(t, err)
- t.Run("Given reporting not enabled and sending usage stats", func(t *testing.T) {
- origSendUsageStats := sendUsageStats
- t.Cleanup(func() {
- sendUsageStats = origSendUsageStats
- })
- statsSent := false
- sendUsageStats = func(uss *UsageStats, b *bytes.Buffer) {
- statsSent = true
- }
+ // Wait for fake HTTP server to receive a request
+ var resp httpResp
+ select {
+ case resp = <-ch:
+ require.NoError(t, resp.err, "Fake server experienced an error")
+ case <-ticker.C:
+ t.Fatalf("Timed out waiting for HTTP request")
+ }
- uss.Cfg.ReportingEnabled = false
- err := uss.sendUsageStats(context.Background())
- require.NoError(t, err)
+ t.Logf("Received response from fake HTTP server: %+v\n", resp)
- require.False(t, statsSent)
- })
+ assert.NotNil(t, resp.req)
- t.Run("Given reporting enabled, stats should be gathered and sent to HTTP endpoint", func(t *testing.T) {
- origCfg := uss.Cfg
- t.Cleanup(func() {
- uss.Cfg = origCfg
- })
- uss.Cfg = &setting.Cfg{
- ReportingEnabled: true,
- BuildVersion: "5.0.0",
- AnonymousEnabled: true,
- BasicAuthEnabled: true,
- LDAPEnabled: true,
- AuthProxyEnabled: true,
- Packaging: "deb",
- ReportingDistributor: "hosted-grafana",
- }
+ assert.Equal(t, http.MethodPost, resp.req.Method)
+ assert.Equal(t, "application/json", resp.req.Header.Get("Content-Type"))
- ch := make(chan httpResp)
- ticker := time.NewTicker(2 * time.Second)
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- buf, err := ioutil.ReadAll(r.Body)
- if err != nil {
- t.Logf("Fake HTTP handler received an error: %s", err.Error())
- ch <- httpResp{
- err: err,
- }
- return
- }
- require.NoError(t, err, "Failed to read response body, err=%v", err)
- t.Logf("Fake HTTP handler received a response")
- ch <- httpResp{
- responseBuffer: bytes.NewBuffer(buf),
- req: r,
- }
- }))
- t.Cleanup(ts.Close)
- t.Cleanup(func() {
- close(ch)
- })
- usageStatsURL = ts.URL
+ require.NotNil(t, resp.responseBuffer)
- err := uss.sendUsageStats(context.Background())
- require.NoError(t, err)
+ j := make(map[string]interface{})
+ err = json.Unmarshal(resp.responseBuffer.Bytes(), &j)
+ require.NoError(t, err)
- // Wait for fake HTTP server to receive a request
- var resp httpResp
- select {
- case resp = <-ch:
- require.NoError(t, resp.err, "Fake server experienced an error")
- case <-ticker.C:
- t.Fatalf("Timed out waiting for HTTP request")
- }
+ assert.Equal(t, "5_0_0", j["version"])
+ assert.Equal(t, runtime.GOOS, j["os"])
+ assert.Equal(t, runtime.GOARCH, j["arch"])
- t.Logf("Received response from fake HTTP server: %+v\n", resp)
+ usageId := uss.GetUsageStatsId(context.Background())
+ assert.NotEmpty(t, usageId)
- assert.NotNil(t, resp.req)
+ metrics, ok := j["metrics"].(map[string]interface{})
+ require.True(t, ok)
+ assert.EqualValues(t, 1, metrics[metricName])
+ })
+}
- assert.Equal(t, http.MethodPost, resp.req.Method)
- assert.Equal(t, "application/json", resp.req.Header.Get("Content-Type"))
+func TestGetUsageReport_IncludesMetrics(t *testing.T) {
+ sqlStore := mockstore.NewSQLStoreMock()
+ uss := createService(t, setting.Cfg{}, sqlStore, true)
+ metricName := "stats.test_metric.count"
- require.NotNil(t, resp.responseBuffer)
-
- j, err := simplejson.NewFromReader(resp.responseBuffer)
- require.NoError(t, err)
-
- assert.Equal(t, "5_0_0", j.Get("version").MustString())
- assert.Equal(t, runtime.GOOS, j.Get("os").MustString())
- assert.Equal(t, runtime.GOARCH, j.Get("arch").MustString())
-
- usageId := uss.GetUsageStatsId(context.Background())
- assert.NotEmpty(t, usageId)
-
- metrics := j.Get("metrics")
- assert.Equal(t, 15, metrics.Get("stats.total_auth_token.count").MustInt())
- assert.Equal(t, 2, metrics.Get("stats.api_keys.count").MustInt())
- assert.Equal(t, 5, metrics.Get("stats.avg_auth_token_per_user.count").MustInt())
- assert.Equal(t, 16, metrics.Get("stats.dashboard_versions.count").MustInt())
- assert.Equal(t, 17, metrics.Get("stats.annotations.count").MustInt())
- assert.Equal(t, 18, metrics.Get("stats.alert_rules.count").MustInt())
- assert.Equal(t, 19, metrics.Get("stats.library_panels.count").MustInt())
- assert.Equal(t, 20, metrics.Get("stats.library_variables.count").MustInt())
- assert.Equal(t, 0, metrics.Get("stats.live_users.count").MustInt())
- assert.Equal(t, 0, metrics.Get("stats.live_clients.count").MustInt())
-
- assert.Equal(t, 9, metrics.Get("stats.ds."+models.DS_ES+".count").MustInt())
- assert.Equal(t, 10, metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt())
-
- assert.Equal(t, 11+12, metrics.Get("stats.ds.other.count").MustInt())
-
- assert.Equal(t, 1, metrics.Get("stats.ds_access."+models.DS_ES+".direct.count").MustInt())
- assert.Equal(t, 2, metrics.Get("stats.ds_access."+models.DS_ES+".proxy.count").MustInt())
- assert.Equal(t, 3, metrics.Get("stats.ds_access."+models.DS_PROMETHEUS+".proxy.count").MustInt())
- assert.Equal(t, 6+7, metrics.Get("stats.ds_access.other.direct.count").MustInt())
- assert.Equal(t, 4+8, metrics.Get("stats.ds_access.other.proxy.count").MustInt())
-
- assert.Equal(t, 1, metrics.Get("stats.alert_notifiers.slack.count").MustInt())
- assert.Equal(t, 2, metrics.Get("stats.alert_notifiers.webhook.count").MustInt())
-
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.anonymous.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.basic_auth.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.ldap.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.auth_proxy.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_github.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_gitlab.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_google.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_azuread.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt())
-
- assert.Equal(t, 1, metrics.Get("stats.packaging.deb.count").MustInt())
- assert.Equal(t, 1, metrics.Get("stats.distributor.hosted-grafana.count").MustInt())
-
- assert.LessOrEqual(t, 60, metrics.Get("stats.uptime").MustInt())
- assert.Greater(t, 70, metrics.Get("stats.uptime").MustInt())
- })
+ uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
+ return map[string]interface{}{metricName: 1}, nil
})
- t.Run("When updating total stats", func(t *testing.T) {
- sqlStore := mockstore.NewSQLStoreMock()
- uss := createService(t, setting.Cfg{}, sqlStore, false)
- uss.Cfg.MetricsEndpointEnabled = true
- uss.Cfg.MetricsEndpointDisableTotalStats = false
+ report, err := uss.GetUsageReport(context.Background())
+ require.NoError(t, err, "Expected no error")
- sqlStore.ExpectedSystemStats = &models.SystemStats{}
+ metric := report.Metrics[metricName]
+ assert.Equal(t, 1, metric)
+}
- t.Run("When metrics is disabled and total stats is enabled, stats should not be updated", func(t *testing.T) {
- uss.Cfg.MetricsEndpointEnabled = false
- uss.Cfg.MetricsEndpointDisableTotalStats = false
- uss.updateTotalStats(context.Background())
- })
+func TestRegisterMetrics(t *testing.T) {
+ const goodMetricName = "stats.test_external_metric.count"
- t.Run("When metrics is enabled and total stats is disabled, stats should not be updated", func(t *testing.T) {
- uss.Cfg.MetricsEndpointEnabled = true
- uss.Cfg.MetricsEndpointDisableTotalStats = true
+ sqlStore := mockstore.NewSQLStoreMock()
+ uss := createService(t, setting.Cfg{}, sqlStore, false)
+ metrics := map[string]interface{}{"stats.test_metric.count": 1, "stats.test_metric_second.count": 2}
- uss.updateTotalStats(context.Background())
- })
-
- t.Run("When metrics is disabled and total stats is disabled, stats should not be updated", func(t *testing.T) {
- uss.Cfg.MetricsEndpointEnabled = false
- uss.Cfg.MetricsEndpointDisableTotalStats = true
-
- uss.updateTotalStats(context.Background())
- })
-
- t.Run("When metrics is enabled and total stats is enabled, stats should be updated", func(t *testing.T) {
- uss.Cfg.MetricsEndpointEnabled = true
- uss.Cfg.MetricsEndpointDisableTotalStats = false
-
- uss.updateTotalStats(context.Background())
- })
+ uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
+ return map[string]interface{}{goodMetricName: 1}, nil
})
- t.Run("When registering a metric", func(t *testing.T) {
- sqlStore := mockstore.NewSQLStoreMock()
- uss := createService(t, setting.Cfg{}, sqlStore, false)
- metricName := "stats.test_metric.count"
+ {
+ extMetrics, err := uss.externalMetrics[0](context.Background())
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{goodMetricName: 1}, extMetrics)
+ }
- t.Run("Adds a new metric to the external metrics", func(t *testing.T) {
- uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
- return map[string]interface{}{metricName: 1}, nil
- })
+ uss.gatherMetrics(context.Background(), metrics)
+ assert.Equal(t, 1, metrics[goodMetricName])
- metrics, err := uss.externalMetrics[0](context.Background())
- require.NoError(t, err)
- assert.Equal(t, map[string]interface{}{metricName: 1}, metrics)
- })
- })
-
- t.Run("When getting usage report", func(t *testing.T) {
- sqlStore := mockstore.NewSQLStoreMock()
- uss := createService(t, setting.Cfg{}, sqlStore, true)
- metricName := "stats.test_metric.count"
-
- createConcurrentTokens(t, uss.SQLStore)
-
- t.Run("Should include metrics for concurrent users", func(t *testing.T) {
- report, err := uss.GetUsageReport(context.Background())
- require.NoError(t, err)
-
- assert.Equal(t, int32(1), report.Metrics["stats.auth_token_per_user_le_3"])
- assert.Equal(t, int32(2), report.Metrics["stats.auth_token_per_user_le_6"])
- assert.Equal(t, int32(3), report.Metrics["stats.auth_token_per_user_le_9"])
- assert.Equal(t, int32(4), report.Metrics["stats.auth_token_per_user_le_12"])
- assert.Equal(t, int32(5), report.Metrics["stats.auth_token_per_user_le_15"])
- assert.Equal(t, int32(6), report.Metrics["stats.auth_token_per_user_le_inf"])
- })
-
- t.Run("Should include external metrics", func(t *testing.T) {
- uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
- return map[string]interface{}{metricName: 1}, nil
- })
-
- report, err := uss.GetUsageReport(context.Background())
- require.NoError(t, err, "Expected no error")
-
- metric := report.Metrics[metricName]
- assert.Equal(t, 1, metric)
- })
-
- t.Run("Should include feature usage stats", func(t *testing.T) {
- report, err := uss.GetUsageReport(context.Background())
- require.NoError(t, err, "Expected no error")
-
- assert.Equal(t, 1, report.Metrics["stats.features.feature_1.count"])
- assert.Equal(t, 1, report.Metrics["stats.features.feature_2.count"])
- })
- })
-
- t.Run("When registering external metrics", func(t *testing.T) {
- sqlStore := mockstore.NewSQLStoreMock()
- uss := createService(t, setting.Cfg{}, sqlStore, false)
- metrics := map[string]interface{}{"stats.test_metric.count": 1, "stats.test_metric_second.count": 2}
- extMetricName := "stats.test_external_metric.count"
+ t.Run("do not add metrics that return an error when fetched", func(t *testing.T) {
+ const badMetricName = "stats.test_external_metric_error.count"
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
- return map[string]interface{}{extMetricName: 1}, nil
+ return map[string]interface{}{badMetricName: 1}, errors.New("some error")
})
+ uss.gatherMetrics(context.Background(), metrics)
- uss.registerExternalMetrics(context.Background(), metrics)
+ extErrorMetric := metrics[badMetricName]
+ extMetric := metrics[goodMetricName]
- assert.Equal(t, 1, metrics[extMetricName])
-
- t.Run("When loading a metric results to an error", func(t *testing.T) {
- uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
- return map[string]interface{}{extMetricName: 1}, nil
- })
- extErrorMetricName := "stats.test_external_metric_error.count"
-
- t.Run("Should not add it to metrics", func(t *testing.T) {
- uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
- return map[string]interface{}{extErrorMetricName: 1}, errors.New("some error")
- })
-
- uss.registerExternalMetrics(context.Background(), metrics)
-
- extErrorMetric := metrics[extErrorMetricName]
- extMetric := metrics[extMetricName]
-
- require.Nil(t, extErrorMetric, "Invalid metric should not be added")
- assert.Equal(t, 1, extMetric)
- assert.Len(t, metrics, 3, "Expected only one available metric")
- })
- })
+ require.Nil(t, extErrorMetric, "Invalid metric should not be added")
+ assert.Equal(t, 1, extMetric)
+ assert.Len(t, metrics, 3, "Expected only one available metric")
})
}
@@ -479,27 +221,6 @@ func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type
return result
}
-func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStats) {
- t.Helper()
-
- uss.pluginStore = &fakePluginStore{
- plugins: map[string]plugins.PluginDTO{
- models.DS_ES: {
- Signature: "internal",
- },
- models.DS_PROMETHEUS: {
- Signature: "internal",
- },
- models.DS_GRAPHITE: {
- Signature: "internal",
- },
- models.DS_MYSQL: {
- Signature: "internal",
- },
- },
- }
-}
-
type httpResp struct {
req *http.Request
responseBuffer *bytes.Buffer
@@ -511,15 +232,11 @@ func createService(t *testing.T, cfg setting.Cfg, sqlStore sqlstore.Store, withD
if withDB {
sqlStore = sqlstore.InitTestDB(t)
}
- return &UsageStats{
- Cfg: &cfg,
- SQLStore: sqlStore,
- externalMetrics: make([]usagestats.MetricsFunc, 0),
- pluginStore: &fakePluginStore{},
- kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
- log: log.New("infra.usagestats"),
- startTime: time.Now().Add(-1 * time.Minute),
- RouteRegister: routing.NewRouteRegister(),
- features: featuremgmt.WithFeatures("feature1", "feature2"),
- }
+
+ return ProvideService(
+ &cfg,
+ &fakePluginStore{},
+ kvstore.ProvideService(sqlStore),
+ routing.NewRouteRegister(),
+ )
}
diff --git a/pkg/infra/usagestats/statscollector/concurrent_users.go b/pkg/infra/usagestats/statscollector/concurrent_users.go
new file mode 100644
index 00000000000..55d23881ebc
--- /dev/null
+++ b/pkg/infra/usagestats/statscollector/concurrent_users.go
@@ -0,0 +1,58 @@
+package statscollector
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+)
+
+const concurrentUserStatsCacheLifetime = time.Hour
+
+type concurrentUsersStats struct {
+ BucketLE3 int32 `xorm:"bucket_le_3"`
+ BucketLE6 int32 `xorm:"bucket_le_6"`
+ BucketLE9 int32 `xorm:"bucket_le_9"`
+ BucketLE12 int32 `xorm:"bucket_le_12"`
+ BucketLE15 int32 `xorm:"bucket_le_15"`
+ BucketLEInf int32 `xorm:"bucket_le_inf"`
+}
+
+type memoConcurrentUserStats struct {
+ stats *concurrentUsersStats
+
+ memoized time.Time
+}
+
+func (s *Service) concurrentUsers(ctx context.Context) (*concurrentUsersStats, error) {
+ memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime)
+ if !s.concurrentUserStatsCache.memoized.Before(memoizationPeriod) {
+ return s.concurrentUserStatsCache.stats, nil
+ }
+
+ s.concurrentUserStatsCache.stats = &concurrentUsersStats{}
+ err := s.sqlstore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
+ // Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive.
+ rawSQL := `
+SELECT
+ COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3,
+ COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6,
+ COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9,
+ COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12,
+ COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15,
+ COUNT(1) AS bucket_le_inf
+FROM (select count(1) as tokens from user_auth_token group by user_id) uat;`
+ _, err := sess.SQL(rawSQL).Get(s.concurrentUserStatsCache.stats)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err)
+ }
+
+ s.concurrentUserStatsCache.memoized = time.Now()
+ return s.concurrentUserStatsCache.stats, nil
+}
diff --git a/pkg/infra/usagestats/service/usage_stats_service_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go
similarity index 57%
rename from pkg/infra/usagestats/service/usage_stats_service_test.go
rename to pkg/infra/usagestats/statscollector/concurrent_users_test.go
index 5960c0fb8c6..dfa027d0b85 100644
--- a/pkg/infra/usagestats/service/usage_stats_service_test.go
+++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go
@@ -1,4 +1,4 @@
-package service
+package statscollector
import (
"context"
@@ -8,21 +8,35 @@ import (
"testing"
"time"
- "github.com/grafana/grafana/pkg/infra/kvstore"
- "github.com/grafana/grafana/pkg/infra/log"
- "github.com/grafana/grafana/pkg/services/sqlstore"
- "github.com/grafana/grafana/pkg/util"
+ "github.com/grafana/grafana/pkg/setting"
+
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/util"
)
-func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) {
+func TestConcurrentUsersMetrics(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
- uss := &UsageStats{
- SQLStore: sqlStore,
- kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
- log: log.New("infra.usagestats"),
- }
+ s := createService(t, setting.NewCfg(), sqlStore)
+
+ createConcurrentTokens(t, sqlStore)
+
+ stats, err := s.collect(context.Background())
+ require.NoError(t, err)
+
+ assert.Equal(t, int32(1), stats["stats.auth_token_per_user_le_3"])
+ assert.Equal(t, int32(2), stats["stats.auth_token_per_user_le_6"])
+ assert.Equal(t, int32(3), stats["stats.auth_token_per_user_le_9"])
+ assert.Equal(t, int32(4), stats["stats.auth_token_per_user_le_12"])
+ assert.Equal(t, int32(5), stats["stats.auth_token_per_user_le_15"])
+ assert.Equal(t, int32(6), stats["stats.auth_token_per_user_le_inf"])
+}
+
+func TestConcurrentUsersStats(t *testing.T) {
+ sqlStore := sqlstore.InitTestDB(t)
+ s := createService(t, setting.NewCfg(), sqlStore)
createConcurrentTokens(t, sqlStore)
@@ -31,10 +45,10 @@ func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) {
cancel()
})
- actualResult, err := uss.GetConcurrentUsersStats(ctx)
+ actualResult, err := s.concurrentUsers(ctx)
require.NoError(t, err)
- expectedCachedResult := &concurrentUsersStats{
+ expectedResult := &concurrentUsersStats{
BucketLE3: 1,
BucketLE6: 2,
BucketLE9: 3,
@@ -42,14 +56,39 @@ func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) {
BucketLE15: 5,
BucketLEInf: 6,
}
- assert.Equal(t, expectedCachedResult, actualResult)
+ assert.Equal(t, expectedResult, actualResult)
createToken(t, 8, sqlStore)
require.NoError(t, err)
- actualResult, err = uss.GetConcurrentUsersStats(ctx)
+ // Cached, new token is not reflected yet.
+ actualResult, err = s.concurrentUsers(ctx)
require.NoError(t, err)
- assert.Equal(t, expectedCachedResult, actualResult)
+ assert.Equal(t, expectedResult, actualResult)
+
+ // Bust cache
+ s.concurrentUserStatsCache = memoConcurrentUserStats{}
+
+ expectedResult = &concurrentUsersStats{
+ BucketLE3: 2,
+ BucketLE6: 3,
+ BucketLE9: 4,
+ BucketLE12: 5,
+ BucketLE15: 6,
+ BucketLEInf: 7,
+ }
+ actualResult, err = s.concurrentUsers(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, expectedResult, actualResult)
+}
+
+func createConcurrentTokens(t *testing.T, sqlStore sqlstore.Store) {
+ t.Helper()
+ for u := 1; u <= 6; u++ {
+ for tkn := 1; tkn <= u*3; tkn++ {
+ createToken(t, u, sqlStore)
+ }
+ }
}
func createToken(t *testing.T, uID int, sqlStore sqlstore.Store) {
@@ -83,15 +122,6 @@ func createToken(t *testing.T, uID int, sqlStore sqlstore.Store) {
require.NoError(t, err)
}
-func createConcurrentTokens(t *testing.T, sqlStore sqlstore.Store) {
- t.Helper()
- for u := 1; u <= 6; u++ {
- for tkn := 1; tkn <= u*3; tkn++ {
- createToken(t, u, sqlStore)
- }
- }
-}
-
type userAuthToken struct {
UserID int64 `xorm:"user_id"`
AuthToken string
diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go
new file mode 100644
index 00000000000..b96abfb4e68
--- /dev/null
+++ b/pkg/infra/usagestats/statscollector/service.go
@@ -0,0 +1,321 @@
+package statscollector
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
+
+ "github.com/grafana/grafana/pkg/infra/usagestats"
+
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/infra/metrics"
+ "github.com/grafana/grafana/pkg/login/social"
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/plugins"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+type Service struct {
+ cfg *setting.Cfg
+ sqlstore sqlstore.Store
+ plugins plugins.Store
+ social social.Service
+ usageStats usagestats.Service
+ features *featuremgmt.FeatureManager
+
+ log log.Logger
+
+ startTime time.Time
+ concurrentUserStatsCache memoConcurrentUserStats
+}
+
+func ProvideService(
+ usagestats usagestats.Service,
+ cfg *setting.Cfg,
+ store sqlstore.Store,
+ social social.Service,
+ plugins plugins.Store,
+ features *featuremgmt.FeatureManager,
+) *Service {
+ s := &Service{
+ cfg: cfg,
+ sqlstore: store,
+ plugins: plugins,
+ social: social,
+ usageStats: usagestats,
+ features: features,
+
+ startTime: time.Now(),
+ log: log.New("infra.usagestats.collector"),
+ }
+
+ usagestats.RegisterMetricsFunc(s.collect)
+
+ return s
+}
+
+func (s *Service) Run(ctx context.Context) error {
+ s.updateTotalStats(ctx)
+ updateStatsTicker := time.NewTicker(time.Minute * 30)
+ defer updateStatsTicker.Stop()
+
+ for {
+ select {
+ case <-updateStatsTicker.C:
+ s.updateTotalStats(ctx)
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
+
+func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) {
+ m := map[string]interface{}{}
+
+ statsQuery := models.GetSystemStatsQuery{}
+ if err := s.sqlstore.GetSystemStats(ctx, &statsQuery); err != nil {
+ s.log.Error("Failed to get system stats", "error", err)
+ return nil, err
+ }
+
+ m["stats.dashboards.count"] = statsQuery.Result.Dashboards
+ m["stats.users.count"] = statsQuery.Result.Users
+ m["stats.admins.count"] = statsQuery.Result.Admins
+ m["stats.editors.count"] = statsQuery.Result.Editors
+ m["stats.viewers.count"] = statsQuery.Result.Viewers
+ m["stats.orgs.count"] = statsQuery.Result.Orgs
+ m["stats.playlist.count"] = statsQuery.Result.Playlists
+ m["stats.plugins.apps.count"] = s.appCount(ctx)
+ m["stats.plugins.panels.count"] = s.panelCount(ctx)
+ m["stats.plugins.datasources.count"] = s.dataSourceCount(ctx)
+ m["stats.alerts.count"] = statsQuery.Result.Alerts
+ m["stats.active_users.count"] = statsQuery.Result.ActiveUsers
+ m["stats.active_admins.count"] = statsQuery.Result.ActiveAdmins
+ m["stats.active_editors.count"] = statsQuery.Result.ActiveEditors
+ m["stats.active_viewers.count"] = statsQuery.Result.ActiveViewers
+ m["stats.active_sessions.count"] = statsQuery.Result.ActiveSessions
+ m["stats.monthly_active_users.count"] = statsQuery.Result.MonthlyActiveUsers
+ m["stats.daily_active_users.count"] = statsQuery.Result.DailyActiveUsers
+ m["stats.daily_active_admins.count"] = statsQuery.Result.DailyActiveAdmins
+ m["stats.daily_active_editors.count"] = statsQuery.Result.DailyActiveEditors
+ m["stats.daily_active_viewers.count"] = statsQuery.Result.DailyActiveViewers
+ m["stats.daily_active_sessions.count"] = statsQuery.Result.DailyActiveSessions
+ m["stats.datasources.count"] = statsQuery.Result.Datasources
+ m["stats.stars.count"] = statsQuery.Result.Stars
+ m["stats.folders.count"] = statsQuery.Result.Folders
+ m["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
+ m["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
+ m["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
+ m["stats.snapshots.count"] = statsQuery.Result.Snapshots
+ m["stats.teams.count"] = statsQuery.Result.Teams
+ m["stats.total_auth_token.count"] = statsQuery.Result.AuthTokens
+ m["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions
+ m["stats.annotations.count"] = statsQuery.Result.Annotations
+ m["stats.alert_rules.count"] = statsQuery.Result.AlertRules
+ m["stats.library_panels.count"] = statsQuery.Result.LibraryPanels
+ m["stats.library_variables.count"] = statsQuery.Result.LibraryVariables
+ m["stats.dashboards_viewers_can_edit.count"] = statsQuery.Result.DashboardsViewersCanEdit
+ m["stats.dashboards_viewers_can_admin.count"] = statsQuery.Result.DashboardsViewersCanAdmin
+ m["stats.folders_viewers_can_edit.count"] = statsQuery.Result.FoldersViewersCanEdit
+ m["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin
+ m["stats.api_keys.count"] = statsQuery.Result.APIKeys
+
+ ossEditionCount := 1
+ enterpriseEditionCount := 0
+ if s.cfg.IsEnterprise {
+ enterpriseEditionCount = 1
+ ossEditionCount = 0
+ }
+ m["stats.edition.oss.count"] = ossEditionCount
+ m["stats.edition.enterprise.count"] = enterpriseEditionCount
+
+ userCount := statsQuery.Result.Users
+ avgAuthTokensPerUser := statsQuery.Result.AuthTokens
+ if userCount != 0 {
+ avgAuthTokensPerUser /= userCount
+ }
+
+ m["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser
+
+ dsStats := models.GetDataSourceStatsQuery{}
+ if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil {
+ s.log.Error("Failed to get datasource stats", "error", err)
+ return nil, err
+ }
+
+ // send counters for each data source
+ // but ignore any custom data sources
+ // as sending that name could be sensitive information
+ dsOtherCount := 0
+ for _, dsStat := range dsStats.Result {
+ if s.usageStats.ShouldBeReported(ctx, dsStat.Type) {
+ m["stats.ds."+dsStat.Type+".count"] = dsStat.Count
+ } else {
+ dsOtherCount += dsStat.Count
+ }
+ }
+ m["stats.ds.other.count"] = dsOtherCount
+
+ esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES}
+ if err := s.sqlstore.GetDataSourcesByType(ctx, &esDataSourcesQuery); err != nil {
+ s.log.Error("Failed to get elasticsearch json data", "error", err)
+ return nil, err
+ }
+
+ for _, data := range esDataSourcesQuery.Result {
+ esVersion, err := data.JsonData.Get("esVersion").Int()
+ if err != nil {
+ continue
+ }
+
+ statName := fmt.Sprintf("stats.ds.elasticsearch.v%d.count", esVersion)
+
+ count, _ := m[statName].(int64)
+
+ m[statName] = count + 1
+ }
+
+ m["stats.packaging."+s.cfg.Packaging+".count"] = 1
+ m["stats.distributor."+s.cfg.ReportingDistributor+".count"] = 1
+
+ // fetch datasource access stats
+ dsAccessStats := models.GetDataSourceAccessStatsQuery{}
+ if err := s.sqlstore.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil {
+ s.log.Error("Failed to get datasource access stats", "error", err)
+ return nil, err
+ }
+
+ // send access counters for each data source
+ // but ignore any custom data sources
+ // as sending that name could be sensitive information
+ dsAccessOtherCount := make(map[string]int64)
+ for _, dsAccessStat := range dsAccessStats.Result {
+ if dsAccessStat.Access == "" {
+ continue
+ }
+
+ access := strings.ToLower(dsAccessStat.Access)
+
+ if s.usageStats.ShouldBeReported(ctx, dsAccessStat.Type) {
+ m["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
+ } else {
+ old := dsAccessOtherCount[access]
+ dsAccessOtherCount[access] = old + dsAccessStat.Count
+ }
+ }
+
+ for access, count := range dsAccessOtherCount {
+ m["stats.ds_access.other."+access+".count"] = count
+ }
+
+ // get stats about alert notifier usage
+ anStats := models.GetAlertNotifierUsageStatsQuery{}
+ if err := s.sqlstore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil {
+ s.log.Error("Failed to get alert notification stats", "error", err)
+ return nil, err
+ }
+
+ for _, stats := range anStats.Result {
+ m["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
+ }
+
+ // Add stats about auth configuration
+ authTypes := map[string]bool{}
+ authTypes["anonymous"] = s.cfg.AnonymousEnabled
+ authTypes["basic_auth"] = s.cfg.BasicAuthEnabled
+ authTypes["ldap"] = s.cfg.LDAPEnabled
+ authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled
+
+ for provider, enabled := range s.social.GetOAuthProviders() {
+ authTypes["oauth_"+provider] = enabled
+ }
+
+ for authType, enabled := range authTypes {
+ enabledValue := 0
+ if enabled {
+ enabledValue = 1
+ }
+ m["stats.auth_enabled."+authType+".count"] = enabledValue
+ }
+
+ // Get concurrent users stats as histogram
+ concurrentUsersStats, err := s.concurrentUsers(ctx)
+ if err != nil {
+ s.log.Error("Failed to get concurrent users stats", "error", err)
+ return nil, err
+ }
+
+ // Histogram is cumulative and metric name has a postfix of le_""
+ m["stats.auth_token_per_user_le_3"] = concurrentUsersStats.BucketLE3
+ m["stats.auth_token_per_user_le_6"] = concurrentUsersStats.BucketLE6
+ m["stats.auth_token_per_user_le_9"] = concurrentUsersStats.BucketLE9
+ m["stats.auth_token_per_user_le_12"] = concurrentUsersStats.BucketLE12
+ m["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15
+ m["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf
+
+ m["stats.uptime"] = int64(time.Since(s.startTime).Seconds())
+
+ featureUsageStats := s.features.GetUsageStats(ctx)
+ for k, v := range featureUsageStats {
+ m[k] = v
+ }
+
+ return m, nil
+}
+
+func (s *Service) updateTotalStats(ctx context.Context) bool {
+ if !s.cfg.MetricsEndpointEnabled || s.cfg.MetricsEndpointDisableTotalStats {
+ return false
+ }
+
+ statsQuery := models.GetSystemStatsQuery{}
+ if err := s.sqlstore.GetSystemStats(ctx, &statsQuery); err != nil {
+ s.log.Error("Failed to get system stats", "error", err)
+ return false
+ }
+
+ metrics.MStatTotalDashboards.Set(float64(statsQuery.Result.Dashboards))
+ metrics.MStatTotalFolders.Set(float64(statsQuery.Result.Folders))
+ metrics.MStatTotalUsers.Set(float64(statsQuery.Result.Users))
+ metrics.MStatActiveUsers.Set(float64(statsQuery.Result.ActiveUsers))
+ metrics.MStatTotalPlaylists.Set(float64(statsQuery.Result.Playlists))
+ metrics.MStatTotalOrgs.Set(float64(statsQuery.Result.Orgs))
+ metrics.StatsTotalViewers.Set(float64(statsQuery.Result.Viewers))
+ metrics.StatsTotalActiveViewers.Set(float64(statsQuery.Result.ActiveViewers))
+ metrics.StatsTotalEditors.Set(float64(statsQuery.Result.Editors))
+ metrics.StatsTotalActiveEditors.Set(float64(statsQuery.Result.ActiveEditors))
+ metrics.StatsTotalAdmins.Set(float64(statsQuery.Result.Admins))
+ metrics.StatsTotalActiveAdmins.Set(float64(statsQuery.Result.ActiveAdmins))
+ metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions))
+ metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations))
+ metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules))
+ metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels))
+ metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables))
+
+ dsStats := models.GetDataSourceStatsQuery{}
+ if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil {
+ s.log.Error("Failed to get datasource stats", "error", err)
+ return true
+ }
+
+ for _, dsStat := range dsStats.Result {
+ metrics.StatsTotalDataSources.WithLabelValues(dsStat.Type).Set(float64(dsStat.Count))
+ }
+ return true
+}
+
+func (s *Service) appCount(ctx context.Context) int {
+ return len(s.plugins.Plugins(ctx, plugins.App))
+}
+
+func (s *Service) panelCount(ctx context.Context) int {
+ return len(s.plugins.Plugins(ctx, plugins.Panel))
+}
+
+func (s *Service) dataSourceCount(ctx context.Context) int {
+ return len(s.plugins.Plugins(ctx, plugins.DataSource))
+}
diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go
new file mode 100644
index 00000000000..6ff76c947bd
--- /dev/null
+++ b/pkg/infra/usagestats/statscollector/service_test.go
@@ -0,0 +1,362 @@
+package statscollector
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/infra/usagestats"
+ "github.com/grafana/grafana/pkg/login/social"
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/plugins"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+func TestTotalStatsUpdate(t *testing.T) {
+ sqlStore := mockstore.NewSQLStoreMock()
+ s := createService(t, setting.NewCfg(), sqlStore)
+ s.cfg.MetricsEndpointEnabled = true
+ s.cfg.MetricsEndpointDisableTotalStats = false
+
+ sqlStore.ExpectedSystemStats = &models.SystemStats{}
+
+ tests := []struct {
+ MetricsEndpointEnabled bool
+ MetricsEndpointDisableTotalStats bool
+ ExpectedUpdate bool
+ }{
+ {
+ MetricsEndpointEnabled: false,
+ MetricsEndpointDisableTotalStats: false,
+ ExpectedUpdate: false,
+ },
+ {
+ MetricsEndpointEnabled: false,
+ MetricsEndpointDisableTotalStats: true,
+ ExpectedUpdate: false,
+ },
+ {
+ MetricsEndpointEnabled: true,
+ MetricsEndpointDisableTotalStats: true,
+ ExpectedUpdate: false,
+ },
+ {
+ MetricsEndpointEnabled: true,
+ MetricsEndpointDisableTotalStats: false,
+ ExpectedUpdate: true,
+ },
+ }
+
+ for _, tc := range tests {
+ tc := tc
+
+ t.Run(fmt.Sprintf(
+ "metricsEnabled(%v) * totalStatsDisabled(%v) = %v",
+ tc.MetricsEndpointEnabled,
+ tc.MetricsEndpointDisableTotalStats,
+ tc.ExpectedUpdate,
+ ), func(t *testing.T) {
+ s.cfg.MetricsEndpointEnabled = tc.MetricsEndpointEnabled
+ s.cfg.MetricsEndpointDisableTotalStats = tc.MetricsEndpointDisableTotalStats
+
+ assert.Equal(t, tc.ExpectedUpdate, s.updateTotalStats(context.Background()))
+ })
+ }
+}
+
+func TestFeatureUsageStats(t *testing.T) {
+ store := mockstore.NewSQLStoreMock()
+ mockSystemStats(store)
+ s := createService(t, setting.NewCfg(), store)
+
+ m, err := s.collect(context.Background())
+ require.NoError(t, err, "Expected no error")
+
+ assert.Equal(t, 1, m["stats.features.feature_1.count"])
+ assert.Equal(t, 1, m["stats.features.feature_2.count"])
+}
+
+func TestCollectingUsageStats(t *testing.T) {
+ sqlStore := mockstore.NewSQLStoreMock()
+ s := createService(t, &setting.Cfg{
+ ReportingEnabled: true,
+ BuildVersion: "5.0.0",
+ AnonymousEnabled: true,
+ BasicAuthEnabled: true,
+ LDAPEnabled: true,
+ AuthProxyEnabled: true,
+ Packaging: "deb",
+ ReportingDistributor: "hosted-grafana",
+ }, sqlStore)
+
+ s.startTime = time.Now().Add(-1 * time.Minute)
+
+ mockSystemStats(sqlStore)
+ setupSomeDataSourcePlugins(t, s)
+
+ sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{
+ {
+ Type: models.DS_ES,
+ Count: 9,
+ },
+ {
+ Type: models.DS_PROMETHEUS,
+ Count: 10,
+ },
+ {
+ Type: "unknown_ds",
+ Count: 11,
+ },
+ {
+ Type: "unknown_ds2",
+ Count: 12,
+ },
+ }
+
+ sqlStore.ExpectedDataSources = []*models.DataSource{
+ {
+ JsonData: simplejson.NewFromAny(map[string]interface{}{
+ "esVersion": 2,
+ }),
+ },
+ {
+ JsonData: simplejson.NewFromAny(map[string]interface{}{
+ "esVersion": 2,
+ }),
+ },
+ {
+ JsonData: simplejson.NewFromAny(map[string]interface{}{
+ "esVersion": 70,
+ }),
+ },
+ }
+
+ sqlStore.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{
+ {
+ Type: models.DS_ES,
+ Access: "direct",
+ Count: 1,
+ },
+ {
+ Type: models.DS_ES,
+ Access: "proxy",
+ Count: 2,
+ },
+ {
+ Type: models.DS_PROMETHEUS,
+ Access: "proxy",
+ Count: 3,
+ },
+ {
+ Type: "unknown_ds",
+ Access: "proxy",
+ Count: 4,
+ },
+ {
+ Type: "unknown_ds2",
+ Access: "",
+ Count: 5,
+ },
+ {
+ Type: "unknown_ds3",
+ Access: "direct",
+ Count: 6,
+ },
+ {
+ Type: "unknown_ds4",
+ Access: "direct",
+ Count: 7,
+ },
+ {
+ Type: "unknown_ds5",
+ Access: "proxy",
+ Count: 8,
+ },
+ }
+
+ sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{
+ {
+ Type: "slack",
+ Count: 1,
+ },
+ {
+ Type: "webhook",
+ Count: 2,
+ },
+ }
+
+ createConcurrentTokens(t, sqlStore)
+
+ s.social = &mockSocial{
+ OAuthProviders: map[string]bool{
+ "github": true,
+ "gitlab": true,
+ "azuread": true,
+ "google": true,
+ "generic_oauth": true,
+ "grafana_com": true,
+ },
+ }
+
+ metrics, err := s.collect(context.Background())
+ require.NoError(t, err)
+
+ assert.EqualValues(t, 15, metrics["stats.total_auth_token.count"])
+ assert.EqualValues(t, 2, metrics["stats.api_keys.count"])
+ assert.EqualValues(t, 5, metrics["stats.avg_auth_token_per_user.count"])
+ assert.EqualValues(t, 16, metrics["stats.dashboard_versions.count"])
+ assert.EqualValues(t, 17, metrics["stats.annotations.count"])
+ assert.EqualValues(t, 18, metrics["stats.alert_rules.count"])
+ assert.EqualValues(t, 19, metrics["stats.library_panels.count"])
+ assert.EqualValues(t, 20, metrics["stats.library_variables.count"])
+
+ assert.EqualValues(t, 9, metrics["stats.ds."+models.DS_ES+".count"])
+ assert.EqualValues(t, 10, metrics["stats.ds."+models.DS_PROMETHEUS+".count"])
+
+ assert.EqualValues(t, 11+12, metrics["stats.ds.other.count"])
+
+ assert.EqualValues(t, 1, metrics["stats.ds_access."+models.DS_ES+".direct.count"])
+ assert.EqualValues(t, 2, metrics["stats.ds_access."+models.DS_ES+".proxy.count"])
+ assert.EqualValues(t, 3, metrics["stats.ds_access."+models.DS_PROMETHEUS+".proxy.count"])
+ assert.EqualValues(t, 6+7, metrics["stats.ds_access.other.direct.count"])
+ assert.EqualValues(t, 4+8, metrics["stats.ds_access.other.proxy.count"])
+
+ assert.EqualValues(t, 1, metrics["stats.alert_notifiers.slack.count"])
+ assert.EqualValues(t, 2, metrics["stats.alert_notifiers.webhook.count"])
+
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.anonymous.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.basic_auth.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.ldap.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.auth_proxy.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_github.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_gitlab.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_google.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_azuread.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_generic_oauth.count"])
+ assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_grafana_com.count"])
+
+ assert.EqualValues(t, 1, metrics["stats.packaging.deb.count"])
+ assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"])
+
+ assert.InDelta(t, int64(65), metrics["stats.uptime"], 6)
+}
+
+func mockSystemStats(sqlStore *mockstore.SQLStoreMock) {
+ sqlStore.ExpectedSystemStats = &models.SystemStats{
+ Dashboards: 1,
+ Datasources: 2,
+ Users: 3,
+ Admins: 31,
+ Editors: 32,
+ Viewers: 33,
+ ActiveUsers: 4,
+ ActiveAdmins: 21,
+ ActiveEditors: 22,
+ ActiveViewers: 23,
+ ActiveSessions: 24,
+ DailyActiveUsers: 25,
+ DailyActiveAdmins: 26,
+ DailyActiveEditors: 27,
+ DailyActiveViewers: 28,
+ DailyActiveSessions: 29,
+ Orgs: 5,
+ Playlists: 6,
+ Alerts: 7,
+ Stars: 8,
+ Folders: 9,
+ DashboardPermissions: 10,
+ FolderPermissions: 11,
+ ProvisionedDashboards: 12,
+ Snapshots: 13,
+ Teams: 14,
+ AuthTokens: 15,
+ DashboardVersions: 16,
+ Annotations: 17,
+ AlertRules: 18,
+ LibraryPanels: 19,
+ LibraryVariables: 20,
+ DashboardsViewersCanAdmin: 3,
+ DashboardsViewersCanEdit: 2,
+ FoldersViewersCanAdmin: 1,
+ FoldersViewersCanEdit: 5,
+ APIKeys: 2,
+ }
+}
+
+type mockSocial struct {
+ social.Service
+
+ OAuthProviders map[string]bool
+}
+
+func (m *mockSocial) GetOAuthProviders() map[string]bool {
+ return m.OAuthProviders
+}
+
+type fakePluginStore struct {
+ plugins.Store
+
+ plugins map[string]plugins.PluginDTO
+}
+
+func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) {
+ p, exists := pr.plugins[pluginID]
+
+ return p, exists
+}
+
+func setupSomeDataSourcePlugins(t *testing.T, s *Service) {
+ t.Helper()
+
+ s.plugins = &fakePluginStore{
+ plugins: map[string]plugins.PluginDTO{
+ models.DS_ES: {
+ Signature: "internal",
+ },
+ models.DS_PROMETHEUS: {
+ Signature: "internal",
+ },
+ models.DS_GRAPHITE: {
+ Signature: "internal",
+ },
+ models.DS_MYSQL: {
+ Signature: "internal",
+ },
+ },
+ }
+}
+
+func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type) []plugins.PluginDTO {
+ var result []plugins.PluginDTO
+ for _, v := range pr.plugins {
+ for _, t := range pluginTypes {
+ if v.Type == t {
+ result = append(result, v)
+ }
+ }
+ }
+
+ return result
+}
+
+func createService(t testing.TB, cfg *setting.Cfg, store sqlstore.Store) *Service {
+ t.Helper()
+
+ return ProvideService(
+ &usagestats.UsageStatsMock{},
+ cfg,
+ store,
+ &mockSocial{},
+ &fakePluginStore{},
+ featuremgmt.WithFeatures("feature1", "feature2"),
+ )
+}
diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go
index 8521761435e..529e7abde09 100644
--- a/pkg/server/backgroundsvcs/background_services.go
+++ b/pkg/server/backgroundsvcs/background_services.go
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
+ "github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/registry"
@@ -32,9 +33,10 @@ func ProvideBackgroundServiceRegistry(
pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService, pm *manager.PluginManager,
rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService, tracing tracing.Tracer,
provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, usageStats *uss.UsageStats,
- grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService,
- metrics *metrics.InternalMetricsService, secretsService *secretsManager.SecretsService,
- remoteCache *remotecache.RemoteCache, thumbnailsService thumbs.Service, StorageService store.StorageService,
+ statsCollector *statscollector.Service, grafanaUpdateChecker *updatechecker.GrafanaService,
+ pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService,
+ secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache,
+ thumbnailsService thumbs.Service, StorageService store.StorageService,
// Need to make sure these are initialized, is there a better place to put them?
_ *dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
_ serviceaccounts.Service, _ *guardian.Provider,
@@ -56,11 +58,13 @@ func ProvideBackgroundServiceRegistry(
pluginsUpdateChecker,
metrics,
usageStats,
+ statsCollector,
tracing,
remoteCache,
secretsService,
StorageService,
- thumbnailsService)
+ thumbnailsService,
+ )
}
// BackgroundServiceRegistry provides background services.
diff --git a/pkg/server/wire.go b/pkg/server/wire.go
index 566aa24de28..61a10765e42 100644
--- a/pkg/server/wire.go
+++ b/pkg/server/wire.go
@@ -5,6 +5,7 @@ package server
import (
"github.com/google/wire"
+
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/avatar"
@@ -21,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
+ "github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
loginpkg "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models"
@@ -235,6 +237,7 @@ var wireBasicSet = wire.NewSet(
guardian.ProvideService,
avatar.ProvideAvatarCacheServer,
authproxy.ProvideAuthProxy,
+ statscollector.ProvideService,
)
var wireSet = wire.NewSet(
From ad432108e66cc9fa2c2eb232c1a888088122ee03 Mon Sep 17 00:00:00 2001
From: Serge Zaitsev
Date: Fri, 8 Apr 2022 13:56:38 +0200
Subject: [PATCH 47/55] Chore: Remove bus from dashboards provisioning (#47495)
* Chore: Remove bus from dashboards provisioning
* fix symlink test, make it run on darwin
* remove unused mock
---
.../provisioning/dashboards/dashboard.go | 10 ++--
.../provisioning/dashboards/file_reader.go | 8 +--
...ux_test.go => file_reader_symlink_test.go} | 6 +--
.../dashboards/file_reader_test.go | 52 +++++++++----------
.../provisioning/dashboards/validator_test.go | 28 +++++-----
pkg/services/provisioning/provisioning.go | 2 +-
.../provisioning/provisioning_test.go | 2 +-
pkg/services/provisioning/utils/utils.go | 4 ++
8 files changed, 59 insertions(+), 53 deletions(-)
rename pkg/services/provisioning/dashboards/{file_reader_linux_test.go => file_reader_symlink_test.go} (91%)
diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go
index 60752f263ac..881af5dd2cf 100644
--- a/pkg/services/provisioning/dashboards/dashboard.go
+++ b/pkg/services/provisioning/dashboards/dashboard.go
@@ -23,7 +23,7 @@ type DashboardProvisioner interface {
}
// DashboardProvisionerFactory creates DashboardProvisioners based on input
-type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, utils.OrgStore) (DashboardProvisioner, error)
+type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, utils.OrgStore, utils.DashboardStore) (DashboardProvisioner, error)
// Provisioner is responsible for syncing dashboard from disk to Grafana's database.
type Provisioner struct {
@@ -35,7 +35,7 @@ type Provisioner struct {
}
// New returns a new DashboardProvisioner
-func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgStore utils.OrgStore) (DashboardProvisioner, error) {
+func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgStore utils.OrgStore, dashboardStore utils.DashboardStore) (DashboardProvisioner, error) {
logger := log.New("provisioning.dashboard")
cfgReader := &configReader{path: configDirectory, log: logger, orgStore: orgStore}
configs, err := cfgReader.readConfig(ctx)
@@ -43,7 +43,7 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
return nil, errutil.Wrap("Failed to read dashboards config", err)
}
- fileReaders, err := getFileReaders(configs, logger, provisioner)
+ fileReaders, err := getFileReaders(configs, logger, provisioner, dashboardStore)
if err != nil {
return nil, errutil.Wrap("Failed to initialize file readers", err)
}
@@ -123,14 +123,14 @@ func (provider *Provisioner) GetAllowUIUpdatesFromConfig(name string) bool {
}
func getFileReaders(
- configs []*config, logger log.Logger, service dashboards.DashboardProvisioningService,
+ configs []*config, logger log.Logger, service dashboards.DashboardProvisioningService, store utils.DashboardStore,
) ([]*FileReader, error) {
var readers []*FileReader
for _, config := range configs {
switch config.Type {
case "file":
- fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name), service)
+ fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name), service, store)
if err != nil {
return nil, errutil.Wrapf(err, "Failed to create file reader for config %v", config.Name)
}
diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go
index 36dfdf7c541..827a941a87a 100644
--- a/pkg/services/provisioning/dashboards/file_reader.go
+++ b/pkg/services/provisioning/dashboards/file_reader.go
@@ -11,12 +11,12 @@ import (
"sync"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/provisioning/utils"
"github.com/grafana/grafana/pkg/util"
)
@@ -33,6 +33,7 @@ type FileReader struct {
Path string
log log.Logger
dashboardProvisioningService dashboards.DashboardProvisioningService
+ dashboardStore utils.DashboardStore
FoldersFromFilesStructure bool
mux sync.RWMutex
@@ -41,7 +42,7 @@ type FileReader struct {
}
// NewDashboardFileReader returns a new filereader based on `config`
-func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.DashboardProvisioningService) (*FileReader, error) {
+func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.DashboardProvisioningService, dashboardStore utils.DashboardStore) (*FileReader, error) {
var path string
path, ok := cfg.Options["path"].(string)
if !ok {
@@ -63,6 +64,7 @@ func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.Dash
Path: path,
log: log,
dashboardProvisioningService: service,
+ dashboardStore: dashboardStore,
FoldersFromFilesStructure: foldersFromFilesStructure,
usageTracker: newUsageTracker(),
}, nil
@@ -298,7 +300,7 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv
}
cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(folderName), OrgId: cfg.OrgID}
- err := bus.Dispatch(ctx, cmd)
+ err := fr.dashboardStore.GetDashboard(ctx, cmd)
if err != nil && !errors.Is(err, models.ErrDashboardNotFound) {
return 0, err
diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_symlink_test.go
similarity index 91%
rename from pkg/services/provisioning/dashboards/file_reader_linux_test.go
rename to pkg/services/provisioning/dashboards/file_reader_symlink_test.go
index c8f288d52f4..91b356e251a 100644
--- a/pkg/services/provisioning/dashboards/file_reader_linux_test.go
+++ b/pkg/services/provisioning/dashboards/file_reader_symlink_test.go
@@ -1,5 +1,5 @@
-//go:build linux
-// +build linux
+//go:build linux || darwin
+// +build linux darwin
package dashboards
@@ -25,7 +25,7 @@ func TestProvisionedSymlinkedFolder(t *testing.T) {
Options: map[string]interface{}{"path": symlinkedFolder},
}
- reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil)
+ reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil)
if err != nil {
t.Error("expected err to be nil")
}
diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go
index 8e6b01f55d1..627ed6041b8 100644
--- a/pkg/services/provisioning/dashboards/file_reader_test.go
+++ b/pkg/services/provisioning/dashboards/file_reader_test.go
@@ -8,7 +8,6 @@ import (
"testing"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -42,7 +41,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
t.Run("using path parameter", func(t *testing.T) {
cfg := setup()
cfg.Options["path"] = defaultDashboards
- reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil)
+ reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil)
require.NoError(t, err)
require.NotEqual(t, reader.Path, "")
})
@@ -50,7 +49,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
t.Run("using folder as options", func(t *testing.T) {
cfg := setup()
cfg.Options["folder"] = defaultDashboards
- reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil)
+ reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil)
require.NoError(t, err)
require.NotEqual(t, reader.Path, "")
})
@@ -59,7 +58,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
cfg := setup()
cfg.Options["path"] = foldersFromFilesStructure
cfg.Options["foldersFromFilesStructure"] = true
- reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil)
+ reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil)
require.NoError(t, err)
require.NotEqual(t, reader.Path, "")
})
@@ -72,7 +71,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
}
cfg.Options["folder"] = fullPath
- reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil)
+ reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil)
require.NoError(t, err)
require.Equal(t, reader.Path, fullPath)
@@ -82,7 +81,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
t.Run("using relative path", func(t *testing.T) {
cfg := setup()
cfg.Options["folder"] = defaultDashboards
- reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil)
+ reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil)
require.NoError(t, err)
resolvedPath := reader.resolvedPath()
@@ -96,9 +95,8 @@ func TestDashboardFileReader(t *testing.T) {
fakeService := &dashboards.FakeDashboardProvisioning{}
defer fakeService.AssertExpectations(t)
+ fakeStore := &fakeDashboardStore{}
setup := func() {
- bus.ClearBusHandlers()
- bus.AddHandler("test", mockGetDashboardQuery)
cfg = &config{
Name: configName,
Type: "file",
@@ -118,7 +116,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 2}, nil).Times(2)
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -138,7 +136,7 @@ func TestDashboardFileReader(t *testing.T) {
inserted++
})
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -175,7 +173,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once()
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -203,7 +201,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once()
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -238,7 +236,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once()
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -266,7 +264,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once()
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -281,7 +279,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(nil, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once()
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -298,7 +296,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3)
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -315,7 +313,7 @@ func TestDashboardFileReader(t *testing.T) {
Folder: "",
}
- _, err := NewDashboardFileReader(cfg, logger, nil)
+ _, err := NewDashboardFileReader(cfg, logger, nil, nil)
require.NotNil(t, err)
})
@@ -323,7 +321,7 @@ func TestDashboardFileReader(t *testing.T) {
setup()
cfg.Options["path"] = brokenDashboards
- _, err := NewDashboardFileReader(cfg, logger, nil)
+ _, err := NewDashboardFileReader(cfg, logger, nil, nil)
require.NoError(t, err)
})
@@ -336,14 +334,14 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2)
- reader1, err := NewDashboardFileReader(cfg1, logger, nil)
+ reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore)
reader1.dashboardProvisioningService = fakeService
require.NoError(t, err)
err = reader1.walkDisk(context.Background())
require.NoError(t, err)
- reader2, err := NewDashboardFileReader(cfg2, logger, nil)
+ reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore)
reader2.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -363,7 +361,7 @@ func TestDashboardFileReader(t *testing.T) {
"folder": defaultDashboards,
},
}
- r, err := NewDashboardFileReader(cfg, logger, nil)
+ r, err := NewDashboardFileReader(cfg, logger, nil, nil)
require.NoError(t, err)
_, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder)
@@ -383,7 +381,7 @@ func TestDashboardFileReader(t *testing.T) {
}
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once()
- r, err := NewDashboardFileReader(cfg, logger, nil)
+ r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
require.NoError(t, err)
_, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder)
@@ -403,7 +401,7 @@ func TestDashboardFileReader(t *testing.T) {
},
}
- r, err := NewDashboardFileReader(cfg, logger, nil)
+ r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
require.NoError(t, err)
_, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder)
@@ -458,7 +456,7 @@ func TestDashboardFileReader(t *testing.T) {
cfg.DisableDeletion = true
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -473,7 +471,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once()
fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
- reader, err := NewDashboardFileReader(cfg, logger, nil)
+ reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
reader.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -512,6 +510,8 @@ func (ffi FakeFileInfo) Sys() interface{} {
return nil
}
-func mockGetDashboardQuery(_ context.Context, _ *models.GetDashboardQuery) error {
+type fakeDashboardStore struct{}
+
+func (fds *fakeDashboardStore) GetDashboard(_ context.Context, _ *models.GetDashboardQuery) error {
return models.ErrDashboardNotFound
}
diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go
index 1af1e7c05ff..b3e39e3072d 100644
--- a/pkg/services/provisioning/dashboards/validator_test.go
+++ b/pkg/services/provisioning/dashboards/validator_test.go
@@ -5,7 +5,6 @@ import (
"sort"
"testing"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -19,11 +18,9 @@ const (
)
func TestDuplicatesValidator(t *testing.T) {
- bus.ClearBusHandlers()
fakeService := &dashboards.FakeDashboardProvisioning{}
defer fakeService.AssertExpectations(t)
- bus.AddHandler("test", mockGetDashboardQuery)
cfg := &config{
Name: "Default",
Type: "file",
@@ -36,7 +33,8 @@ func TestDuplicatesValidator(t *testing.T) {
t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) {
const folderName = "duplicates-validator-folder"
- r, err := NewDashboardFileReader(cfg, logger, nil)
+ fakeStore := &fakeDashboardStore{}
+ r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
require.NoError(t, err)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6)
fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(4)
@@ -55,11 +53,11 @@ func TestDuplicatesValidator(t *testing.T) {
Options: map[string]interface{}{"path": dashboardContainingUID},
}
- reader1, err := NewDashboardFileReader(cfg1, logger, nil)
+ reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore)
reader1.dashboardProvisioningService = fakeService
require.NoError(t, err)
- reader2, err := NewDashboardFileReader(cfg2, logger, nil)
+ reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore)
reader2.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -91,7 +89,8 @@ func TestDuplicatesValidator(t *testing.T) {
t.Run("Duplicates validator should not collect info about duplicate UIDs and titles within folders for different orgs", func(t *testing.T) {
const folderName = "duplicates-validator-folder"
- r, err := NewDashboardFileReader(cfg, logger, nil)
+ fakeStore := &fakeDashboardStore{}
+ r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
require.NoError(t, err)
folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName)
require.NoError(t, err)
@@ -107,11 +106,11 @@ func TestDuplicatesValidator(t *testing.T) {
Options: map[string]interface{}{"path": dashboardContainingUID},
}
- reader1, err := NewDashboardFileReader(cfg1, logger, nil)
+ reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore)
reader1.dashboardProvisioningService = fakeService
require.NoError(t, err)
- reader2, err := NewDashboardFileReader(cfg2, logger, nil)
+ reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore)
reader2.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -154,6 +153,7 @@ func TestDuplicatesValidator(t *testing.T) {
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5)
fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(3)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5)
+ fakeStore := &fakeDashboardStore{}
cfg1 := &config{
Name: "first", Type: "file", OrgID: 1, Folder: "duplicates-validator-folder",
@@ -167,15 +167,15 @@ func TestDuplicatesValidator(t *testing.T) {
Name: "third", Type: "file", OrgID: 2, Folder: "duplicates-validator-folder",
Options: map[string]interface{}{"path": twoDashboardsWithUID},
}
- reader1, err := NewDashboardFileReader(cfg1, logger, nil)
+ reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore)
reader1.dashboardProvisioningService = fakeService
require.NoError(t, err)
- reader2, err := NewDashboardFileReader(cfg2, logger, nil)
+ reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore)
reader2.dashboardProvisioningService = fakeService
require.NoError(t, err)
- reader3, err := NewDashboardFileReader(cfg3, logger, nil)
+ reader3, err := NewDashboardFileReader(cfg3, logger, nil, fakeStore)
reader3.dashboardProvisioningService = fakeService
require.NoError(t, err)
@@ -192,7 +192,7 @@ func TestDuplicatesValidator(t *testing.T) {
duplicates := duplicateValidator.getDuplicates()
- r, err := NewDashboardFileReader(cfg, logger, nil)
+ r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore)
require.NoError(t, err)
folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg1.Folder)
require.NoError(t, err)
@@ -209,7 +209,7 @@ func TestDuplicatesValidator(t *testing.T) {
sort.Strings(titleUsageReaders)
require.Equal(t, []string{"first"}, titleUsageReaders)
- r, err = NewDashboardFileReader(cfg3, logger, nil)
+ r, err = NewDashboardFileReader(cfg3, logger, nil, fakeStore)
require.NoError(t, err)
folderID, err = r.getOrCreateFolderID(context.Background(), cfg3, fakeService, cfg3.Folder)
require.NoError(t, err)
diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go
index 4fb9202524f..091d3329aed 100644
--- a/pkg/services/provisioning/provisioning.go
+++ b/pkg/services/provisioning/provisioning.go
@@ -187,7 +187,7 @@ func (ps *ProvisioningServiceImpl) ProvisionNotifications(ctx context.Context) e
func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) error {
dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards")
- dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardService, ps.SQLStore)
+ dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardService, ps.SQLStore, ps.SQLStore)
if err != nil {
return errutil.Wrap("Failed to create provisioner", err)
}
diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go
index 4493d6569dd..ad2547e3ed6 100644
--- a/pkg/services/provisioning/provisioning_test.go
+++ b/pkg/services/provisioning/provisioning_test.go
@@ -93,7 +93,7 @@ func setup() *serviceTestStruct {
}
serviceTest.service = newProvisioningServiceImpl(
- func(context.Context, string, dashboardstore.DashboardProvisioningService, utils.OrgStore) (dashboards.DashboardProvisioner, error) {
+ func(context.Context, string, dashboardstore.DashboardProvisioningService, utils.OrgStore, utils.DashboardStore) (dashboards.DashboardProvisioner, error) {
return serviceTest.mock, nil
},
nil,
diff --git a/pkg/services/provisioning/utils/utils.go b/pkg/services/provisioning/utils/utils.go
index 18f74408b52..315f4fb251e 100644
--- a/pkg/services/provisioning/utils/utils.go
+++ b/pkg/services/provisioning/utils/utils.go
@@ -12,6 +12,10 @@ type OrgStore interface {
GetOrgById(context.Context, *models.GetOrgByIdQuery) error
}
+type DashboardStore interface {
+ GetDashboard(context.Context, *models.GetDashboardQuery) error
+}
+
func CheckOrgExists(ctx context.Context, store OrgStore, orgID int64) error {
query := models.GetOrgByIdQuery{Id: orgID}
if err := store.GetOrgById(ctx, &query); err != nil {
From b31c7d36548d831bbd1e8a621cbac8f3fc9a5a68 Mon Sep 17 00:00:00 2001
From: Serge Zaitsev
Date: Fri, 8 Apr 2022 14:30:25 +0200
Subject: [PATCH 48/55] Chore: Remove bus from alerting rule (#47508)
* Chore: Remove bus from alerting rule
* fix alerting tests
* fix provide service
---
pkg/api/alerting.go | 3 +--
pkg/services/alerting/engine.go | 1 +
pkg/services/alerting/engine_test.go | 4 ++++
pkg/services/alerting/extractor.go | 6 ++++--
pkg/services/alerting/extractor_test.go | 6 ++++--
pkg/services/alerting/reader.go | 2 +-
pkg/services/alerting/rule.go | 13 ++++++-------
pkg/services/alerting/rule_test.go | 8 ++++----
pkg/services/alerting/test_rule.go | 2 +-
.../libraryelements/libraryelements_test.go | 2 +-
pkg/services/librarypanels/librarypanels_test.go | 2 +-
11 files changed, 28 insertions(+), 21 deletions(-)
diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go
index 4b58ace4bce..487d4ac9905 100644
--- a/pkg/api/alerting.go
+++ b/pkg/api/alerting.go
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/guardian"
@@ -491,7 +490,7 @@ func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response {
SecureSettings: dto.SecureSettings,
}
- if err := bus.Dispatch(c.Req.Context(), cmd); err != nil {
+ if err := hs.AlertNotificationService.HandleNotificationTestCommand(c.Req.Context(), cmd); err != nil {
if errors.Is(err, models.ErrSmtpNotEnabled) {
return response.Error(412, err.Error(), err)
}
diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go
index cb39ff67448..9566d7725b4 100644
--- a/pkg/services/alerting/engine.go
+++ b/pkg/services/alerting/engine.go
@@ -30,6 +30,7 @@ type AlertStore interface {
GetDashboardUIDById(context.Context, *models.GetDashboardRefByIdQuery) error
SetAlertNotificationStateToCompleteCommand(context.Context, *models.SetAlertNotificationStateToCompleteCommand) error
SetAlertNotificationStateToPendingCommand(context.Context, *models.SetAlertNotificationStateToPendingCommand) error
+ GetAlertNotificationUidWithId(context.Context, *models.GetAlertNotificationUidQuery) error
GetAlertNotificationsWithUidToSend(context.Context, *models.GetAlertNotificationsWithUidToSendQuery) error
GetOrCreateAlertNotificationState(context.Context, *models.GetOrCreateNotificationStateQuery) error
SetAlertState(context.Context, *models.SetAlertStateCommand) error
diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go
index 6e7fb674d68..3ed3ee42362 100644
--- a/pkg/services/alerting/engine_test.go
+++ b/pkg/services/alerting/engine_test.go
@@ -65,6 +65,10 @@ func (a *AlertStoreMock) GetAllAlertQueryHandler(c context.Context, cmd *models.
return nil
}
+func (a *AlertStoreMock) GetAlertNotificationUidWithId(c context.Context, query *models.GetAlertNotificationUidQuery) error {
+ return nil
+}
+
func (a *AlertStoreMock) GetAlertNotificationsWithUidToSend(c context.Context, cmd *models.GetAlertNotificationsWithUidToSendQuery) error {
if a.getAlertNotificationsWithUidToSend != nil {
return a.getAlertNotificationsWithUidToSend(c, cmd)
diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go
index 3238b5b60ac..460489b0a3d 100644
--- a/pkg/services/alerting/extractor.go
+++ b/pkg/services/alerting/extractor.go
@@ -22,13 +22,15 @@ type DashAlertExtractor interface {
type DashAlertExtractorService struct {
datasourcePermissionsService permissions.DatasourcePermissionsService
datasourceService datasources.DataSourceService
+ alertStore AlertStore
log log.Logger
}
-func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService) *DashAlertExtractorService {
+func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService, alertStore AlertStore) *DashAlertExtractorService {
return &DashAlertExtractorService{
datasourcePermissionsService: datasourcePermissionsService,
datasourceService: datasourceService,
+ alertStore: alertStore,
log: log.New("alerting.extractor"),
}
}
@@ -231,7 +233,7 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json
alert.Settings = jsonAlert
// validate
- _, err = NewRuleFromDBAlert(ctx, alert, logTranslationFailures)
+ _, err = NewRuleFromDBAlert(ctx, e.alertStore, alert, logTranslationFailures)
if err != nil {
return nil, err
}
diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go
index 90c7499d9e3..9b2dba8b0fc 100644
--- a/pkg/services/alerting/extractor_test.go
+++ b/pkg/services/alerting/extractor_test.go
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -36,7 +37,8 @@ func TestAlertRuleExtraction(t *testing.T) {
}
dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs}
- extractor := ProvideDashAlertExtractorService(dsPermissions, dsService)
+ store := mockstore.NewSQLStoreMock()
+ extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, store)
t.Run("Parsing alert rules from dashboard json", func(t *testing.T) {
dashJSON, err := simplejson.NewJson(json)
@@ -312,7 +314,7 @@ func TestFilterPermissionsErrors(t *testing.T) {
dsPermissions := permissions.NewMockDatasourcePermissionService()
dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs}
- extractor := ProvideDashAlertExtractorService(dsPermissions, dsService)
+ extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, nil)
tc := []struct {
name string
diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go
index 6d4cc8dd80f..ebd41626164 100644
--- a/pkg/services/alerting/reader.go
+++ b/pkg/services/alerting/reader.go
@@ -38,7 +38,7 @@ func (arr *defaultRuleReader) fetch(ctx context.Context) []*Rule {
res := make([]*Rule, 0)
for _, ruleDef := range cmd.Result {
- if model, err := NewRuleFromDBAlert(ctx, ruleDef, false); err != nil {
+ if model, err := NewRuleFromDBAlert(ctx, arr.sqlStore, ruleDef, false); err != nil {
arr.log.Error("Could not build alert model for rule", "ruleId", ruleDef.Id, "error", err)
} else {
res = append(res, model)
diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go
index 05ffc47805d..7dd920d2bfa 100644
--- a/pkg/services/alerting/rule.go
+++ b/pkg/services/alerting/rule.go
@@ -9,7 +9,6 @@ import (
"strconv"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
)
@@ -145,7 +144,7 @@ func getForValue(rawFor string) (time.Duration, error) {
// NewRuleFromDBAlert maps a db version of
// alert to an in-memory version.
-func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslationFailures bool) (*Rule, error) {
+func NewRuleFromDBAlert(ctx context.Context, store AlertStore, ruleDef *models.Alert, logTranslationFailures bool) (*Rule, error) {
model := &Rule{}
model.ID = ruleDef.Id
model.OrgID = ruleDef.OrgId
@@ -170,7 +169,7 @@ func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslati
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v)
if id, err := jsonModel.Get("id").Int64(); err == nil {
- uid, err := translateNotificationIDToUID(ctx, id, ruleDef.OrgId)
+ uid, err := translateNotificationIDToUID(ctx, store, id, ruleDef.OrgId)
if err != nil {
if !errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID) {
logger.Error("Failed to translate notification id to uid", "error", err.Error(), "dashboardId", model.DashboardID, "alert", model.Name, "panelId", model.PanelID, "notificationId", id)
@@ -211,8 +210,8 @@ func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslati
return model, nil
}
-func translateNotificationIDToUID(ctx context.Context, id int64, orgID int64) (string, error) {
- notificationUID, err := getAlertNotificationUIDByIDAndOrgID(ctx, id, orgID)
+func translateNotificationIDToUID(ctx context.Context, store AlertStore, id int64, orgID int64) (string, error) {
+ notificationUID, err := getAlertNotificationUIDByIDAndOrgID(ctx, store, id, orgID)
if err != nil {
return "", err
}
@@ -220,13 +219,13 @@ func translateNotificationIDToUID(ctx context.Context, id int64, orgID int64) (s
return notificationUID, nil
}
-func getAlertNotificationUIDByIDAndOrgID(ctx context.Context, notificationID int64, orgID int64) (string, error) {
+func getAlertNotificationUIDByIDAndOrgID(ctx context.Context, store AlertStore, notificationID int64, orgID int64) (string, error) {
query := &models.GetAlertNotificationUidQuery{
OrgId: orgID,
Id: notificationID,
}
- if err := bus.Dispatch(ctx, query); err != nil {
+ if err := store.GetAlertNotificationUidWithId(ctx, query); err != nil {
return "", err
}
diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go
index 55c603a4c2b..506b7cb03b6 100644
--- a/pkg/services/alerting/rule_test.go
+++ b/pkg/services/alerting/rule_test.go
@@ -130,7 +130,7 @@ func TestAlertRuleModel(t *testing.T) {
Settings: alertJSON,
}
- alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false)
+ alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false)
require.Nil(t, err)
require.Len(t, alertRule.Conditions, 1)
@@ -169,7 +169,7 @@ func TestAlertRuleModel(t *testing.T) {
Settings: alertJSON,
}
- alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false)
+ alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false)
require.Nil(t, err)
require.NotContains(t, alertRule.Notifications, "999")
require.Contains(t, alertRule.Notifications, "notifier2")
@@ -200,7 +200,7 @@ func TestAlertRuleModel(t *testing.T) {
Settings: alertJSON,
}
- alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false)
+ alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false)
require.Nil(t, err)
require.EqualValues(t, alertRule.Frequency, 60)
})
@@ -238,7 +238,7 @@ func TestAlertRuleModel(t *testing.T) {
Settings: alertJSON,
}
- _, err := NewRuleFromDBAlert(context.Background(), alert, false)
+ _, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false)
require.NotNil(t, err)
require.EqualValues(t, err.Error(), "alert validation error: Neither id nor uid is specified in 'notifications' block, type assertion to string failed AlertId: 1 PanelId: 1 DashboardId: 1")
})
diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go
index b8c4e2e5edf..011990faad7 100644
--- a/pkg/services/alerting/test_rule.go
+++ b/pkg/services/alerting/test_rule.go
@@ -25,7 +25,7 @@ func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID
if alert.PanelId != panelID {
continue
}
- rule, err := NewRuleFromDBAlert(context.Background(), alert, true)
+ rule, err := NewRuleFromDBAlert(context.Background(), e.sqlStore, alert, true)
if err != nil {
return nil, err
}
diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go
index 2358b8f6342..a4bc6ed6cfa 100644
--- a/pkg/services/libraryelements/libraryelements_test.go
+++ b/pkg/services/libraryelements/libraryelements_test.go
@@ -201,7 +201,7 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.Sign
}
dashboardStore := database.ProvideDashboardStore(sqlStore)
- dashAlertExtractor := alerting.ProvideDashAlertExtractorService(nil, nil)
+ dashAlertExtractor := alerting.ProvideDashAlertExtractorService(nil, nil, nil)
service := dashboardservice.ProvideDashboardService(
setting.NewCfg(), dashboardStore, dashAlertExtractor,
featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(),
diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go
index 0d13913c966..26f28a798c2 100644
--- a/pkg/services/librarypanels/librarypanels_test.go
+++ b/pkg/services/librarypanels/librarypanels_test.go
@@ -1422,7 +1422,7 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user *models.Sig
}
dashboardStore := database.ProvideDashboardStore(sqlStore)
- dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil)
+ dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil, nil)
service := dashboardservice.ProvideDashboardService(
setting.NewCfg(), dashboardStore, dashAlertService,
featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(),
From c530100d45a7235acc773c43970e8a5d99faede7 Mon Sep 17 00:00:00 2001
From: Gilles De Mey
Date: Fri, 8 Apr 2022 15:26:33 +0200
Subject: [PATCH 49/55] fix: don't allow editing rule types for existing rules
(#47512)
---
.../components/rule-editor/AlertTypeStep.tsx | 39 +++++++++----------
1 file changed, 18 insertions(+), 21 deletions(-)
diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx
index aa2179f2265..b06e7e2497c 100644
--- a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx
@@ -38,27 +38,24 @@ export const AlertTypeStep: FC = ({ editingExistingRule }) => {
return (
-
- (
-
- )}
- name="type"
- control={control}
- rules={{
- required: { value: true, message: 'Please select alert type' },
- }}
- />
-
+ {!editingExistingRule && (
+
+ (
+
+ )}
+ name="type"
+ control={control}
+ rules={{
+ required: { value: true, message: 'Please select alert type' },
+ }}
+ />
+
+ )}
Date: Fri, 8 Apr 2022 16:09:01 +0200
Subject: [PATCH 50/55] fixed dashboard spelling (#47515)
---
docs/sources/dashboards/dashboard-ui/dashboard-row.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/sources/dashboards/dashboard-ui/dashboard-row.md b/docs/sources/dashboards/dashboard-ui/dashboard-row.md
index b00e1f1c80c..52e637ed689 100644
--- a/docs/sources/dashboards/dashboard-ui/dashboard-row.md
+++ b/docs/sources/dashboards/dashboard-ui/dashboard-row.md
@@ -4,7 +4,7 @@ aliases = ["/docs/grafana/latest/features/dashboard/dashboards/"]
weight = 80
+++
-# Dshboard rows
+# Dashboard rows
A dashboard row is a logical divider within a dashboard. It is used to group panels together.
From 18e93c70773141b9a43857300b89a69e543cfdea Mon Sep 17 00:00:00 2001
From: Serge Zaitsev
Date: Fri, 8 Apr 2022 16:15:06 +0200
Subject: [PATCH 51/55] Chore: Remove bus (#47511)
* Chore: Remove bus
* remove unused const
---
pkg/api/admin_users_test.go | 28 ----
pkg/api/alerting_test.go | 7 -
pkg/api/annotations_test.go | 9 --
pkg/api/common_test.go | 4 -
pkg/api/dashboard_snapshot_test.go | 8 --
pkg/api/dashboard_test.go | 20 ---
pkg/api/folder_permission_test.go | 3 -
pkg/api/folder_test.go | 4 -
pkg/api/pluginproxy/ds_proxy_test.go | 13 --
pkg/api/plugins_test.go | 3 -
pkg/api/short_url_test.go | 3 -
pkg/api/user_test.go | 19 ---
pkg/api/user_token_test.go | 15 --
pkg/bus/bus.go | 107 --------------
pkg/bus/bus_test.go | 135 ------------------
pkg/expr/service_test.go | 7 -
pkg/login/auth.go | 2 -
pkg/middleware/middleware_basic_auth_test.go | 16 ---
pkg/middleware/middleware_jwt_auth_test.go | 9 --
pkg/middleware/middleware_test.go | 3 -
pkg/middleware/org_redirect_test.go | 5 -
pkg/middleware/recovery_test.go | 3 -
pkg/services/alerting/notifier_test.go | 9 --
pkg/services/alerting/service.go | 14 --
.../manager/dashboard_service_test.go | 3 -
.../dashboardsnapshots/dashboardsnapshots.go | 6 -
.../datasources/service/datasource_service.go | 8 --
.../authinfoservice/database/database.go | 9 --
.../login/loginservice/loginservice.go | 1 -
.../login/loginservice/loginservice_test.go | 2 -
pkg/services/notifications/notifications.go | 7 -
.../notifications/notifications_test.go | 37 +++--
.../service/dashboard_updater_test.go | 2 -
pkg/services/search/service.go | 1 -
pkg/services/sqlstore/alert.go | 11 --
pkg/services/sqlstore/alert_notification.go | 5 -
.../sqlstore/alert_notification_test.go | 9 +-
pkg/services/sqlstore/apikey.go | 9 --
pkg/services/sqlstore/dashboard.go | 13 --
pkg/services/sqlstore/dashboard_acl.go | 5 -
pkg/services/sqlstore/dashboard_version.go | 7 -
pkg/services/sqlstore/login_attempt.go | 7 -
pkg/services/sqlstore/org.go | 11 --
pkg/services/sqlstore/org_users.go | 8 --
pkg/services/sqlstore/playlist.go | 10 --
pkg/services/sqlstore/preferences.go | 8 --
pkg/services/sqlstore/quota.go | 11 --
pkg/services/sqlstore/sqlstore.go | 22 ---
pkg/services/sqlstore/star.go | 8 --
pkg/services/sqlstore/stats.go | 10 --
pkg/services/sqlstore/team.go | 14 --
pkg/services/sqlstore/temp_user.go | 10 --
pkg/services/sqlstore/user.go | 20 ---
53 files changed, 19 insertions(+), 701 deletions(-)
diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go
index b2d331eb213..0ba11679d3c 100644
--- a/pkg/api/admin_users_test.go
+++ b/pkg/api/admin_users_test.go
@@ -1,7 +1,6 @@
package api
import (
- "context"
"fmt"
"testing"
@@ -49,11 +48,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
mock := mockstore.NewSQLStoreMock()
adminLogoutUserScenario(t, "Should not be allowed when calling POST on",
"/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
- bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
- cmd.Result = &models.User{Id: testUserID}
- return nil
- })
-
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
}, mock)
@@ -182,8 +176,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
- bus.ClearBusHandlers()
-
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
@@ -202,8 +194,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
- bus.ClearBusHandlers()
-
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
@@ -222,8 +212,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
- bus.ClearBusHandlers()
-
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
@@ -241,8 +229,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
adminCreateUserScenario(t, "Should return an error", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
- bus.ClearBusHandlers()
-
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 412, sc.resp.Code)
@@ -256,8 +242,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
func putAdminScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
hs := &HTTPServer{
Cfg: setting.NewCfg(),
SQLStore: sqlStore,
@@ -284,8 +268,6 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string
func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: auth.NewFakeUserAuthTokenService(),
@@ -312,8 +294,6 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern
func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -343,8 +323,6 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou
func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -372,8 +350,6 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route
func adminDisableUserScenario(t *testing.T, desc string, action string, url string, routePattern string, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
authInfoService := &logintest.AuthInfoServiceFake{}
@@ -410,8 +386,6 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern
SQLStore: mockstore.NewSQLStoreMock(),
}
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
sc := setupScenarioContext(t, url)
sc.sqlStore = hs.SQLStore
sc.authInfoService = &logintest.AuthInfoServiceFake{}
@@ -430,8 +404,6 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern
func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
hs := HTTPServer{
Bus: bus.GetBus(),
Login: loginservice.LoginServiceMock{
diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go
index 1f09ca06a19..12deadcc9d5 100644
--- a/pkg/api/alerting_test.go
+++ b/pkg/api/alerting_test.go
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/search"
@@ -124,18 +123,12 @@ func TestAlertingAPIEndpoint(t *testing.T) {
}
func callPauseAlert(sc *scenarioContext) {
- bus.AddHandler("test", func(ctx context.Context, cmd *models.PauseAlertCommand) error {
- return nil
- })
-
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func postAlertScenario(t *testing.T, hs *HTTPServer, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.PauseAlertCommand, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go
index 30b0af8b746..300a751454a 100644
--- a/pkg/api/annotations_test.go
+++ b/pkg/api/annotations_test.go
@@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
@@ -293,8 +292,6 @@ var fakeAnnoRepo *fakeAnnotationsRepo
func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.PostAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
@@ -324,8 +321,6 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern
func putAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
@@ -354,8 +349,6 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s
func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
@@ -385,8 +378,6 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern
func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.MassDeleteAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go
index f1870cca517..41ccf6ddd1c 100644
--- a/pkg/api/common_test.go
+++ b/pkg/api/common_test.go
@@ -53,8 +53,6 @@ func loggedInUserScenario(t *testing.T, desc string, url string, routePattern st
func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
sc := setupScenarioContext(t, url)
sc.sqlStore = sqlStore
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
@@ -82,8 +80,6 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url
func anonymousUserScenario(t *testing.T, desc string, method string, url string, routePattern string, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
sc.context = c
diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go
index a6d1bcef985..d34d285c46d 100644
--- a/pkg/api/dashboard_snapshot_test.go
+++ b/pkg/api/dashboard_snapshot_test.go
@@ -1,7 +1,6 @@
package api
import (
- "context"
"fmt"
"net/http"
"net/http/httptest"
@@ -9,7 +8,6 @@ import (
"testing"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
@@ -52,12 +50,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
sqlmock.ExpectedDashboardAclInfoList = aclMockResp
sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{}
- // we need it here for now for the guadian service to work
- bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error {
- query.Result = aclMockResp
- return nil
- })
-
return mockSnapshotResult
}
diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go
index 54b77fbb520..24ec9479b2d 100644
--- a/pkg/api/dashboard_test.go
+++ b/pkg/api/dashboard_test.go
@@ -957,31 +957,17 @@ func (hs *HTTPServer) callGetDashboard(sc *scenarioContext) {
}
func (hs *HTTPServer) callGetDashboardVersion(sc *scenarioContext) {
- bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardVersionQuery) error {
- query.Result = &models.DashboardVersion{}
- return nil
- })
-
sc.handlerFunc = hs.GetDashboardVersion
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) {
- bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardVersionsQuery) error {
- query.Result = []*models.DashboardVersionDTO{}
- return nil
- })
-
sc.handlerFunc = hs.GetDashboardVersions
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T,
sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) {
- bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error {
- return nil
- })
-
hs.dashboardService = mockDashboard
sc.handlerFunc = hs.DeleteDashboardByUID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@@ -1003,8 +989,6 @@ func callPostDashboardShouldReturnSuccess(sc *scenarioContext) {
func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, cmd models.SaveDashboardCommand, dashboardService dashboards.DashboardService, folderService dashboards.FolderService, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
cfg := setting.NewCfg()
hs := HTTPServer{
Bus: bus.GetBus(),
@@ -1040,8 +1024,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role models.RoleType, fn scenarioFunc, sqlmock sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
cfg := setting.NewCfg()
hs := HTTPServer{
Cfg: cfg,
@@ -1076,8 +1058,6 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
cfg := setting.NewCfg()
mockSQLStore := mockstore.NewSQLStoreMock()
hs := HTTPServer{
diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go
index 18b45c069cb..9198f15e55b 100644
--- a/pkg/api/folder_permission_test.go
+++ b/pkg/api/folder_permission_test.go
@@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
@@ -355,8 +354,6 @@ func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) {
func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) {
t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
sc := setupScenarioContext(t, ctx.url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go
index e61f232c1f3..aa437ea2cf5 100644
--- a/pkg/api/folder_test.go
+++ b/pkg/api/folder_test.go
@@ -138,8 +138,6 @@ func callCreateFolder(sc *scenarioContext) {
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
cmd models.CreateFolderCommand, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
hs := HTTPServer{
Bus: bus.GetBus(),
Cfg: setting.NewCfg(),
@@ -170,8 +168,6 @@ func callUpdateFolder(sc *scenarioContext) {
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
cmd models.UpdateFolderCommand, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
hs := HTTPServer{
Cfg: setting.NewCfg(),
folderService: folderService,
diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go
index 370cf09883f..9af527368de 100644
--- a/pkg/api/pluginproxy/ds_proxy_test.go
+++ b/pkg/api/pluginproxy/ds_proxy_test.go
@@ -462,19 +462,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
})
t.Run("When proxying a datasource that has OAuth token pass-through enabled", func(t *testing.T) {
- bus.AddHandler("test", func(ctx context.Context, query *models.GetAuthInfoQuery) error {
- query.Result = &models.UserAuth{
- Id: 1,
- UserId: 1,
- AuthModule: "generic_oauth",
- OAuthAccessToken: "testtoken",
- OAuthRefreshToken: "testrefreshtoken",
- OAuthTokenType: "Bearer",
- OAuthExpiry: time.Now().AddDate(0, 0, 1),
- }
- return nil
- })
-
ds := &models.DataSource{
Type: "custom-datasource",
Url: "http://host/root/",
diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go
index 8c8674fd6c4..3504d51e4cf 100644
--- a/pkg/api/plugins_test.go
+++ b/pkg/api/plugins_test.go
@@ -15,7 +15,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@@ -218,8 +217,6 @@ func callGetPluginAsset(sc *scenarioContext) {
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store,
logger log.Logger, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
hs := HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: pluginStore,
diff --git a/pkg/api/short_url_test.go b/pkg/api/short_url_test.go
index 752004738f2..bb592a9d30a 100644
--- a/pkg/api/short_url_test.go
+++ b/pkg/api/short_url_test.go
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/shorturls"
@@ -54,8 +53,6 @@ func callCreateShortURL(sc *scenarioContext) {
func createShortURLScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CreateShortURLCmd, shortURLService shorturls.Service, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
hs := HTTPServer{
Cfg: setting.NewCfg(),
ShortURLService: shortURLService,
diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go
index bc4df9b7ec9..fbf77357ad7 100644
--- a/pkg/api/user_test.go
+++ b/pkg/api/user_test.go
@@ -107,25 +107,6 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
}, mock)
loggedInUserScenario(t, "When calling GET on", "/api/users/lookup", "/api/users/lookup", func(sc *scenarioContext) {
- fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC)
- bus.AddHandler("test", func(ctx context.Context, query *models.GetUserByLoginQuery) error {
- require.Equal(t, "danlee", query.LoginOrEmail)
-
- query.Result = &models.User{
- Id: int64(1),
- Email: "daniel@grafana.com",
- Name: "Daniel",
- Login: "danlee",
- Theme: "light",
- IsAdmin: true,
- OrgId: int64(2),
- IsDisabled: false,
- Updated: fakeNow,
- Created: fakeNow,
- }
-
- return nil
- })
createUserCmd := models.CreateUserCommand{
Email: fmt.Sprint("admin", "@test.com"),
Name: "admin",
diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go
index 37766385f13..4a07a6b737e 100644
--- a/pkg/api/user_token_test.go
+++ b/pkg/api/user_token_test.go
@@ -79,11 +79,6 @@ func TestUserTokenAPIEndpoint(t *testing.T) {
token := &models.UserToken{Id: 2}
mock := mockstore.NewSQLStoreMock()
revokeUserAuthTokenInternalScenario(t, "Should not be successful", cmd, testUserID, token, func(sc *scenarioContext) {
- bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
- cmd.Result = &models.User{Id: testUserID}
- return nil
- })
-
sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) {
return token, nil
}
@@ -153,8 +148,6 @@ func TestUserTokenAPIEndpoint(t *testing.T) {
func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd,
userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -183,8 +176,6 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat
func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -212,8 +203,6 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte
func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: auth.NewFakeUserAuthTokenService(),
@@ -239,8 +228,6 @@ func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId
func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.RevokeAuthTokenCmd, userId int64,
token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -267,8 +254,6 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.R
func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go
index e3f207ef814..28fdc4b002e 100644
--- a/pkg/bus/bus.go
+++ b/pkg/bus/bus.go
@@ -21,41 +21,17 @@ type Msg interface{}
// ErrHandlerNotFound defines an error if a handler is not found
var ErrHandlerNotFound = errors.New("handler not found")
-// TransactionManager defines a transaction interface
-type TransactionManager interface {
- InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
-}
-
// Bus type defines the bus interface structure
type Bus interface {
- Dispatch(ctx context.Context, msg Msg) error
-
Publish(ctx context.Context, msg Msg) error
-
- // InTransaction starts a transaction and store it in the context.
- // The caller can then pass a function with multiple DispatchCtx calls that
- // all will be executed in the same transaction. InTransaction will rollback if the
- // callback returns an error.
- InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
-
- AddHandler(handler HandlerFunc)
-
AddEventListener(handler HandlerFunc)
-
- // SetTransactionManager allows the user to replace the internal
- // noop TransactionManager that is responsible for managing
- // transactions in `InTransaction`
- SetTransactionManager(tm TransactionManager)
}
// InProcBus defines the bus structure
type InProcBus struct {
logger log.Logger
- handlers map[string]HandlerFunc
- handlersWithCtx map[string]HandlerFunc
listeners map[string][]HandlerFunc
listenersWithCtx map[string][]HandlerFunc
- txMng TransactionManager
tracer tracing.Tracer
}
@@ -64,11 +40,6 @@ func ProvideBus(tracer tracing.Tracer) *InProcBus {
return globalBus
}
-// InTransaction defines an in transaction function
-func (b *InProcBus) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
- return b.txMng.InTransaction(ctx, fn)
-}
-
// temp stuff, not sure how to handle bus instance, and init yet
var globalBus = New()
@@ -76,11 +47,8 @@ var globalBus = New()
func New() *InProcBus {
bus := &InProcBus{
logger: log.New("bus"),
- handlers: make(map[string]HandlerFunc),
- handlersWithCtx: make(map[string]HandlerFunc),
listeners: make(map[string][]HandlerFunc),
listenersWithCtx: make(map[string][]HandlerFunc),
- txMng: &noopTransactionManager{},
}
bus.tracer = tracing.InitializeForBus()
return bus
@@ -91,46 +59,6 @@ func GetBus() Bus {
return globalBus
}
-// SetTransactionManager function assign a transaction manager to the bus.
-func (b *InProcBus) SetTransactionManager(tm TransactionManager) {
- b.txMng = tm
-}
-
-// DispatchCtx function dispatch a message to the bus context.
-func (b *InProcBus) Dispatch(ctx context.Context, msg Msg) error {
- var msgName = reflect.TypeOf(msg).Elem().Name()
-
- ctx, span := b.tracer.Start(ctx, "bus - "+msgName)
- defer span.End()
-
- span.SetAttributes("msg", msgName, attribute.Key("msg").String(msgName))
-
- withCtx := true
- var handler = b.handlersWithCtx[msgName]
- if handler == nil {
- withCtx = false
- handler = b.handlers[msgName]
- if handler == nil {
- return ErrHandlerNotFound
- }
- }
-
- var params = []reflect.Value{}
- if withCtx {
- params = append(params, reflect.ValueOf(ctx))
- } else if setting.Env == setting.Dev {
- b.logger.Warn("DispatchCtx called with message handler registered using AddHandler and should be changed to use AddHandler", "msgName", msgName)
- }
- params = append(params, reflect.ValueOf(msg))
-
- ret := reflect.ValueOf(handler).Call(params)
- err := ret[0].Interface()
- if err == nil {
- return nil
- }
- return err.(error)
-}
-
// PublishCtx function publish a message to the bus listener.
func (b *InProcBus) Publish(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
@@ -176,17 +104,6 @@ func callListeners(listeners []HandlerFunc, params []reflect.Value) error {
return nil
}
-func (b *InProcBus) AddHandler(handler HandlerFunc) {
- handlerType := reflect.TypeOf(handler)
- queryTypeName := handlerType.In(1).Elem().Name()
- b.handlersWithCtx[queryTypeName] = handler
-}
-
-// GetHandlerCtx returns the handler function for the given struct name.
-func (b *InProcBus) GetHandlerCtx(name string) HandlerFunc {
- return b.handlersWithCtx[name]
-}
-
func (b *InProcBus) AddEventListener(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
eventName := handlerType.In(1).Elem().Name()
@@ -197,36 +114,12 @@ func (b *InProcBus) AddEventListener(handler HandlerFunc) {
b.listenersWithCtx[eventName] = append(b.listenersWithCtx[eventName], handler)
}
-// AddHandler attaches a handler function to the global bus context.
-// Package level function.
-func AddHandler(implName string, handler HandlerFunc) {
- globalBus.AddHandler(handler)
-}
-
// AddEventListenerCtx attaches a handler function to the event listener.
// Package level function.
func AddEventListener(handler HandlerFunc) {
globalBus.AddEventListener(handler)
}
-func Dispatch(ctx context.Context, msg Msg) error {
- return globalBus.Dispatch(ctx, msg)
-}
-
func Publish(ctx context.Context, msg Msg) error {
return globalBus.Publish(ctx, msg)
}
-
-func GetHandlerCtx(name string) HandlerFunc {
- return globalBus.GetHandlerCtx(name)
-}
-
-func ClearBusHandlers() {
- globalBus = New()
-}
-
-type noopTransactionManager struct{}
-
-func (*noopTransactionManager) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
- return fn(ctx)
-}
diff --git a/pkg/bus/bus_test.go b/pkg/bus/bus_test.go
index 05789ef1f8b..3bb6a6bb86e 100644
--- a/pkg/bus/bus_test.go
+++ b/pkg/bus/bus_test.go
@@ -2,7 +2,6 @@ package bus
import (
"context"
- "errors"
"testing"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -14,140 +13,6 @@ type testQuery struct {
Resp string
}
-func TestDispatch(t *testing.T) {
- bus := New()
- tracer, err := tracing.InitializeTracerForTest()
- require.NoError(t, err)
- bus.tracer = tracer
-
- var invoked bool
-
- bus.AddHandler(func(ctx context.Context, query *testQuery) error {
- invoked = true
- return nil
- })
-
- require.NoError(t, err)
- err = bus.Dispatch(context.Background(), &testQuery{})
- require.NoError(t, err)
-
- require.True(t, invoked, "expected handler to be called")
-}
-
-func TestDispatch_NoRegisteredHandler(t *testing.T) {
- bus := New()
- tracer, err := tracing.InitializeTracerForTest()
- require.NoError(t, err)
- bus.tracer = tracer
-
- err = bus.Dispatch(context.Background(), &testQuery{})
- require.Equal(t, err, ErrHandlerNotFound,
- "expected bus to return HandlerNotFound since no handler is registered")
-}
-
-func TestDispatch_ContextHandler(t *testing.T) {
- bus := New()
- tracer, err := tracing.InitializeTracerForTest()
- require.NoError(t, err)
- bus.tracer = tracer
-
- var invoked bool
-
- bus.AddHandler(func(ctx context.Context, query *testQuery) error {
- invoked = true
- return nil
- })
-
- err = bus.Dispatch(context.Background(), &testQuery{})
- require.NoError(t, err)
-
- require.True(t, invoked, "expected handler to be called")
-}
-
-func TestDispatchCtx(t *testing.T) {
- bus := New()
- tracer, err := tracing.InitializeTracerForTest()
- require.NoError(t, err)
- bus.tracer = tracer
-
- var invoked bool
-
- bus.AddHandler(func(ctx context.Context, query *testQuery) error {
- invoked = true
- return nil
- })
-
- err = bus.Dispatch(context.Background(), &testQuery{})
- require.NoError(t, err)
-
- require.True(t, invoked, "expected handler to be called")
-}
-
-func TestDispatchCtx_NoContextHandler(t *testing.T) {
- bus := New()
- tracer, err := tracing.InitializeTracerForTest()
- require.NoError(t, err)
- bus.tracer = tracer
-
- var invoked bool
-
- bus.AddHandler(func(ctx context.Context, query *testQuery) error {
- invoked = true
- return nil
- })
-
- err = bus.Dispatch(context.Background(), &testQuery{})
- require.NoError(t, err)
-
- require.True(t, invoked, "expected handler to be called")
-}
-
-func TestDispatchCtx_NoRegisteredHandler(t *testing.T) {
- bus := New()
- tracer, err := tracing.InitializeTracerForTest()
- require.NoError(t, err)
- bus.tracer = tracer
-
- err = bus.Dispatch(context.Background(), &testQuery{})
- require.Equal(t, err, ErrHandlerNotFound,
- "expected bus to return HandlerNotFound since no handler is registered")
-}
-
-func TestQuery(t *testing.T) {
- bus := New()
- tracer, err := tracing.InitializeTracerForTest()
- require.NoError(t, err)
- bus.tracer = tracer
-
- want := "hello from handler"
-
- bus.AddHandler(func(ctx context.Context, q *testQuery) error {
- q.Resp = want
- return nil
- })
-
- q := &testQuery{}
-
- err = bus.Dispatch(context.Background(), q)
- require.NoError(t, err, "unable to dispatch query")
-
- require.Equal(t, want, q.Resp)
-}
-
-func TestQuery_HandlerReturnsError(t *testing.T) {
- bus := New()
- tracer, err := tracing.InitializeTracerForTest()
- require.NoError(t, err)
- bus.tracer = tracer
-
- bus.AddHandler(func(ctx context.Context, query *testQuery) error {
- return errors.New("handler error")
- })
-
- err = bus.Dispatch(context.Background(), &testQuery{})
- require.Error(t, err, "expected error but got none")
-}
-
func TestEventPublish(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
diff --git a/pkg/expr/service_test.go b/pkg/expr/service_test.go
index 0449a98a4cc..6741174baad 100644
--- a/pkg/expr/service_test.go
+++ b/pkg/expr/service_test.go
@@ -10,8 +10,6 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
- "github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
@@ -38,11 +36,6 @@ func TestService(t *testing.T) {
secretsService: secretsService,
}
- bus.AddHandler("test", func(_ context.Context, query *models.GetDataSourceQuery) error {
- query.Result = &models.DataSource{Uid: "1", OrgId: 1, Type: "test", JsonData: simplejson.New()}
- return nil
- })
-
queries := []Query{
{
RefID: "A",
diff --git a/pkg/login/auth.go b/pkg/login/auth.go
index ae76ee3bec5..d75b1cd8ad2 100644
--- a/pkg/login/auth.go
+++ b/pkg/login/auth.go
@@ -4,7 +4,6 @@ import (
"context"
"errors"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
@@ -41,7 +40,6 @@ func ProvideService(store sqlstore.Store, loginService login.Service) *Authentic
store: store,
loginService: loginService,
}
- bus.AddHandler("auth", a.AuthenticateUser)
return a
}
diff --git a/pkg/middleware/middleware_basic_auth_test.go b/pkg/middleware/middleware_basic_auth_test.go
index dda1b609269..5836a0d315b 100644
--- a/pkg/middleware/middleware_basic_auth_test.go
+++ b/pkg/middleware/middleware_basic_auth_test.go
@@ -1,11 +1,9 @@
package middleware
import (
- "context"
"encoding/json"
"testing"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
@@ -42,22 +40,8 @@ func TestMiddlewareBasicAuth(t *testing.T) {
middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) {
const password = "MyPass"
- const salt = "Salt"
const orgID int64 = 2
- bus.AddHandler("grafana-auth", func(ctx context.Context, query *models.LoginUserQuery) error {
- t.Log("Handling LoginUserQuery")
- encoded, err := util.EncodePassword(password, salt)
- if err != nil {
- return err
- }
- query.User = &models.User{
- Password: encoded,
- Salt: salt,
- }
- return nil
- })
-
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: orgID, UserId: id}
authHeader := util.GetBasicAuthHeader("myUser", password)
diff --git a/pkg/middleware/middleware_jwt_auth_test.go b/pkg/middleware/middleware_jwt_auth_test.go
index 02d4075201d..31403464d69 100644
--- a/pkg/middleware/middleware_jwt_auth_test.go
+++ b/pkg/middleware/middleware_jwt_auth_test.go
@@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/setting"
@@ -109,14 +108,6 @@ func TestMiddlewareJWTAuth(t *testing.T) {
}, nil
}
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{UserId: id, OrgId: orgID, Email: myEmail}
- bus.AddHandler("upsert-user", func(ctx context.Context, command *models.UpsertUserCommand) error {
- command.Result = &models.User{
- Id: id,
- Name: command.ExternalUser.Name,
- Email: command.ExternalUser.Email,
- }
- return nil
- })
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go
index b52083dbeed..76dbd87b1be 100644
--- a/pkg/middleware/middleware_test.go
+++ b/pkg/middleware/middleware_test.go
@@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/api/dtos"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
@@ -570,8 +569,6 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
t.Helper()
t.Run(desc, func(t *testing.T) {
- t.Cleanup(bus.ClearBusHandlers)
-
logger := log.New("test")
loginMaxLifetime, err := gtime.ParseDuration("30d")
diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go
index 22450ec880e..f943d12bd0a 100644
--- a/pkg/middleware/org_redirect_test.go
+++ b/pkg/middleware/org_redirect_test.go
@@ -5,7 +5,6 @@ import (
"fmt"
"testing"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/require"
)
@@ -47,10 +46,6 @@ func TestOrgRedirectMiddleware(t *testing.T) {
middlewareScenario(t, tc.desc, func(t *testing.T, sc *scenarioContext) {
sc.withTokenSessionCookie("token")
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 1, UserId: 12}
- bus.AddHandler("test", func(ctx context.Context, query *models.SetUsingOrgCommand) error {
- return nil
- })
-
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 0,
diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go
index 7e3e4a5939f..251558056dc 100644
--- a/pkg/middleware/recovery_test.go
+++ b/pkg/middleware/recovery_test.go
@@ -5,7 +5,6 @@ import (
"strings"
"testing"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
@@ -48,8 +47,6 @@ func panicHandler(c *models.ReqContext) {
func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
t.Run(desc, func(t *testing.T) {
- defer bus.ClearBusHandlers()
-
cfg := setting.NewCfg()
cfg.ErrTemplateName = "error-template"
sc := &scenarioContext{
diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go
index 558697083c8..c921908c6b8 100644
--- a/pkg/services/alerting/notifier_test.go
+++ b/pkg/services/alerting/notifier_test.go
@@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
- "github.com/grafana/grafana/pkg/bus"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/imguploader"
@@ -205,14 +204,6 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext
return nil
}
- bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error {
- return nil
- })
-
- bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error {
- return nil
- })
-
setting.AlertingNotificationTimeout = 30 * time.Second
scenarioCtx := &scenarioContext{
diff --git a/pkg/services/alerting/service.go b/pkg/services/alerting/service.go
index 5031b4cb9cc..8db32e18728 100644
--- a/pkg/services/alerting/service.go
+++ b/pkg/services/alerting/service.go
@@ -28,20 +28,6 @@ func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, encryptionService enc
NotificationService: notificationService,
}
- s.Bus.AddHandler(s.GetAlertNotifications)
- s.Bus.AddHandler(s.CreateAlertNotificationCommand)
- s.Bus.AddHandler(s.UpdateAlertNotification)
- s.Bus.AddHandler(s.DeleteAlertNotification)
- s.Bus.AddHandler(s.GetAllAlertNotifications)
- s.Bus.AddHandler(s.GetOrCreateAlertNotificationState)
- s.Bus.AddHandler(s.SetAlertNotificationStateToCompleteCommand)
- s.Bus.AddHandler(s.SetAlertNotificationStateToPendingCommand)
- s.Bus.AddHandler(s.GetAlertNotificationsWithUid)
- s.Bus.AddHandler(s.UpdateAlertNotificationWithUid)
- s.Bus.AddHandler(s.DeleteAlertNotificationWithUid)
- s.Bus.AddHandler(s.GetAlertNotificationsWithUidToSend)
- s.Bus.AddHandler(s.HandleNotificationTestCommand)
-
return s
}
diff --git a/pkg/services/dashboards/manager/dashboard_service_test.go b/pkg/services/dashboards/manager/dashboard_service_test.go
index 7e303d70471..b1a11805674 100644
--- a/pkg/services/dashboards/manager/dashboard_service_test.go
+++ b/pkg/services/dashboards/manager/dashboard_service_test.go
@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
@@ -22,8 +21,6 @@ import (
func TestDashboardService(t *testing.T) {
t.Run("Dashboard service tests", func(t *testing.T) {
- bus.ClearBusHandlers()
-
fakeStore := m.FakeDashboardStore{}
defer fakeStore.AssertExpectations(t)
service := &DashboardServiceImpl{
diff --git a/pkg/services/dashboardsnapshots/dashboardsnapshots.go b/pkg/services/dashboardsnapshots/dashboardsnapshots.go
index 990e60bb818..88745a181ed 100644
--- a/pkg/services/dashboardsnapshots/dashboardsnapshots.go
+++ b/pkg/services/dashboardsnapshots/dashboardsnapshots.go
@@ -23,12 +23,6 @@ func ProvideService(bus bus.Bus, store sqlstore.Store, secretsService secrets.Se
SecretsService: secretsService,
}
- s.Bus.AddHandler(s.CreateDashboardSnapshot)
- s.Bus.AddHandler(s.GetDashboardSnapshot)
- s.Bus.AddHandler(s.DeleteDashboardSnapshot)
- s.Bus.AddHandler(s.SearchDashboardSnapshots)
- s.Bus.AddHandler(s.DeleteExpiredSnapshots)
-
return s
}
diff --git a/pkg/services/datasources/service/datasource_service.go b/pkg/services/datasources/service/datasource_service.go
index 52ffe634707..30381852578 100644
--- a/pkg/services/datasources/service/datasource_service.go
+++ b/pkg/services/datasources/service/datasource_service.go
@@ -79,14 +79,6 @@ func ProvideService(
permissionsService: permissionsServices.GetDataSourceService(),
}
- s.Bus.AddHandler(s.GetDataSources)
- s.Bus.AddHandler(s.GetDataSourcesByType)
- s.Bus.AddHandler(s.GetDataSource)
- s.Bus.AddHandler(s.AddDataSource)
- s.Bus.AddHandler(s.DeleteDataSource)
- s.Bus.AddHandler(s.UpdateDataSource)
- s.Bus.AddHandler(s.GetDefaultDataSource)
-
ac.RegisterAttributeScopeResolver(NewNameScopeResolver(store))
ac.RegisterAttributeScopeResolver(NewIDScopeResolver(store))
diff --git a/pkg/services/login/authinfoservice/database/database.go b/pkg/services/login/authinfoservice/database/database.go
index 5fa3ee4a89e..2ac7ad076e1 100644
--- a/pkg/services/login/authinfoservice/database/database.go
+++ b/pkg/services/login/authinfoservice/database/database.go
@@ -28,18 +28,9 @@ func ProvideAuthInfoStore(sqlStore sqlstore.Store, bus bus.Bus, secretsService s
secretsService: secretsService,
logger: log.New("login.authinfo.store"),
}
- store.registerBusHandlers()
return store
}
-func (s *AuthInfoStore) registerBusHandlers() {
- s.bus.AddHandler(s.GetExternalUserInfoByLogin)
- s.bus.AddHandler(s.GetAuthInfo)
- s.bus.AddHandler(s.SetAuthInfo)
- s.bus.AddHandler(s.UpdateAuthInfo)
- s.bus.AddHandler(s.DeleteAuthInfo)
-}
-
func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error {
userQuery := models.GetUserByLoginQuery{LoginOrEmail: query.LoginOrEmail}
err := s.sqlStore.GetUserByLogin(ctx, &userQuery)
diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go
index db5ccf002cb..a8f4f30837b 100644
--- a/pkg/services/login/loginservice/loginservice.go
+++ b/pkg/services/login/loginservice/loginservice.go
@@ -23,7 +23,6 @@ func ProvideService(sqlStore sqlstore.Store, bus bus.Bus, quotaService *quota.Qu
QuotaService: quotaService,
AuthInfoService: authInfoService,
}
- bus.AddHandler(s.UpsertUser)
return s
}
diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go
index 4bace3556f9..924506e5845 100644
--- a/pkg/services/login/loginservice/loginservice_test.go
+++ b/pkg/services/login/loginservice/loginservice_test.go
@@ -80,8 +80,6 @@ func Test_teamSync(t *testing.T) {
Login: "test_user",
}
authInfoMock.ExpectedUser = expectedUser
- bus.ClearBusHandlers()
- t.Cleanup(func() { bus.ClearBusHandlers() })
var actualUser *models.User
var actualExternalUser *models.ExternalUserInfo
diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go
index 221b3223891..7c86ebb8348 100644
--- a/pkg/services/notifications/notifications.go
+++ b/pkg/services/notifications/notifications.go
@@ -45,13 +45,6 @@ func ProvideService(bus bus.Bus, cfg *setting.Cfg, mailer Mailer, store TempUser
store: store,
}
- ns.Bus.AddHandler(ns.SendResetPasswordEmail)
- ns.Bus.AddHandler(ns.ValidateResetPasswordCode)
- ns.Bus.AddHandler(ns.SendEmailCommandHandler)
-
- ns.Bus.AddHandler(ns.SendEmailCommandHandlerSync)
- ns.Bus.AddHandler(ns.SendWebhookSync)
-
ns.Bus.AddEventListener(ns.signUpStartedHandler)
ns.Bus.AddEventListener(ns.signUpCompletedHandler)
diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go
index bd07a4dd291..0244c7df558 100644
--- a/pkg/services/notifications/notifications_test.go
+++ b/pkg/services/notifications/notifications_test.go
@@ -35,7 +35,7 @@ func TestSendEmailSync(t *testing.T) {
bus := bus.New()
t.Run("When sending emails synchronously", func(t *testing.T) {
- _, mailer := createSut(t, bus)
+ ns, mailer := createSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
@@ -44,8 +44,7 @@ func TestSendEmailSync(t *testing.T) {
Template: "welcome_on_signup",
},
}
-
- err := bus.Dispatch(context.Background(), cmd)
+ err := ns.SendEmailCommandHandlerSync(context.Background(), cmd)
require.NoError(t, err)
require.NotEmpty(t, mailer.Sent)
@@ -55,7 +54,7 @@ func TestSendEmailSync(t *testing.T) {
})
t.Run("When using Single Email mode with multiple recipients", func(t *testing.T) {
- _, mailer := createSut(t, bus)
+ ns, mailer := createSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
@@ -65,14 +64,14 @@ func TestSendEmailSync(t *testing.T) {
},
}
- err := bus.Dispatch(context.Background(), cmd)
+ err := ns.SendEmailCommandHandlerSync(context.Background(), cmd)
require.NoError(t, err)
require.Len(t, mailer.Sent, 1)
})
t.Run("When using Multi Email mode with multiple recipients", func(t *testing.T) {
- _, mailer := createSut(t, bus)
+ ns, mailer := createSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
@@ -82,14 +81,14 @@ func TestSendEmailSync(t *testing.T) {
},
}
- err := bus.Dispatch(context.Background(), cmd)
+ err := ns.SendEmailCommandHandlerSync(context.Background(), cmd)
require.NoError(t, err)
require.Len(t, mailer.Sent, 3)
})
t.Run("When attaching files to emails", func(t *testing.T) {
- _, mailer := createSut(t, bus)
+ ns, mailer := createSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
@@ -105,7 +104,7 @@ func TestSendEmailSync(t *testing.T) {
},
}
- err := bus.Dispatch(context.Background(), cmd)
+ err := ns.SendEmailCommandHandlerSync(context.Background(), cmd)
require.NoError(t, err)
require.NotEmpty(t, mailer.Sent)
@@ -119,7 +118,7 @@ func TestSendEmailSync(t *testing.T) {
t.Run("When SMTP disabled in configuration", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Enabled = false
- _, mailer, err := createSutWithConfig(t, bus, cfg)
+ ns, mailer, err := createSutWithConfig(t, bus, cfg)
require.NoError(t, err)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
@@ -130,7 +129,7 @@ func TestSendEmailSync(t *testing.T) {
},
}
- err = bus.Dispatch(context.Background(), cmd)
+ err = ns.SendEmailCommandHandlerSync(context.Background(), cmd)
require.ErrorIs(t, err, models.ErrSmtpNotEnabled)
require.Empty(t, mailer.Sent)
@@ -139,7 +138,7 @@ func TestSendEmailSync(t *testing.T) {
t.Run("When invalid content type in configuration", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data")
- _, mailer, err := createSutWithConfig(t, bus, cfg)
+ ns, mailer, err := createSutWithConfig(t, bus, cfg)
require.NoError(t, err)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
@@ -150,14 +149,14 @@ func TestSendEmailSync(t *testing.T) {
},
}
- err = bus.Dispatch(context.Background(), cmd)
+ err = ns.SendEmailCommandHandlerSync(context.Background(), cmd)
require.Error(t, err)
require.Empty(t, mailer.Sent)
})
t.Run("When SMTP dialer is disconnected", func(t *testing.T) {
- _ = createDisconnectedSut(t, bus)
+ ns := createDisconnectedSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
@@ -167,7 +166,7 @@ func TestSendEmailSync(t *testing.T) {
},
}
- err := bus.Dispatch(context.Background(), cmd)
+ err := ns.SendEmailCommandHandlerSync(context.Background(), cmd)
require.Error(t, err)
})
@@ -210,7 +209,7 @@ func TestSendEmailAsync(t *testing.T) {
t.Run("When invalid content type in configuration", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data")
- _, mailer, err := createSutWithConfig(t, bus, cfg)
+ ns, mailer, err := createSutWithConfig(t, bus, cfg)
require.NoError(t, err)
cmd := &models.SendEmailCommand{
Subject: "subject",
@@ -219,14 +218,14 @@ func TestSendEmailAsync(t *testing.T) {
Template: "welcome_on_signup",
}
- err = bus.Dispatch(context.Background(), cmd)
+ err = ns.SendEmailCommandHandler(context.Background(), cmd)
require.Error(t, err)
require.Empty(t, mailer.Sent)
})
t.Run("When SMTP dialer is disconnected", func(t *testing.T) {
- _ = createDisconnectedSut(t, bus)
+ ns := createDisconnectedSut(t, bus)
cmd := &models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
@@ -234,7 +233,7 @@ func TestSendEmailAsync(t *testing.T) {
Template: "welcome_on_signup",
}
- err := bus.Dispatch(context.Background(), cmd)
+ err := ns.SendEmailCommandHandler(context.Background(), cmd)
// The async version should not surface connection errors via Bus. It should only log them.
require.NoError(t, err)
diff --git a/pkg/services/plugindashboards/service/dashboard_updater_test.go b/pkg/services/plugindashboards/service/dashboard_updater_test.go
index 5648bb613be..05a0e33630e 100644
--- a/pkg/services/plugindashboards/service/dashboard_updater_test.go
+++ b/pkg/services/plugindashboards/service/dashboard_updater_test.go
@@ -569,8 +569,6 @@ func scenario(t *testing.T, desc string, input scenarioInput, f func(ctx *scenar
sCtx.dashboardService,
)
- t.Cleanup(bus.ClearBusHandlers)
-
t.Run(desc, func(t *testing.T) {
f(sCtx)
})
diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go
index d8b3eb2e95a..1656f6ee123 100644
--- a/pkg/services/search/service.go
+++ b/pkg/services/search/service.go
@@ -21,7 +21,6 @@ func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlstore *sqlstore.SQLStore)
},
sqlstore: sqlstore,
}
- s.Bus.AddHandler(s.SearchHandler)
return s
}
diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go
index 7e79cb6d24c..d4b07cb64db 100644
--- a/pkg/services/sqlstore/alert.go
+++ b/pkg/services/sqlstore/alert.go
@@ -7,23 +7,12 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
// timeNow makes it possible to test usage of time
var timeNow = time.Now
-func (ss *SQLStore) addAlertQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.HandleAlertsQuery)
- bus.AddHandler("sql", ss.GetAlertById)
- bus.AddHandler("sql", ss.GetAllAlertQueryHandler)
- bus.AddHandler("sql", ss.SetAlertState)
- bus.AddHandler("sql", ss.GetAlertStatesForDashboard)
- bus.AddHandler("sql", ss.PauseAlert)
- bus.AddHandler("sql", ss.PauseAllAlerts)
-}
-
func (ss *SQLStore) GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
alert := models.Alert{}
diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go
index 0d6bd261415..c8c515e81ee 100644
--- a/pkg/services/sqlstore/alert_notification.go
+++ b/pkg/services/sqlstore/alert_notification.go
@@ -8,7 +8,6 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
@@ -79,10 +78,6 @@ func (ss *SQLStore) GetAlertNotifications(ctx context.Context, query *models.Get
return getAlertNotificationInternal(ctx, query, newSession(ctx))
}
-func (ss *SQLStore) addAlertNotificationUidByIdHandler() {
- bus.AddHandler("sql", ss.GetAlertNotificationUidWithId)
-}
-
func (ss *SQLStore) GetAlertNotificationUidWithId(ctx context.Context, query *models.GetAlertNotificationUidQuery) error {
cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id)
diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go
index 6fae4142bab..6d2b7a3d9b7 100644
--- a/pkg/services/sqlstore/alert_notification_test.go
+++ b/pkg/services/sqlstore/alert_notification_test.go
@@ -10,7 +10,6 @@ import (
"testing"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
@@ -19,13 +18,7 @@ import (
func TestAlertNotificationSQLAccess(t *testing.T) {
var sqlStore *SQLStore
- setup := func() {
- sqlStore = InitTestDB(t)
- // Set up bus handlers
- bus.AddHandler("deleteAlertNotification", func(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error {
- return sqlStore.DeleteAlertNotification(ctx, cmd)
- })
- }
+ setup := func() { sqlStore = InitTestDB(t) }
t.Run("Alert notification state", func(t *testing.T) {
setup()
diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go
index eb31a56efaf..876d41382d2 100644
--- a/pkg/services/sqlstore/apikey.go
+++ b/pkg/services/sqlstore/apikey.go
@@ -6,18 +6,9 @@ import (
"xorm.io/xorm"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
-func (ss *SQLStore) addAPIKeysQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.GetAPIKeys)
- bus.AddHandler("sql", ss.GetApiKeyById)
- bus.AddHandler("sql", ss.GetApiKeyByName)
- bus.AddHandler("sql", ss.DeleteApiKey)
- bus.AddHandler("sql", ss.AddAPIKey)
-}
-
// GetAPIKeys queries the database based
// on input on GetApiKeysQuery
func (ss *SQLStore) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuery) error {
diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go
index c53c577a886..4017e8d8872 100644
--- a/pkg/services/sqlstore/dashboard.go
+++ b/pkg/services/sqlstore/dashboard.go
@@ -6,7 +6,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
@@ -26,18 +25,6 @@ func init() {
prometheus.MustRegister(shadowSearchCounter)
}
-func (ss *SQLStore) addDashboardQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.GetDashboard)
- bus.AddHandler("sql", ss.GetDashboardUIDById)
- bus.AddHandler("sql", ss.GetDashboardTags)
- bus.AddHandler("sql", ss.SearchDashboards)
- bus.AddHandler("sql", ss.GetDashboards)
- bus.AddHandler("sql", ss.HasEditPermissionInFolders)
- bus.AddHandler("sql", ss.GetDashboardPermissionsForUser)
- bus.AddHandler("sql", ss.GetDashboardSlugById)
- bus.AddHandler("sql", ss.HasAdminPermissionInFolders)
-}
-
var generateNewUid func() string = util.GenerateShortUID
func (ss *SQLStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error {
diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go
index bcc04b2a17a..df4a4fcf129 100644
--- a/pkg/services/sqlstore/dashboard_acl.go
+++ b/pkg/services/sqlstore/dashboard_acl.go
@@ -3,14 +3,9 @@ package sqlstore
import (
"context"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
-func (ss *SQLStore) addDashboardACLQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.GetDashboardAclInfoList)
-}
-
// GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three
// different places.
// 1) Permissions for the dashboard
diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go
index 3c6c6832ef8..e8bd87b6461 100644
--- a/pkg/services/sqlstore/dashboard_version.go
+++ b/pkg/services/sqlstore/dashboard_version.go
@@ -4,17 +4,10 @@ import (
"context"
"strings"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
-func (ss *SQLStore) addDashboardVersionQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.GetDashboardVersion)
- bus.AddHandler("sql", ss.GetDashboardVersions)
- bus.AddHandler("sql", ss.DeleteExpiredVersions)
-}
-
// GetDashboardVersion gets the dashboard version for the given dashboard ID and version number.
func (ss *SQLStore) GetDashboardVersion(ctx context.Context, query *models.GetDashboardVersionQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
diff --git a/pkg/services/sqlstore/login_attempt.go b/pkg/services/sqlstore/login_attempt.go
index fbbf85d786b..dd93a249552 100644
--- a/pkg/services/sqlstore/login_attempt.go
+++ b/pkg/services/sqlstore/login_attempt.go
@@ -5,18 +5,11 @@ import (
"strconv"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
var getTimeNow = time.Now
-func (ss *SQLStore) addLoginAttemptQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.CreateLoginAttempt)
- bus.AddHandler("sql", ss.DeleteOldLoginAttempts)
- bus.AddHandler("sql", ss.GetUserLoginAttemptCount)
-}
-
func (ss *SQLStore) CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
loginAttempt := models.LoginAttempt{
diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go
index 6cac1cea15f..1cdd72a938a 100644
--- a/pkg/services/sqlstore/org.go
+++ b/pkg/services/sqlstore/org.go
@@ -5,7 +5,6 @@ import (
"fmt"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
@@ -15,16 +14,6 @@ import (
// MainOrgName is the name of the main organization.
const MainOrgName = "Main Org."
-func (ss *SQLStore) addOrgQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.GetOrgById)
- bus.AddHandler("sql", CreateOrg)
- bus.AddHandler("sql", ss.UpdateOrg)
- bus.AddHandler("sql", ss.UpdateOrgAddress)
- bus.AddHandler("sql", ss.GetOrgByNameHandler)
- bus.AddHandler("sql", ss.SearchOrgs)
- bus.AddHandler("sql", ss.DeleteOrg)
-}
-
func (ss *SQLStore) SearchOrgs(ctx context.Context, query *models.SearchOrgsQuery) error {
return ss.WithDbSession(ctx, func(dbSession *DBSession) error {
query.Result = make([]*models.OrgDTO, 0)
diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go
index 24f1a9e8119..60d7e5ca8f7 100644
--- a/pkg/services/sqlstore/org_users.go
+++ b/pkg/services/sqlstore/org_users.go
@@ -6,20 +6,12 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
)
-func (ss *SQLStore) addOrgUsersQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.AddOrgUser)
- bus.AddHandler("sql", ss.RemoveOrgUser)
- bus.AddHandler("sql", ss.GetOrgUsers)
- bus.AddHandler("sql", ss.UpdateOrgUser)
-}
-
func (ss *SQLStore) AddOrgUser(ctx context.Context, cmd *models.AddOrgUserCommand) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
// check if user exists
diff --git a/pkg/services/sqlstore/playlist.go b/pkg/services/sqlstore/playlist.go
index b0d8e96652d..49f3aa311c8 100644
--- a/pkg/services/sqlstore/playlist.go
+++ b/pkg/services/sqlstore/playlist.go
@@ -3,19 +3,9 @@ package sqlstore
import (
"context"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
-func (ss *SQLStore) addPlaylistQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.CreatePlaylist)
- bus.AddHandler("sql", ss.UpdatePlaylist)
- bus.AddHandler("sql", ss.DeletePlaylist)
- bus.AddHandler("sql", ss.SearchPlaylists)
- bus.AddHandler("sql", ss.GetPlaylist)
- bus.AddHandler("sql", ss.GetPlaylistItem)
-}
-
func (ss *SQLStore) CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
playlist := models.Playlist{
diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go
index 1fccba8adc3..da3a5388ef1 100644
--- a/pkg/services/sqlstore/preferences.go
+++ b/pkg/services/sqlstore/preferences.go
@@ -5,17 +5,9 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
-func (ss *SQLStore) addPreferencesQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.GetPreferences)
- bus.AddHandler("sql", ss.GetPreferencesWithDefaults)
- bus.AddHandler("sql", ss.SavePreferences)
- bus.AddHandler("sql", ss.PatchPreferences)
-}
-
func (ss *SQLStore) GetPreferencesWithDefaults(ctx context.Context, query *models.GetPreferencesWithDefaultsQuery) error {
return ss.WithDbSession(ctx, func(dbSession *DBSession) error {
params := make([]interface{}, 0)
diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go
index abc0c39b98c..3ed4428fa71 100644
--- a/pkg/services/sqlstore/quota.go
+++ b/pkg/services/sqlstore/quota.go
@@ -5,7 +5,6 @@ import (
"fmt"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
@@ -15,16 +14,6 @@ const (
dashboardTarget = "dashboard"
)
-func (ss *SQLStore) addQuotaQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.GetOrgQuotaByTarget)
- bus.AddHandler("sql", ss.GetOrgQuotas)
- bus.AddHandler("sql", ss.UpdateOrgQuota)
- bus.AddHandler("sql", ss.GetUserQuotaByTarget)
- bus.AddHandler("sql", ss.GetUserQuotas)
- bus.AddHandler("sql", ss.UpdateUserQuota)
- bus.AddHandler("sql", ss.GetGlobalQuotaByTarget)
-}
-
type targetCount struct {
Count int64
}
diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go
index 792ce846329..ca6ec8d6135 100644
--- a/pkg/services/sqlstore/sqlstore.go
+++ b/pkg/services/sqlstore/sqlstore.go
@@ -111,28 +111,6 @@ func newSQLStore(cfg *setting.Cfg, cacheService *localcache.CacheService, b bus.
// Init repo instances
annotations.SetRepository(&SQLAnnotationRepo{sql: ss})
annotations.SetAnnotationCleaner(&AnnotationCleanupService{batchSize: ss.Cfg.AnnotationCleanupJobBatchSize, log: log.New("annotationcleaner")})
- ss.Bus.SetTransactionManager(ss)
-
- // Register handlers
- ss.addStatsQueryAndCommandHandlers()
- ss.addUserQueryAndCommandHandlers()
- ss.addAlertNotificationUidByIdHandler()
- ss.addPreferencesQueryAndCommandHandlers()
- ss.addDashboardQueryAndCommandHandlers()
- ss.addDashboardACLQueryAndCommandHandlers()
- ss.addQuotaQueryAndCommandHandlers()
- ss.addOrgUsersQueryAndCommandHandlers()
- ss.addStarQueryAndCommandHandlers()
- ss.addAlertQueryAndCommandHandlers()
- ss.addTempUserQueryAndCommandHandlers()
- ss.addDashboardVersionQueryAndCommandHandlers()
- ss.addAPIKeysQueryAndCommandHandlers()
- ss.addPlaylistQueryAndCommandHandlers()
- ss.addLoginAttemptQueryAndCommandHandlers()
- ss.addTeamQueryAndCommandHandlers()
- ss.addOrgQueryAndCommandHandlers()
-
- bus.AddHandler("sql", ss.GetDBHealthQuery)
// if err := ss.Reset(); err != nil {
// return nil, err
diff --git a/pkg/services/sqlstore/star.go b/pkg/services/sqlstore/star.go
index fef91f9e9b4..847fdfec6e0 100644
--- a/pkg/services/sqlstore/star.go
+++ b/pkg/services/sqlstore/star.go
@@ -3,17 +3,9 @@ package sqlstore
import (
"context"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
-func (ss *SQLStore) addStarQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.StarDashboard)
- bus.AddHandler("sql", ss.UnstarDashboard)
- bus.AddHandler("sql", ss.GetUserStars)
- bus.AddHandler("sql", ss.IsStarredByUserCtx)
-}
-
func (ss *SQLStore) IsStarredByUserCtx(ctx context.Context, query *models.IsStarredByUserQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
rawSQL := "SELECT 1 from star where user_id=? and dashboard_id=?"
diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go
index 43777190fe3..e1e1500994e 100644
--- a/pkg/services/sqlstore/stats.go
+++ b/pkg/services/sqlstore/stats.go
@@ -5,20 +5,10 @@ import (
"strconv"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
-func (ss *SQLStore) addStatsQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.GetAdminStats)
- bus.AddHandler("sql", ss.GetSystemUserCountStats)
- bus.AddHandler("sql", ss.GetAlertNotifiersUsageStats)
- bus.AddHandler("sql", ss.GetDataSourceAccessStats)
- bus.AddHandler("sql", ss.GetDataSourceStats)
- bus.AddHandler("sql", ss.GetSystemStats)
-}
-
const activeUserTimeLimit = time.Hour * 24 * 30
const dailyActiveUserTimeLimit = time.Hour * 24
diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go
index abedc2110b3..6e266fed697 100644
--- a/pkg/services/sqlstore/team.go
+++ b/pkg/services/sqlstore/team.go
@@ -7,25 +7,11 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
-func (ss *SQLStore) addTeamQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.UpdateTeam)
- bus.AddHandler("sql", ss.DeleteTeam)
- bus.AddHandler("sql", ss.SearchTeams)
- bus.AddHandler("sql", ss.GetTeamById)
- bus.AddHandler("sql", ss.GetTeamsByUser)
-
- bus.AddHandler("sql", ss.UpdateTeamMember)
- bus.AddHandler("sql", ss.RemoveTeamMember)
- bus.AddHandler("sql", ss.GetTeamMembers)
- bus.AddHandler("sql", IsAdminOfTeams)
-}
-
type TeamStore interface {
UpdateTeam(ctx context.Context, cmd *models.UpdateTeamCommand) error
DeleteTeam(ctx context.Context, cmd *models.DeleteTeamCommand) error
diff --git a/pkg/services/sqlstore/temp_user.go b/pkg/services/sqlstore/temp_user.go
index 97f32f77a41..80115ade956 100644
--- a/pkg/services/sqlstore/temp_user.go
+++ b/pkg/services/sqlstore/temp_user.go
@@ -4,19 +4,9 @@ import (
"context"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
-func (ss *SQLStore) addTempUserQueryAndCommandHandlers() {
- bus.AddHandler("sql", ss.CreateTempUser)
- bus.AddHandler("sql", ss.GetTempUsersQuery)
- bus.AddHandler("sql", ss.UpdateTempUserStatus)
- bus.AddHandler("sql", ss.GetTempUserByCode)
- bus.AddHandler("sql", ss.UpdateTempUserWithEmailSent)
- bus.AddHandler("sql", ss.ExpireOldUserInvites)
-}
-
func (ss *SQLStore) UpdateTempUserStatus(ctx context.Context, cmd *models.UpdateTempUserStatusCommand) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
var rawSQL = "UPDATE temp_user SET status=? WHERE code=?"
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index c9ce4d9b098..b159dcd6bf8 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -8,7 +8,6 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -16,25 +15,6 @@ import (
"github.com/grafana/grafana/pkg/util"
)
-func (ss *SQLStore) addUserQueryAndCommandHandlers() {
- ss.Bus.AddHandler(ss.GetSignedInUserWithCacheCtx)
-
- bus.AddHandler("sql", ss.GetUserById)
- bus.AddHandler("sql", ss.UpdateUser)
- bus.AddHandler("sql", ss.ChangeUserPassword)
- bus.AddHandler("sql", ss.GetUserByLogin)
- bus.AddHandler("sql", ss.GetUserByEmail)
- bus.AddHandler("sql", ss.SetUsingOrg)
- bus.AddHandler("sql", ss.UpdateUserLastSeenAt)
- bus.AddHandler("sql", ss.GetUserProfile)
- bus.AddHandler("sql", ss.SearchUsers)
- bus.AddHandler("sql", ss.GetUserOrgList)
- bus.AddHandler("sql", ss.DisableUser)
- bus.AddHandler("sql", ss.BatchDisableUsers)
- bus.AddHandler("sql", ss.DeleteUser)
- bus.AddHandler("sql", ss.SetUserHelpFlag)
-}
-
func getOrgIdForNewUser(sess *DBSession, cmd models.CreateUserCommand) (int64, error) {
if cmd.SkipOrgSetup {
return -1, nil
From 7e844064a4c216d7468fbbdf122c05a7d97ffbc0 Mon Sep 17 00:00:00 2001
From: Gilles De Mey
Date: Fri, 8 Apr 2022 17:15:19 +0200
Subject: [PATCH 52/55] Alerting: fix collapsable toggle text regression
(#47517)
---
.../components/CollapseToggle.test.tsx | 29 +++++++++++++++++++
.../unified/components/CollapseToggle.tsx | 4 ++-
2 files changed, 32 insertions(+), 1 deletion(-)
create mode 100644 public/app/features/alerting/unified/components/CollapseToggle.test.tsx
diff --git a/public/app/features/alerting/unified/components/CollapseToggle.test.tsx b/public/app/features/alerting/unified/components/CollapseToggle.test.tsx
new file mode 100644
index 00000000000..384da5879fd
--- /dev/null
+++ b/public/app/features/alerting/unified/components/CollapseToggle.test.tsx
@@ -0,0 +1,29 @@
+import { render, screen } from '@testing-library/react';
+import { noop } from 'lodash';
+import React from 'react';
+import { CollapseToggle } from './CollapseToggle';
+
+describe('TestToggle', () => {
+ it('should render text', () => {
+ render( );
+ expect(screen.getByRole('button')).toHaveTextContent('Hello, world');
+ });
+
+ it('should respect isCollapsed', () => {
+ const { rerender } = render( );
+ expect(screen.getByRole('button', { expanded: true })).toBeInTheDocument();
+
+ rerender( );
+ expect(screen.getByRole('button', { expanded: false })).toBeInTheDocument();
+ });
+
+ it('should call onToggle', () => {
+ const onToggle = jest.fn();
+ render( );
+ screen.getByRole('button').click();
+
+ expect(onToggle).toHaveBeenCalledWith(false);
+ // it should also not have any impact on the actual expanded state since the component does not track its own state
+ expect(screen.getByRole('button', { expanded: false })).toBeInTheDocument();
+ });
+});
diff --git a/public/app/features/alerting/unified/components/CollapseToggle.tsx b/public/app/features/alerting/unified/components/CollapseToggle.tsx
index c63256e077f..3e670a396be 100644
--- a/public/app/features/alerting/unified/components/CollapseToggle.tsx
+++ b/public/app/features/alerting/unified/components/CollapseToggle.tsx
@@ -34,7 +34,9 @@ export const CollapseToggle: FC = ({
icon={isCollapsed ? 'angle-right' : 'angle-down'}
onClick={() => onToggle(!isCollapsed)}
{...restOfProps}
- />
+ >
+ {text}
+
);
};
From 883ce5ad307a991492c51e2e470c7490b4f68a0f Mon Sep 17 00:00:00 2001
From: Eric Leijonmarck
Date: Fri, 8 Apr 2022 16:23:03 +0100
Subject: [PATCH 53/55] Service accounts: UI polish improvements (#47461)
* ui polishes
* change icon
---
pkg/api/index.go | 2 +-
...al.tsx => CreateServiceAccountTokenModal.tsx} | 5 ++++-
.../serviceaccounts/ServiceAccountPage.tsx | 2 +-
.../serviceaccounts/ServiceAccountProfile.tsx | 16 ++++++++++++----
.../serviceaccounts/ServiceAccountsListPage.tsx | 3 ++-
5 files changed, 20 insertions(+), 8 deletions(-)
rename public/app/features/serviceaccounts/{CreateTokenModal.tsx => CreateServiceAccountTokenModal.tsx} (96%)
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 20c21a3c7c6..d4e0bc8ce3d 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -307,7 +307,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
Id: "serviceaccounts",
Description: "Manage service accounts",
// TODO: change icon to "key-skeleton-alt" when it's available
- Icon: "key-skeleton-alt",
+ Icon: "keyhole-circle",
Url: hs.Cfg.AppSubURL + "/org/serviceaccounts",
})
}
diff --git a/public/app/features/serviceaccounts/CreateTokenModal.tsx b/public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx
similarity index 96%
rename from public/app/features/serviceaccounts/CreateTokenModal.tsx
rename to public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx
index 4cf8338a0d1..996ee89962e 100644
--- a/public/app/features/serviceaccounts/CreateTokenModal.tsx
+++ b/public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx
@@ -64,8 +64,11 @@ export const CreateTokenModal = ({ isOpen, token, onCreateToken, onClose }: Crea
{
diff --git a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx
index 2c94e38b171..408d4346d25 100644
--- a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx
+++ b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx
@@ -66,10 +66,18 @@ export function ServiceAccountProfile({
return (
<>
- Information
-
-
-
+
+
+
+
+
+ {serviceAccount.name}
+
+
+ Information
diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx
index 3d597ed8ed2..2970193f335 100644
--- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx
+++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx
@@ -78,6 +78,7 @@ const ServiceAccountsListPage = ({
return (
+ Service accounts
- Display name
+ Account
ID
Roles
Status
From e3b123c3fc2fe779f7728b6f750a57bbaf52c219 Mon Sep 17 00:00:00 2001
From: Josh Hunt
Date: Fri, 8 Apr 2022 16:37:25 +0100
Subject: [PATCH 54/55] GrafanaUI: Change chevron directions for open/closed
state (#47386)
---
.../grafana-ui/src/components/Collapse/CollapsableSection.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx b/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx
index 24fe7a67f77..c58c7b8c3a1 100644
--- a/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx
+++ b/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx
@@ -60,7 +60,7 @@ export const CollapsableSection: FC = ({
{loading ? (
) : (
-
+
)}
From ebe5f3646f286d10776d6f0ce80a4e0eaf78220d Mon Sep 17 00:00:00 2001
From: Kevin Yu
Date: Fri, 8 Apr 2022 08:49:46 -0700
Subject: [PATCH 55/55] AzureMonitor: Use Resource Picker in Metrics Query
Editor (#47164)
* wip: new metrics query editor
* prepend subscriptions to url path
* remove duplicated setQueryValue file
* add tests for new metrics query editor
* wip start extracting resource field into a shared component
* fix query editor test
* fix up backend tests
* move azure monitor specific getters to azure_monitor_datasource
* use existing useAsyncState hook
* extract useAsyncState into separate file
* add datahooks tests
* extract resource field component
* cleanup
* clarify variable names
* remove logs query specific resource field component
* add url_builder test
* add azure_monitor_datasources tests
* address comments
* add types
* pass resourceUri to resource field component
* add test to check we reset the query fields when changing resources
---
.../metrics/azuremonitor-datasource.go | 1 -
.../metrics/azuremonitor-datasource_test.go | 4 +-
pkg/tsdb/azuremonitor/metrics/url-builder.go | 55 +-
.../azuremonitor/metrics/url-builder_test.go | 20 +-
pkg/tsdb/azuremonitor/types/types.go | 7 +-
.../__mocks__/datasource.ts | 9 +
.../__mocks__/query.ts | 2 +
.../__mocks__/resourcePickerData.ts | 1 +
.../__mocks__/resourcePickerRows.ts | 8 +-
.../azure_monitor_datasource.test.ts | 903 +++++++++++-------
.../azure_monitor/azure_monitor_datasource.ts | 82 +-
.../azure_monitor/url_builder.test.ts | 319 ++++---
.../azure_monitor/url_builder.ts | 20 +-
.../LogsQueryEditor/LogsQueryEditor.tsx | 12 +-
.../MetricsQueryEditor/dataHooks.test.ts | 64 --
.../MetricsQueryEditor/dataHooks.ts | 26 +-
.../MetricsQueryEditor/setQueryValue.ts | 15 +
.../MetricsQueryEditor.test.tsx | 226 +++++
.../MetricsQueryEditor.tsx | 110 ++-
.../NewMetricsQueryEditor/dataHooks.test.ts | 165 ++++
.../NewMetricsQueryEditor/dataHooks.ts | 144 +++
.../components/QueryEditor/QueryEditor.tsx | 10 +-
.../ResourceField.tsx | 32 +-
.../components/ResourceField/index.tsx | 1 +
.../types/query.ts | 1 +
.../types/types.ts | 17 +
.../utils/useAsyncState.test.ts | 65 ++
.../utils/useAsyncState.ts | 24 +
28 files changed, 1668 insertions(+), 675 deletions(-)
create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.test.tsx
create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.test.ts
create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.ts
rename public/app/plugins/datasource/grafana-azure-monitor-datasource/components/{LogsQueryEditor => ResourceField}/ResourceField.tsx (83%)
create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/index.tsx
create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useAsyncState.test.ts
create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useAsyncState.ts
diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go
index 3def772d76b..0fcd93f7fca 100644
--- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go
+++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go
@@ -217,7 +217,6 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo types
azlog.Debug("Failed to create request", "error", err)
return nil, errutil.Wrap("Failed to create request", err)
}
- req.URL.Path = "/subscriptions"
req.Header.Set("Content-Type", "application/json")
return req, nil
diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go
index 41453545713..89faac286c0 100644
--- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go
+++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go
@@ -163,7 +163,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
}
azureMonitorQuery := &types.AzureMonitorQuery{
- URL: "12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics",
+ URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics",
UrlComponents: map[string]string{
"metricDefinition": "Microsoft.Compute/virtualMachines",
"resourceGroup": "grafanastaging",
@@ -587,7 +587,7 @@ func TestAzureMonitorCreateRequest(t *testing.T) {
}{
{
name: "creates a request",
- expectedURL: "http://ds/subscriptions",
+ expectedURL: "http://ds/",
expectedHeaders: http.Header{
"Content-Type": []string{"application/json"},
},
diff --git a/pkg/tsdb/azuremonitor/metrics/url-builder.go b/pkg/tsdb/azuremonitor/metrics/url-builder.go
index 4745f3bcdf0..8c3fad8fd37 100644
--- a/pkg/tsdb/azuremonitor/metrics/url-builder.go
+++ b/pkg/tsdb/azuremonitor/metrics/url-builder.go
@@ -18,6 +18,35 @@ type urlBuilder struct {
ResourceName string
}
+func (params *urlBuilder) buildMetricsURLFromLegacyQuery() string {
+ subscription := params.Subscription
+
+ if params.Subscription == "" {
+ subscription = params.DefaultSubscription
+ }
+
+ metricDefinitionArray := strings.Split(params.MetricDefinition, "/")
+ resourceNameArray := strings.Split(params.ResourceName, "/")
+ provider := metricDefinitionArray[0]
+ metricDefinitionArray = metricDefinitionArray[1:]
+
+ urlArray := []string{
+ "/subscriptions",
+ subscription,
+ "resourceGroups",
+ params.ResourceGroup,
+ "providers",
+ provider,
+ }
+
+ for i, metricDefinition := range metricDefinitionArray {
+ urlArray = append(urlArray, metricDefinition, resourceNameArray[i])
+ }
+
+ resourceURI := strings.Join(urlArray, "/")
+ return resourceURI
+}
+
// BuildMetricsURL checks the metric definition property to see which form of the url
// should be returned
func (params *urlBuilder) BuildMetricsURL() string {
@@ -25,31 +54,7 @@ func (params *urlBuilder) BuildMetricsURL() string {
// Prior to Grafana 9, we had a legacy query object rather than a resourceURI, so we manually create the resource URI
if resourceURI == "" {
- subscription := params.Subscription
-
- if params.Subscription == "" {
- subscription = params.DefaultSubscription
- }
-
- metricDefinitionArray := strings.Split(params.MetricDefinition, "/")
- resourceNameArray := strings.Split(params.ResourceName, "/")
- provider := metricDefinitionArray[0]
- metricDefinitionArray = metricDefinitionArray[1:]
-
- urlArray := []string{
- subscription,
- "resourceGroups",
- params.ResourceGroup,
- "providers",
- provider,
- }
-
- for i := range metricDefinitionArray {
- urlArray = append(urlArray, metricDefinitionArray[i])
- urlArray = append(urlArray, resourceNameArray[i])
- }
-
- resourceURI = strings.Join(urlArray[:], "/")
+ resourceURI = params.buildMetricsURLFromLegacyQuery()
}
return fmt.Sprintf("%s/providers/microsoft.insights/metrics", resourceURI)
diff --git a/pkg/tsdb/azuremonitor/metrics/url-builder_test.go b/pkg/tsdb/azuremonitor/metrics/url-builder_test.go
index 458cf7b491c..1abde71f094 100644
--- a/pkg/tsdb/azuremonitor/metrics/url-builder_test.go
+++ b/pkg/tsdb/azuremonitor/metrics/url-builder_test.go
@@ -3,23 +3,23 @@ package metrics
import (
"testing"
- "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/assert"
)
func TestURLBuilder(t *testing.T) {
t.Run("AzureMonitor URL Builder", func(t *testing.T) {
t.Run("when only resource uri is provided it returns resource/uri/providers/microsoft.insights/metrics", func(t *testing.T) {
ub := &urlBuilder{
- ResourceURI: "resource/uri",
+ ResourceURI: "/subscriptions/sub/resource/uri",
}
url := ub.BuildMetricsURL()
- require.Equal(t, url, "resource/uri/providers/microsoft.insights/metrics")
+ assert.Equal(t, "/subscriptions/sub/resource/uri/providers/microsoft.insights/metrics", url)
})
t.Run("when resource uri and legacy fields are provided the legacy fields are ignored", func(t *testing.T) {
ub := &urlBuilder{
- ResourceURI: "resource/uri",
+ ResourceURI: "/subscriptions/sub/resource/uri",
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricDefinition: "Microsoft.NetApp/netAppAccounts/capacityPools/volumes",
@@ -27,7 +27,7 @@ func TestURLBuilder(t *testing.T) {
}
url := ub.BuildMetricsURL()
- require.Equal(t, url, "resource/uri/providers/microsoft.insights/metrics")
+ assert.Equal(t, "/subscriptions/sub/resource/uri/providers/microsoft.insights/metrics", url)
})
t.Run("Legacy URL Builder params", func(t *testing.T) {
@@ -40,7 +40,7 @@ func TestURLBuilder(t *testing.T) {
}
url := ub.BuildMetricsURL()
- require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
+ assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics", url)
})
t.Run("when metric definition is in the short form and a subscription is defined", func(t *testing.T) {
@@ -53,7 +53,7 @@ func TestURLBuilder(t *testing.T) {
}
url := ub.BuildMetricsURL()
- require.Equal(t, url, "specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
+ assert.Equal(t, "/subscriptions/specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics", url)
})
t.Run("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func(t *testing.T) {
@@ -65,7 +65,7 @@ func TestURLBuilder(t *testing.T) {
}
url := ub.BuildMetricsURL()
- require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
+ assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics", url)
})
t.Run("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func(t *testing.T) {
@@ -77,7 +77,7 @@ func TestURLBuilder(t *testing.T) {
}
url := ub.BuildMetricsURL()
- require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
+ assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics", url)
})
t.Run("when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes", func(t *testing.T) {
@@ -89,7 +89,7 @@ func TestURLBuilder(t *testing.T) {
}
url := ub.BuildMetricsURL()
- require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/providers/microsoft.insights/metrics")
+ assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/providers/microsoft.insights/metrics", url)
})
})
})
diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go
index f0d40823c7b..1a9b4d60271 100644
--- a/pkg/tsdb/azuremonitor/types/types.go
+++ b/pkg/tsdb/azuremonitor/types/types.go
@@ -121,9 +121,12 @@ type AzureMonitorJSONQuery struct {
// Legecy "resource" fields from before the resource picker provided just a single ResourceURI
// These are used for pre-resource picker queries to reconstruct a resource URI
+ // Deprecated
MetricDefinition string `json:"metricDefinition"`
- ResourceGroup string `json:"resourceGroup"`
- ResourceName string `json:"resourceName"`
+ // Deprecated
+ ResourceGroup string `json:"resourceGroup"`
+ // Deprecated
+ ResourceName string `json:"resourceName"`
AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"`
Dimension string `json:"dimension"` // old model
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts
index 23492d50d3f..1793aa8a397 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts
@@ -16,6 +16,14 @@ export default function createMockDatasource(overrides?: DeepPartial
},
getSubscriptions: jest.fn().mockResolvedValueOnce([]),
defaultSubscriptionId: 'subscriptionId',
+ newGetMetricNamespaces: jest.fn().mockResolvedValueOnce([]),
+ newGetMetricNames: jest.fn().mockResolvedValueOnce([]),
+ newGetMetricMetadata: jest.fn().mockResolvedValueOnce({
+ primaryAggType: 'Average',
+ supportedAggTypes: ['Average', 'Maximum', 'Minimum'],
+ supportedTimeGrains: [],
+ dimensions: [],
+ }),
},
getAzureLogAnalyticsWorkspaces: jest.fn().mockResolvedValueOnce([]),
@@ -41,6 +49,7 @@ export default function createMockDatasource(overrides?: DeepPartial
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]),
getResourcesForResourceGroup: jest.fn().mockResolvedValue([]),
getResourceURIFromWorkspace: jest.fn().mockReturnValue(''),
+ getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
},
...overrides,
};
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts
index 883c4452e58..8ed51200cea 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts
@@ -17,6 +17,8 @@ export default function createMockQuery(): AzureMonitorQuery {
azureMonitor: {
// aggOptions: [],
+ resourceUri:
+ '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
aggregation: 'Average',
allowedTimeGrainsMs: [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
// dimensionFilter: '*',
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/resourcePickerData.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/resourcePickerData.ts
index 9d963262bd7..e4659041b64 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/resourcePickerData.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/resourcePickerData.ts
@@ -10,6 +10,7 @@ export default function createMockResourcePickerData(overrides?: DeepPartial [
{
id: 'dev-1',
uri: '/subscriptions/def-456/resourceGroups/dev-1',
- name: 'Development',
+ name: 'Development 1',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
@@ -39,7 +39,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
{
id: 'dev-2',
uri: '/subscriptions/def-456/resourceGroups/dev-2',
- name: 'Development',
+ name: 'Development 2',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
@@ -55,7 +55,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
{
id: 'dev-4',
uri: '/subscriptions/def-456/resourceGroups/dev-4',
- name: 'Development',
+ name: 'Development 3',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
@@ -63,7 +63,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
{
id: 'dev-5',
uri: '/subscriptions/def-456/resourceGroups/dev-5',
- name: 'Development',
+ name: 'Development 4',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts
index 8236bb81437..a6cbb496067 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts
@@ -75,266 +75,59 @@ describe('AzureMonitorDatasource', () => {
});
});
});
- describe('When performing getSubscriptions', () => {
+
+ describe('When performing newGetMetricNamespaces', () => {
const response = {
value: [
{
- id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572',
- subscriptionId: '99999999-cccc-bbbb-aaaa-9106972f9572',
- tenantId: '99999999-aaaa-bbbb-cccc-51c4f982ec48',
- displayName: 'Primary Subscription',
- state: 'Enabled',
- subscriptionPolicies: {
- locationPlacementId: 'Public_2014-09-01',
- quotaId: 'PayAsYouGo_2014-09-01',
- spendingLimit: 'Off',
+ id: '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1/providers/microsoft.insights/metricNamespaces/Azure.ApplicationInsights',
+ name: 'Azure.ApplicationInsights',
+ type: 'Microsoft.Insights/metricNamespaces',
+ classification: 'Custom',
+ properties: {
+ metricNamespaceName: 'Azure.ApplicationInsights',
},
- authorizationSource: 'RoleBased',
- },
- ],
- count: {
- type: 'Total',
- value: 1,
- },
- };
-
- beforeEach(() => {
- ctx.instanceSettings.jsonData.azureAuthType = 'msi';
- ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
- });
-
- it('should return list of subscriptions', () => {
- return ctx.ds.getSubscriptions().then((results: Array<{ text: string; value: string }>) => {
- expect(results.length).toEqual(1);
- expect(results[0].text).toEqual('Primary Subscription');
- expect(results[0].value).toEqual('99999999-cccc-bbbb-aaaa-9106972f9572');
- });
- });
- });
-
- describe('When performing getResourceGroups', () => {
- const response = {
- value: [{ name: 'grp1' }, { name: 'grp2' }],
- };
-
- beforeEach(() => {
- ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
- });
-
- it('should return list of Resource Groups', () => {
- return ctx.ds.getResourceGroups('subscriptionId').then((results: Array<{ text: string; value: string }>) => {
- expect(results.length).toEqual(2);
- expect(results[0].text).toEqual('grp1');
- expect(results[0].value).toEqual('grp1');
- expect(results[1].text).toEqual('grp2');
- expect(results[1].value).toEqual('grp2');
- });
- });
- });
-
- describe('When performing getMetricDefinitions', () => {
- const response = {
- value: [
- {
- name: 'test',
- type: 'Microsoft.Network/networkInterfaces',
},
{
- location: 'northeurope',
- name: 'northeur',
- type: 'Microsoft.Compute/virtualMachines',
- },
- {
- location: 'westcentralus',
- name: 'us',
- type: 'Microsoft.Compute/virtualMachines',
- },
- {
- name: 'IHaveNoMetrics',
- type: 'IShouldBeFilteredOut',
- },
- {
- name: 'storageTest',
- type: 'Microsoft.Storage/storageAccounts',
+ id: '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1/providers/microsoft.insights/metricNamespaces/microsoft.insights-components',
+ name: 'microsoft.insights-components',
+ type: 'Microsoft.Insights/metricNamespaces',
+ classification: 'Platform',
+ properties: {
+ metricNamespaceName: 'microsoft.insights/components',
+ },
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
- const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
- expect(path).toBe(basePath + '/nodesapp/resources?api-version=2021-04-01');
+ const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
+ const expected =
+ basePath +
+ '/providers/microsoft.insights/components/resource1' +
+ '/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview';
+ expect(path).toBe(expected);
return Promise.resolve(response);
});
});
- it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => {
- return ctx.ds
- .getMetricDefinitions('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodesapp')
+ it('should return list of Metric Namspaces', () => {
+ return ctx.ds.azureMonitorDatasource
+ .newGetMetricNamespaces(
+ '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1'
+ )
.then((results: Array<{ text: string; value: string }>) => {
- expect(results.length).toEqual(7);
- expect(results[0].text).toEqual('Network interface');
- expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
- expect(results[1].text).toEqual('Virtual machine');
- expect(results[1].value).toEqual('Microsoft.Compute/virtualMachines');
- expect(results[2].text).toEqual('Storage account');
- expect(results[2].value).toEqual('Microsoft.Storage/storageAccounts');
- expect(results[3].text).toEqual('Microsoft.Storage/storageAccounts/blobServices');
- expect(results[3].value).toEqual('Microsoft.Storage/storageAccounts/blobServices');
- expect(results[4].text).toEqual('Microsoft.Storage/storageAccounts/fileServices');
- expect(results[4].value).toEqual('Microsoft.Storage/storageAccounts/fileServices');
- expect(results[5].text).toEqual('Microsoft.Storage/storageAccounts/tableServices');
- expect(results[5].value).toEqual('Microsoft.Storage/storageAccounts/tableServices');
- expect(results[6].text).toEqual('Microsoft.Storage/storageAccounts/queueServices');
- expect(results[6].value).toEqual('Microsoft.Storage/storageAccounts/queueServices');
+ expect(results.length).toEqual(2);
+ expect(results[0].text).toEqual('Azure.ApplicationInsights');
+ expect(results[0].value).toEqual('Azure.ApplicationInsights');
+ expect(results[1].text).toEqual('microsoft.insights-components');
+ expect(results[1].value).toEqual('microsoft.insights/components');
});
});
});
- describe('When performing getResourceNames', () => {
- let subscription = '9935389e-9122-4ef9-95f9-1513dd24753f';
- let resourceGroup = 'nodeapp';
- let metricDefinition = 'microsoft.insights/components';
-
- beforeEach(() => {
- subscription = '9935389e-9122-4ef9-95f9-1513dd24753f';
- resourceGroup = 'nodeapp';
- metricDefinition = 'microsoft.insights/components';
- });
-
- describe('and there are no special cases', () => {
- const response = {
- value: [
- {
- name: 'Failure Anomalies - nodeapp',
- type: 'microsoft.insights/alertrules',
- },
- {
- name: resourceGroup,
- type: metricDefinition,
- },
- ],
- };
-
- beforeEach(() => {
- ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
- const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
- expect(path).toBe(
- `${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`
- );
- return Promise.resolve(response);
- });
- });
-
- it('should return list of Resource Names', () => {
- return ctx.ds
- .getResourceNames(subscription, resourceGroup, metricDefinition)
- .then((results: Array<{ text: string; value: string }>) => {
- expect(results.length).toEqual(1);
- expect(results[0].text).toEqual('nodeapp');
- expect(results[0].value).toEqual('nodeapp');
- });
- });
-
- it('should return ignore letter case', () => {
- metricDefinition = 'microsoft.insights/Components';
- return ctx.ds
- .getResourceNames(subscription, resourceGroup, metricDefinition)
- .then((results: Array<{ text: string; value: string }>) => {
- expect(results.length).toEqual(1);
- expect(results[0].text).toEqual('nodeapp');
- expect(results[0].value).toEqual('nodeapp');
- });
- });
- });
-
- describe('and the metric definition is blobServices', () => {
- const response = {
- value: [
- {
- name: 'Failure Anomalies - nodeapp',
- type: 'microsoft.insights/alertrules',
- },
- {
- name: 'storagetest',
- type: 'Microsoft.Storage/storageAccounts',
- },
- ],
- };
-
- beforeEach(() => {
- ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
- const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
- expect(path).toBe(
- basePath +
- `/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`
- );
- return Promise.resolve(response);
- });
- });
-
- it('should return list of Resource Names', () => {
- metricDefinition = 'Microsoft.Storage/storageAccounts/blobServices';
- return ctx.ds
- .getResourceNames(subscription, resourceGroup, metricDefinition)
- .then((results: Array<{ text: string; value: string }>) => {
- expect(results.length).toEqual(1);
- expect(results[0].text).toEqual('storagetest/default');
- expect(results[0].value).toEqual('storagetest/default');
- });
- });
- });
-
- describe('and there are several pages', () => {
- const skipToken = 'token';
- const response1 = {
- value: [
- {
- name: `${resourceGroup}1`,
- type: metricDefinition,
- },
- ],
- nextLink: `https://management.azure.com/resourceuri?$skiptoken=${skipToken}`,
- };
- const response2 = {
- value: [
- {
- name: `${resourceGroup}2`,
- type: metricDefinition,
- },
- ],
- };
-
- beforeEach(() => {
- const fn = jest.fn();
- ctx.ds.azureMonitorDatasource.getResource = fn;
- const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
- const expectedPath = `${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`;
- // first page
- fn.mockImplementationOnce((path: string) => {
- expect(path).toBe(expectedPath);
- return Promise.resolve(response1);
- });
- // second page
- fn.mockImplementationOnce((path: string) => {
- expect(path).toBe(`${expectedPath}&$skiptoken=${skipToken}`);
- return Promise.resolve(response2);
- });
- });
-
- it('should return list of Resource Names', () => {
- return ctx.ds
- .getResourceNames(subscription, resourceGroup, metricDefinition)
- .then((results: Array<{ text: string; value: string }>) => {
- expect(results.length).toEqual(2);
- expect(results[0].value).toEqual(`${resourceGroup}1`);
- expect(results[1].value).toEqual(`${resourceGroup}2`);
- });
- });
- });
- });
-
- describe('When performing getMetricNames', () => {
+ describe('When performing newGetMetricNames', () => {
const response = {
value: [
{
@@ -383,12 +176,9 @@ describe('AzureMonitorDatasource', () => {
});
it('should return list of Metric Definitions', () => {
- return ctx.ds
- .getMetricNames(
- '9935389e-9122-4ef9-95f9-1513dd24753f',
- 'nodeapp',
- 'microsoft.insights/components',
- 'resource1',
+ return ctx.ds.azureMonitorDatasource
+ .newGetMetricNames(
+ '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1',
'default'
)
.then((results: Array<{ text: string; value: string }>) => {
@@ -401,7 +191,7 @@ describe('AzureMonitorDatasource', () => {
});
});
- describe('When performing getMetricMetadata', () => {
+ describe('When performing newGetMetricMetadata', () => {
const response = {
value: [
{
@@ -450,12 +240,9 @@ describe('AzureMonitorDatasource', () => {
});
it('should return Aggregation metadata for a Metric', () => {
- return ctx.ds
- .getMetricMetadata(
- '9935389e-9122-4ef9-95f9-1513dd24753f',
- 'nodeapp',
- 'microsoft.insights/components',
- 'resource1',
+ return ctx.ds.azureMonitorDatasource
+ .newGetMetricMetadata(
+ '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1',
'default',
'UsedCapacity'
)
@@ -467,130 +254,524 @@ describe('AzureMonitorDatasource', () => {
});
});
- describe('When performing getMetricMetadata on metrics with dimensions', () => {
- const response = {
- value: [
- {
- name: {
- value: 'Transactions',
- localizedValue: 'Transactions',
+ describe('Legacy Azure Monitor Query Object data fetchers', () => {
+ describe('When performing getSubscriptions', () => {
+ const response = {
+ value: [
+ {
+ id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572',
+ subscriptionId: '99999999-cccc-bbbb-aaaa-9106972f9572',
+ tenantId: '99999999-aaaa-bbbb-cccc-51c4f982ec48',
+ displayName: 'Primary Subscription',
+ state: 'Enabled',
+ subscriptionPolicies: {
+ locationPlacementId: 'Public_2014-09-01',
+ quotaId: 'PayAsYouGo_2014-09-01',
+ spendingLimit: 'Off',
+ },
+ authorizationSource: 'RoleBased',
},
- unit: 'Count',
- primaryAggregationType: 'Total',
- supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
- isDimensionRequired: false,
- dimensions: [
+ ],
+ count: {
+ type: 'Total',
+ value: 1,
+ },
+ };
+
+ beforeEach(() => {
+ ctx.instanceSettings.jsonData.azureAuthType = 'msi';
+ ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
+ });
+
+ it('should return list of subscriptions', () => {
+ return ctx.ds.getSubscriptions().then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(1);
+ expect(results[0].text).toEqual('Primary Subscription');
+ expect(results[0].value).toEqual('99999999-cccc-bbbb-aaaa-9106972f9572');
+ });
+ });
+ });
+
+ describe('When performing getResourceGroups', () => {
+ const response = {
+ value: [{ name: 'grp1' }, { name: 'grp2' }],
+ };
+
+ beforeEach(() => {
+ ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
+ });
+
+ it('should return list of Resource Groups', () => {
+ return ctx.ds.getResourceGroups('subscriptionId').then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(2);
+ expect(results[0].text).toEqual('grp1');
+ expect(results[0].value).toEqual('grp1');
+ expect(results[1].text).toEqual('grp2');
+ expect(results[1].value).toEqual('grp2');
+ });
+ });
+ });
+
+ describe('When performing getMetricDefinitions', () => {
+ const response = {
+ value: [
+ {
+ name: 'test',
+ type: 'Microsoft.Network/networkInterfaces',
+ },
+ {
+ location: 'northeurope',
+ name: 'northeur',
+ type: 'Microsoft.Compute/virtualMachines',
+ },
+ {
+ location: 'westcentralus',
+ name: 'us',
+ type: 'Microsoft.Compute/virtualMachines',
+ },
+ {
+ name: 'IHaveNoMetrics',
+ type: 'IShouldBeFilteredOut',
+ },
+ {
+ name: 'storageTest',
+ type: 'Microsoft.Storage/storageAccounts',
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
+ const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
+ expect(path).toBe(basePath + '/nodesapp/resources?api-version=2021-04-01');
+ return Promise.resolve(response);
+ });
+ });
+
+ it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => {
+ return ctx.ds
+ .getMetricDefinitions('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodesapp')
+ .then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(7);
+ expect(results[0].text).toEqual('Network interface');
+ expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
+ expect(results[1].text).toEqual('Virtual machine');
+ expect(results[1].value).toEqual('Microsoft.Compute/virtualMachines');
+ expect(results[2].text).toEqual('Storage account');
+ expect(results[2].value).toEqual('Microsoft.Storage/storageAccounts');
+ expect(results[3].text).toEqual('Microsoft.Storage/storageAccounts/blobServices');
+ expect(results[3].value).toEqual('Microsoft.Storage/storageAccounts/blobServices');
+ expect(results[4].text).toEqual('Microsoft.Storage/storageAccounts/fileServices');
+ expect(results[4].value).toEqual('Microsoft.Storage/storageAccounts/fileServices');
+ expect(results[5].text).toEqual('Microsoft.Storage/storageAccounts/tableServices');
+ expect(results[5].value).toEqual('Microsoft.Storage/storageAccounts/tableServices');
+ expect(results[6].text).toEqual('Microsoft.Storage/storageAccounts/queueServices');
+ expect(results[6].value).toEqual('Microsoft.Storage/storageAccounts/queueServices');
+ });
+ });
+ });
+
+ describe('When performing getResourceNames', () => {
+ let subscription = '9935389e-9122-4ef9-95f9-1513dd24753f';
+ let resourceGroup = 'nodeapp';
+ let metricDefinition = 'microsoft.insights/components';
+
+ beforeEach(() => {
+ subscription = '9935389e-9122-4ef9-95f9-1513dd24753f';
+ resourceGroup = 'nodeapp';
+ metricDefinition = 'microsoft.insights/components';
+ });
+
+ describe('and there are no special cases', () => {
+ const response = {
+ value: [
{
- value: 'ResponseType',
- localizedValue: 'Response type',
+ name: 'Failure Anomalies - nodeapp',
+ type: 'microsoft.insights/alertrules',
},
{
- value: 'GeoType',
- localizedValue: 'Geo type',
- },
- {
- value: 'ApiName',
- localizedValue: 'API name',
+ name: resourceGroup,
+ type: metricDefinition,
},
],
- },
- {
- name: {
- value: 'FreeCapacity',
- localizedValue: 'Free capacity',
+ };
+
+ beforeEach(() => {
+ ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
+ const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
+ expect(path).toBe(
+ `${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`
+ );
+ return Promise.resolve(response);
+ });
+ });
+
+ it('should return list of Resource Names', () => {
+ return ctx.ds
+ .getResourceNames(subscription, resourceGroup, metricDefinition)
+ .then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(1);
+ expect(results[0].text).toEqual('nodeapp');
+ expect(results[0].value).toEqual('nodeapp');
+ });
+ });
+
+ it('should return ignore letter case', () => {
+ metricDefinition = 'microsoft.insights/Components';
+ return ctx.ds
+ .getResourceNames(subscription, resourceGroup, metricDefinition)
+ .then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(1);
+ expect(results[0].text).toEqual('nodeapp');
+ expect(results[0].value).toEqual('nodeapp');
+ });
+ });
+ });
+
+ describe('and the metric definition is blobServices', () => {
+ const response = {
+ value: [
+ {
+ name: 'Failure Anomalies - nodeapp',
+ type: 'microsoft.insights/alertrules',
+ },
+ {
+ name: 'storagetest',
+ type: 'Microsoft.Storage/storageAccounts',
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
+ const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
+ expect(path).toBe(
+ basePath +
+ `/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`
+ );
+ return Promise.resolve(response);
+ });
+ });
+
+ it('should return list of Resource Names', () => {
+ metricDefinition = 'Microsoft.Storage/storageAccounts/blobServices';
+ return ctx.ds
+ .getResourceNames(subscription, resourceGroup, metricDefinition)
+ .then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(1);
+ expect(results[0].text).toEqual('storagetest/default');
+ expect(results[0].value).toEqual('storagetest/default');
+ });
+ });
+ });
+
+ describe('and there are several pages', () => {
+ const skipToken = 'token';
+ const response1 = {
+ value: [
+ {
+ name: `${resourceGroup}1`,
+ type: metricDefinition,
+ },
+ ],
+ nextLink: `https://management.azure.com/resourceuri?$skiptoken=${skipToken}`,
+ };
+ const response2 = {
+ value: [
+ {
+ name: `${resourceGroup}2`,
+ type: metricDefinition,
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ const fn = jest.fn();
+ ctx.ds.azureMonitorDatasource.getResource = fn;
+ const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
+ const expectedPath = `${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`;
+ // first page
+ fn.mockImplementationOnce((path: string) => {
+ expect(path).toBe(expectedPath);
+ return Promise.resolve(response1);
+ });
+ // second page
+ fn.mockImplementationOnce((path: string) => {
+ expect(path).toBe(`${expectedPath}&$skiptoken=${skipToken}`);
+ return Promise.resolve(response2);
+ });
+ });
+
+ it('should return list of Resource Names', () => {
+ return ctx.ds
+ .getResourceNames(subscription, resourceGroup, metricDefinition)
+ .then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(2);
+ expect(results[0].value).toEqual(`${resourceGroup}1`);
+ expect(results[1].value).toEqual(`${resourceGroup}2`);
+ });
+ });
+ });
+ });
+
+ describe('When performing getMetricNames', () => {
+ const response = {
+ value: [
+ {
+ name: {
+ value: 'UsedCapacity',
+ localizedValue: 'Used capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Total',
+ supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
+ metricAvailabilities: [
+ { timeGrain: 'PT1H', retention: 'P93D' },
+ { timeGrain: 'PT6H', retention: 'P93D' },
+ { timeGrain: 'PT12H', retention: 'P93D' },
+ { timeGrain: 'P1D', retention: 'P93D' },
+ ],
},
- unit: 'CountPerSecond',
- primaryAggregationType: 'Average',
- supportedAggregationTypes: ['None', 'Average'],
- },
- ],
- };
+ {
+ name: {
+ value: 'FreeCapacity',
+ localizedValue: 'Free capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Average',
+ supportedAggregationTypes: ['None', 'Average'],
+ metricAvailabilities: [
+ { timeGrain: 'PT1H', retention: 'P93D' },
+ { timeGrain: 'PT6H', retention: 'P93D' },
+ { timeGrain: 'PT12H', retention: 'P93D' },
+ { timeGrain: 'P1D', retention: 'P93D' },
+ ],
+ },
+ ],
+ };
- beforeEach(() => {
- ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
- const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
- const expected =
- basePath +
- '/providers/microsoft.insights/components/resource1' +
- '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
- expect(path).toBe(expected);
- return Promise.resolve(response);
- });
- });
-
- it('should return dimensions for a Metric that has dimensions', () => {
- return ctx.ds
- .getMetricMetadata(
- '9935389e-9122-4ef9-95f9-1513dd24753f',
- 'nodeapp',
- 'microsoft.insights/components',
- 'resource1',
- 'default',
- 'Transactions'
- )
- .then((results: any) => {
- expect(results.dimensions).toMatchInlineSnapshot(`
- Array [
- Object {
- "label": "Response type",
- "value": "ResponseType",
- },
- Object {
- "label": "Geo type",
- "value": "GeoType",
- },
- Object {
- "label": "API name",
- "value": "ApiName",
- },
- ]
- `);
+ beforeEach(() => {
+ ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
+ const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
+ const expected =
+ basePath +
+ '/providers/microsoft.insights/components/resource1' +
+ '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
+ expect(path).toBe(expected);
+ return Promise.resolve(response);
});
- });
-
- describe('When performing targetContainsTemplate', () => {
- it('should return false when no variable is being used', () => {
- const query = createMockQuery();
- query.queryType = AzureQueryType.AzureMonitor;
- expect(ctx.ds.targetContainsTemplate(query)).toEqual(false);
});
- it('should return true when subscriptions field is using a variable', () => {
- const query = createMockQuery();
- const templateSrv = new TemplateSrv();
- templateSrv.init([subscriptionsVariable]);
-
- const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv);
- query.queryType = AzureQueryType.AzureMonitor;
- query.subscription = `$${subscriptionsVariable.name}`;
- expect(ds.targetContainsTemplate(query)).toEqual(true);
- });
-
- it('should return false when a variable is used in a different part of the query', () => {
- const query = createMockQuery();
- const templateSrv = new TemplateSrv();
- templateSrv.init([singleVariable]);
-
- const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv);
- query.queryType = AzureQueryType.AzureMonitor;
- query.azureLogAnalytics = { resource: `$${singleVariable.name}` };
- expect(ds.targetContainsTemplate(query)).toEqual(false);
+ it('should return list of Metric Definitions', () => {
+ return ctx.ds
+ .getMetricNames(
+ '9935389e-9122-4ef9-95f9-1513dd24753f',
+ 'nodeapp',
+ 'microsoft.insights/components',
+ 'resource1',
+ 'default'
+ )
+ .then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(2);
+ expect(results[0].text).toEqual('Used capacity');
+ expect(results[0].value).toEqual('UsedCapacity');
+ expect(results[1].text).toEqual('Free capacity');
+ expect(results[1].value).toEqual('FreeCapacity');
+ });
});
});
- it('should return an empty array for a Metric that does not have dimensions', () => {
- return ctx.ds
- .getMetricMetadata(
- '9935389e-9122-4ef9-95f9-1513dd24753f',
- 'nodeapp',
- 'microsoft.insights/components',
- 'resource1',
- 'default',
- 'FreeCapacity'
- )
- .then((results: any) => {
- expect(results.dimensions.length).toEqual(0);
+ describe('When performing getMetricMetadata', () => {
+ const response = {
+ value: [
+ {
+ name: {
+ value: 'UsedCapacity',
+ localizedValue: 'Used capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Total',
+ supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
+ metricAvailabilities: [
+ { timeGrain: 'PT1H', retention: 'P93D' },
+ { timeGrain: 'PT6H', retention: 'P93D' },
+ { timeGrain: 'PT12H', retention: 'P93D' },
+ { timeGrain: 'P1D', retention: 'P93D' },
+ ],
+ },
+ {
+ name: {
+ value: 'FreeCapacity',
+ localizedValue: 'Free capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Average',
+ supportedAggregationTypes: ['None', 'Average'],
+ metricAvailabilities: [
+ { timeGrain: 'PT1H', retention: 'P93D' },
+ { timeGrain: 'PT6H', retention: 'P93D' },
+ { timeGrain: 'PT12H', retention: 'P93D' },
+ { timeGrain: 'P1D', retention: 'P93D' },
+ ],
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
+ const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
+ const expected =
+ basePath +
+ '/providers/microsoft.insights/components/resource1' +
+ '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
+ expect(path).toBe(expected);
+ return Promise.resolve(response);
});
+ });
+
+ it('should return Aggregation metadata for a Metric', () => {
+ return ctx.ds
+ .getMetricMetadata(
+ '9935389e-9122-4ef9-95f9-1513dd24753f',
+ 'nodeapp',
+ 'microsoft.insights/components',
+ 'resource1',
+ 'default',
+ 'UsedCapacity'
+ )
+ .then((results) => {
+ expect(results.primaryAggType).toEqual('Total');
+ expect(results.supportedAggTypes.length).toEqual(6);
+ expect(results.supportedTimeGrains.length).toEqual(5); // 4 time grains from the API + auto
+ });
+ });
+ });
+
+ describe('When performing getMetricMetadata on metrics with dimensions', () => {
+ const response = {
+ value: [
+ {
+ name: {
+ value: 'Transactions',
+ localizedValue: 'Transactions',
+ },
+ unit: 'Count',
+ primaryAggregationType: 'Total',
+ supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
+ isDimensionRequired: false,
+ dimensions: [
+ {
+ value: 'ResponseType',
+ localizedValue: 'Response type',
+ },
+ {
+ value: 'GeoType',
+ localizedValue: 'Geo type',
+ },
+ {
+ value: 'ApiName',
+ localizedValue: 'API name',
+ },
+ ],
+ },
+ {
+ name: {
+ value: 'FreeCapacity',
+ localizedValue: 'Free capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Average',
+ supportedAggregationTypes: ['None', 'Average'],
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
+ const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
+ const expected =
+ basePath +
+ '/providers/microsoft.insights/components/resource1' +
+ '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
+ expect(path).toBe(expected);
+ return Promise.resolve(response);
+ });
+ });
+
+ it('should return dimensions for a Metric that has dimensions', () => {
+ return ctx.ds
+ .getMetricMetadata(
+ '9935389e-9122-4ef9-95f9-1513dd24753f',
+ 'nodeapp',
+ 'microsoft.insights/components',
+ 'resource1',
+ 'default',
+ 'Transactions'
+ )
+ .then((results: any) => {
+ expect(results.dimensions).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "label": "Response type",
+ "value": "ResponseType",
+ },
+ Object {
+ "label": "Geo type",
+ "value": "GeoType",
+ },
+ Object {
+ "label": "API name",
+ "value": "ApiName",
+ },
+ ]
+ `);
+ });
+ });
+
+ describe('When performing targetContainsTemplate', () => {
+ it('should return false when no variable is being used', () => {
+ const query = createMockQuery();
+ query.queryType = AzureQueryType.AzureMonitor;
+ expect(ctx.ds.targetContainsTemplate(query)).toEqual(false);
+ });
+
+ it('should return true when subscriptions field is using a variable', () => {
+ const query = createMockQuery();
+ const templateSrv = new TemplateSrv();
+ templateSrv.init([subscriptionsVariable]);
+
+ const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv);
+ query.queryType = AzureQueryType.AzureMonitor;
+ query.subscription = `$${subscriptionsVariable.name}`;
+ expect(ds.targetContainsTemplate(query)).toEqual(true);
+ });
+
+ it('should return false when a variable is used in a different part of the query', () => {
+ const query = createMockQuery();
+ const templateSrv = new TemplateSrv();
+ templateSrv.init([singleVariable]);
+
+ const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv);
+ query.queryType = AzureQueryType.AzureMonitor;
+ query.azureLogAnalytics = { resource: `$${singleVariable.name}` };
+ expect(ds.targetContainsTemplate(query)).toEqual(false);
+ });
+ });
+
+ it('should return an empty array for a Metric that does not have dimensions', () => {
+ return ctx.ds
+ .getMetricMetadata(
+ '9935389e-9122-4ef9-95f9-1513dd24753f',
+ 'nodeapp',
+ 'microsoft.insights/components',
+ 'resource1',
+ 'default',
+ 'FreeCapacity'
+ )
+ .then((results: any) => {
+ expect(results.dimensions.length).toEqual(0);
+ });
+ });
});
});
});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts
index 9f876557e0f..b302061d483 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts
@@ -9,6 +9,9 @@ import TimegrainConverter from '../time_grain_converter';
import {
AzureDataSourceJsonData,
AzureMonitorMetricDefinitionsResponse,
+ AzureMonitorMetricNamespacesResponse,
+ AzureMonitorMetricNamesResponse,
+ AzureMonitorMetricsMetadataResponse,
AzureMonitorQuery,
AzureMonitorResourceGroupsResponse,
AzureQueryType,
@@ -21,6 +24,10 @@ import UrlBuilder from './url_builder';
const defaultDropdownValue = 'select';
+function hasValue(item?: string) {
+ return !!(item && item !== defaultDropdownValue);
+}
+
export default class AzureMonitorDatasource extends DataSourceWithBackend {
apiVersion = '2018-01-01';
apiPreviewVersion = '2017-12-01-preview';
@@ -40,7 +47,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend {
+ return this.getResource(`${this.resourcePath}/subscriptions?api-version=2019-03-01`).then((result: any) => {
return ResponseParser.parseSubscriptions(result);
});
}
getResourceGroups(subscriptionId: string) {
return this.getResource(
- `${this.resourcePath}/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
+ `${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
).then((result: AzureMonitorResourceGroupsResponse) => {
return ResponseParser.parseResponseValues(result, 'name', 'name');
});
@@ -143,7 +150,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend {
return ResponseParser.parseResponseValues(result, 'type', 'type');
@@ -196,7 +203,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend {
+ return ResponseParser.parseResponseValues(result, 'name', 'properties.metricNamespaceName');
+ });
+ }
+
getMetricNames(
subscriptionId: string,
resourceGroup: string,
@@ -266,6 +286,20 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend {
+ return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
+ });
+ }
+
getMetricMetadata(
subscriptionId: string,
resourceGroup: string,
@@ -289,6 +323,20 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend {
+ return ResponseParser.parseMetadata(result, metricName);
+ });
+ }
+
async testDatasource(): Promise {
const validationError = this.validateDatasource();
if (validationError) {
@@ -296,7 +344,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend((response: any) => {
return {
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts
index 9ed28795078..4aa8b984548 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts
@@ -1,180 +1,209 @@
import UrlBuilder from './url_builder';
describe('AzureMonitorUrlBuilder', () => {
- describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => {
- it('should build the getMetricNamespaces url in the even longer format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
+ describe('when a resource uri is provided', () => {
+ it('builds a getMetricNamesnamespace url', () => {
+ const url = UrlBuilder.newBuildAzureMonitorGetMetricNamespacesUrl(
'',
- 'sub1',
- 'rg',
- 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
- 'rn1/rn2/rn3',
+ '/subscriptions/sub/resource-uri/resource',
'2017-05-01-preview'
);
expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
- 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
+ '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
);
});
});
- describe('when metric definition is Microsoft.Sql/servers/databases', () => {
- it('should build the getMetricNamespaces url in the longer format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
+ describe('when a resource uri and metric namespace is provided', () => {
+ it('builds a getMetricNames url', () => {
+ const url = UrlBuilder.newBuildAzureMonitorGetMetricNamesUrl(
'',
- 'sub1',
- 'rg',
- 'Microsoft.Sql/servers/databases',
- 'rn1/rn2',
- '2017-05-01-preview'
- );
- expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
- 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
- );
- });
- });
-
- describe('when metric definition is Microsoft.Sql/servers', () => {
- it('should build the getMetricNamespaces url in the shorter format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
- '',
- 'sub1',
- 'rg',
+ '/subscriptions/sub/resource-uri/resource',
'Microsoft.Sql/servers',
- 'rn',
'2017-05-01-preview'
);
expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
- 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
+ '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=Microsoft.Sql%2Fservers'
);
});
});
- describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => {
- it('should build the getMetricNames url in the even longer format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
- '',
- 'sub1',
- 'rg',
- 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
- 'rn1/rn2/rn3',
- 'default',
- '2017-05-01-preview'
- );
- expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
- 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
- );
+ describe('Legacy query object', () => {
+ describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => {
+ it('should build the getMetricNamespaces url in the even longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
+ 'rn1/rn2/rn3',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
+ 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
+ );
+ });
});
- });
- describe('when metric definition is Microsoft.Sql/servers/databases and the metricNamespace is default', () => {
- it('should build the getMetricNames url in the longer format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
- '',
- 'sub1',
- 'rg',
- 'Microsoft.Sql/servers/databases',
- 'rn1/rn2',
- 'default',
- '2017-05-01-preview'
- );
- expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
- 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
- );
+ describe('when metric definition is Microsoft.Sql/servers/databases', () => {
+ it('should build the getMetricNamespaces url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.Sql/servers/databases',
+ 'rn1/rn2',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
+ 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
+ );
+ });
});
- });
- describe('when metric definition is Microsoft.Sql/servers and the metricNamespace is default', () => {
- it('should build the getMetricNames url in the shorter format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
- '',
- 'sub1',
- 'rg',
- 'Microsoft.Sql/servers',
- 'rn',
- 'default',
- '2017-05-01-preview'
- );
- expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
- 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
- );
+ describe('when metric definition is Microsoft.Sql/servers', () => {
+ it('should build the getMetricNamespaces url in the shorter format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.Sql/servers',
+ 'rn',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
+ 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
+ );
+ });
});
- });
- describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices and the metricNamespace is default', () => {
- it('should build the getMetricNames url in the longer format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
- '',
- 'sub1',
- 'rg',
- 'Microsoft.Storage/storageAccounts/blobServices',
- 'rn1/default',
- 'default',
- '2017-05-01-preview'
- );
- expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
- 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
- );
+ describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => {
+ it('should build the getMetricNames url in the even longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
+ 'rn1/rn2/rn3',
+ 'default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
+ );
+ });
});
- });
- describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices and the metricNamespace is default', () => {
- it('should build the getMetricNames url in the longer format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
- '',
- 'sub1',
- 'rg',
- 'Microsoft.Storage/storageAccounts/fileServices',
- 'rn1/default',
- 'default',
- '2017-05-01-preview'
- );
- expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
- 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
- );
+ describe('when metric definition is Microsoft.Sql/servers/databases and the metricNamespace is default', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.Sql/servers/databases',
+ 'rn1/rn2',
+ 'default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
+ );
+ });
});
- });
- describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices and the metricNamespace is default', () => {
- it('should build the getMetricNames url in the longer format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
- '',
- 'sub1',
- 'rg',
- 'Microsoft.Storage/storageAccounts/tableServices',
- 'rn1/default',
- 'default',
- '2017-05-01-preview'
- );
- expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
- 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
- );
+ describe('when metric definition is Microsoft.Sql/servers and the metricNamespace is default', () => {
+ it('should build the getMetricNames url in the shorter format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.Sql/servers',
+ 'rn',
+ 'default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
+ );
+ });
});
- });
- describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices and the metricNamespace is default', () => {
- it('should build the getMetricNames url in the longer format', () => {
- const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
- '',
- 'sub1',
- 'rg',
- 'Microsoft.Storage/storageAccounts/queueServices',
- 'rn1/default',
- 'default',
- '2017-05-01-preview'
- );
- expect(url).toBe(
- '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
- 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
- );
+ describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices and the metricNamespace is default', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/blobServices',
+ 'rn1/default',
+ 'default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices and the metricNamespace is default', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/fileServices',
+ 'rn1/default',
+ 'default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices and the metricNamespace is default', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/tableServices',
+ 'rn1/default',
+ 'default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices and the metricNamespace is default', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'sub1',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/queueServices',
+ 'rn1/default',
+ 'default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
+ );
+ });
});
});
});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts
index a477dd23ef4..1d18c89a09d 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts
@@ -10,7 +10,7 @@ export default class UrlBuilder {
const metricDefinitionArray = metricDefinition.split('/');
const resourceNameArray = resourceName.split('/');
const provider = metricDefinitionArray.shift();
- const urlArray = [baseUrl, subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider];
+ const urlArray = [baseUrl, 'subscriptions', subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider];
for (const i in metricDefinitionArray) {
urlArray.push(metricDefinitionArray[i]);
urlArray.push(resourceNameArray[i]);
@@ -31,7 +31,7 @@ export default class UrlBuilder {
const metricDefinitionArray = metricDefinition.split('/');
const resourceNameArray = resourceName.split('/');
const provider = metricDefinitionArray.shift();
- const urlArray = [baseUrl, subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider];
+ const urlArray = [baseUrl, 'subscriptions', subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider];
for (const i in metricDefinitionArray) {
urlArray.push(metricDefinitionArray[i]);
urlArray.push(resourceNameArray[i]);
@@ -42,4 +42,20 @@ export default class UrlBuilder {
`&metricnamespace=${encodeURIComponent(metricNamespace)}`
);
}
+
+ static newBuildAzureMonitorGetMetricNamespacesUrl(baseUrl: string, resourceUri: string, apiVersion: string) {
+ return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}`;
+ }
+
+ static newBuildAzureMonitorGetMetricNamesUrl(
+ baseUrl: string,
+ resourceUri: string,
+ metricNamespace: string,
+ apiVersion: string
+ ) {
+ return (
+ `${baseUrl}${resourceUri}/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}` +
+ `&metricnamespace=${encodeURIComponent(metricNamespace)}`
+ );
+ }
}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx
index d6ae263793d..4e63ac97ed5 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx
@@ -2,10 +2,12 @@ import React from 'react';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
import Datasource from '../../datasource';
import { Alert, InlineFieldRow } from '@grafana/ui';
+import { ResourceRowType } from '../ResourcePicker/types';
+import ResourceField from '../ResourceField';
import QueryField from './QueryField';
import FormatAsField from './FormatAsField';
-import ResourceField from './ResourceField';
import useMigrations from './useMigrations';
+import { setResource } from './setQueryValue';
interface LogsQueryEditorProps {
query: AzureMonitorQuery;
@@ -38,6 +40,14 @@ const LogsQueryEditor: React.FC = ({
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
+ selectableEntryTypes={[
+ ResourceRowType.Subscription,
+ ResourceRowType.ResourceGroup,
+ ResourceRowType.Resource,
+ ResourceRowType.Variable,
+ ]}
+ setResource={setResource}
+ resourceUri={query.azureLogAnalytics?.resource}
/>
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts
index 6de964895cd..2b25f47ad2c 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts
@@ -6,7 +6,6 @@ import { AzureMetricQuery, AzureMonitorOption, AzureMonitorQuery, AzureQueryType
import {
DataHook,
updateSubscriptions,
- useAsyncState,
useMetricNames,
useMetricNamespaces,
useResourceGroups,
@@ -15,75 +14,12 @@ import {
useSubscriptions,
} from './dataHooks';
-interface WaitableMock extends jest.Mock {
- waitToBeCalled(): Promise;
-}
-
const WAIT_OPTIONS = {
timeout: 1000,
};
-function createWaitableMock() {
- let resolve: Function;
-
- const mock = jest.fn() as WaitableMock;
- mock.mockImplementation(() => {
- resolve && resolve();
- });
-
- mock.waitToBeCalled = () => {
- return new Promise((_resolve) => (resolve = _resolve));
- };
-
- return mock;
-}
-
const opt = (text: string, value: string) => ({ text, value });
-describe('AzureMonitor: useAsyncState', () => {
- const MOCKED_RANDOM_VALUE = 0.42069;
-
- beforeEach(() => {
- jest.spyOn(global.Math, 'random').mockReturnValue(MOCKED_RANDOM_VALUE);
- });
-
- afterEach(() => {
- jest.spyOn(global.Math, 'random').mockRestore();
- });
-
- it('should return data from an async function', async () => {
- const apiCall = () => Promise.resolve(['a', 'b', 'c']);
- const setError = jest.fn();
-
- const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
- await waitForNextUpdate();
-
- expect(result.current).toEqual(['a', 'b', 'c']);
- });
-
- it('should report errors through setError', async () => {
- const error = new Error();
- const apiCall = () => Promise.reject(error);
- const setError = createWaitableMock();
-
- const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
- await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
-
- expect(result.current).toEqual([]);
- expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, error);
- });
-
- it('should clear the error once the request is successful', async () => {
- const apiCall = () => Promise.resolve(['a', 'b', 'c']);
- const setError = createWaitableMock();
-
- const { waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
- await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
-
- expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, undefined);
- });
-});
-
interface TestScenario {
name: string;
hook: DataHook;
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts
index 6ffecfec1ea..c340e9929bb 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts
@@ -1,8 +1,9 @@
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useState } from 'react';
import Datasource from '../../datasource';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
import { hasOption, toOption } from '../../utils/common';
+import { useAsyncState } from '../../utils/useAsyncState';
import { setMetricNamespace, setSubscriptionID } from './setQueryValue';
export interface MetricMetadata {
@@ -26,29 +27,6 @@ export type DataHook = (
setError: SetErrorFn
) => AzureMonitorOption[];
-export function useAsyncState(asyncFn: () => Promise, setError: Function, dependencies: unknown[]) {
- // Use the lazy initial state functionality of useState to assign a random ID to the API call
- // to track where errors come from. See useLastError.
- const [errorSource] = useState(() => Math.random());
- const [value, setValue] = useState();
-
- const finalValue = useMemo(() => value ?? [], [value]);
-
- useEffect(() => {
- asyncFn()
- .then((results) => {
- setValue(results);
- setError(errorSource, undefined);
- })
- .catch((err) => {
- setError(errorSource, err);
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, dependencies);
-
- return finalValue;
-}
-
export const updateSubscriptions = (
query: AzureMonitorQuery,
subscriptionOptions: AzureMonitorOption[],
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/setQueryValue.ts
index 108fe67cb1b..19c49b3d3dd 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/setQueryValue.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/setQueryValue.ts
@@ -1,5 +1,20 @@
import { AzureMetricDimension, AzureMonitorQuery } from '../../types';
+export function setResource(query: AzureMonitorQuery, resourceURI: string | undefined): AzureMonitorQuery {
+ return {
+ ...query,
+ azureMonitor: {
+ ...query.azureMonitor,
+ resourceUri: resourceURI,
+ metricNamespace: undefined,
+ metricName: undefined,
+ aggregation: undefined,
+ timeGrain: '',
+ dimensionFilters: [],
+ },
+ };
+}
+
export function setSubscriptionID(query: AzureMonitorQuery, subscriptionID: string): AzureMonitorQuery {
if (query.subscription === subscriptionID) {
return query;
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.test.tsx
new file mode 100644
index 00000000000..792d3b76123
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.test.tsx
@@ -0,0 +1,226 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { selectOptionInTest } from '@grafana/ui';
+
+import MetricsQueryEditor from './MetricsQueryEditor';
+import createMockQuery from '../../__mocks__/query';
+import createMockDatasource from '../../__mocks__/datasource';
+import createMockResourcePickerData from '../../__mocks__/resourcePickerData';
+import {
+ createMockResourceGroupsBySubscription,
+ createMockSubscriptions,
+ mockResourcesByResourceGroup,
+} from '../../__mocks__/resourcePickerRows';
+
+const variableOptionGroup = {
+ label: 'Template variables',
+ options: [],
+};
+
+const resourcePickerData = createMockResourcePickerData({
+ getSubscriptions: jest.fn().mockResolvedValue(createMockSubscriptions()),
+ getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue(createMockResourceGroupsBySubscription()),
+ getResourcesForResourceGroup: jest.fn().mockResolvedValue(mockResourcesByResourceGroup()),
+});
+
+describe('MetricsQueryEditor', () => {
+ const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
+ beforeEach(() => {
+ window.HTMLElement.prototype.scrollIntoView = function () {};
+ });
+ afterEach(() => {
+ window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
+ });
+
+ it('should render', async () => {
+ const mockDatasource = createMockDatasource({ resourcePickerData });
+
+ render(
+ {}}
+ setError={() => {}}
+ />
+ );
+
+ expect(await screen.findByTestId('azure-monitor-metrics-query-editor-with-resource-picker')).toBeInTheDocument();
+ });
+
+ it('should change resource when a resource is selected in the ResourcePicker', async () => {
+ const mockDatasource = createMockDatasource({ resourcePickerData });
+ const query = createMockQuery();
+ delete query?.azureMonitor?.resourceUri;
+ const onChange = jest.fn();
+
+ render(
+ {}}
+ />
+ );
+
+ const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' });
+ expect(resourcePickerButton).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument();
+ resourcePickerButton.click();
+
+ const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
+ expect(subscriptionButton).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Expand A Great Resource Group' })).not.toBeInTheDocument();
+ subscriptionButton.click();
+
+ const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' });
+ expect(resourceGroupButton).toBeInTheDocument();
+ expect(screen.queryByLabelText('web-server')).not.toBeInTheDocument();
+ resourceGroupButton.click();
+
+ const checkbox = await screen.findByLabelText('web-server');
+ expect(checkbox).toBeInTheDocument();
+ expect(checkbox).not.toBeChecked();
+ userEvent.click(checkbox);
+ expect(checkbox).toBeChecked();
+ userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
+
+ expect(onChange).toBeCalledTimes(1);
+ expect(onChange).toBeCalledWith(
+ expect.objectContaining({
+ azureMonitor: expect.objectContaining({
+ resourceUri:
+ '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server',
+ }),
+ })
+ );
+ });
+
+ it('should reset metric namespace, metric name, and aggregation fields after selecting a new resource when a valid query has already been set', async () => {
+ const mockDatasource = createMockDatasource({ resourcePickerData });
+ const query = createMockQuery();
+ const onChange = jest.fn();
+
+ render(
+ {}}
+ />
+ );
+
+ const resourcePickerButton = await screen.findByRole('button', { name: /grafanastaging/ });
+
+ expect(screen.getByText('Microsoft.Compute/virtualMachines')).toBeInTheDocument();
+ expect(screen.getByText('Metric A')).toBeInTheDocument();
+ expect(screen.getByText('Average')).toBeInTheDocument();
+
+ expect(resourcePickerButton).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument();
+ resourcePickerButton.click();
+
+ const subscriptionButton = await screen.findByRole('button', { name: 'Expand Dev Subscription' });
+ expect(subscriptionButton).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Expand Development 3' })).not.toBeInTheDocument();
+ subscriptionButton.click();
+
+ const resourceGroupButton = await screen.findByRole('button', { name: 'Expand Development 3' });
+ expect(resourceGroupButton).toBeInTheDocument();
+ expect(screen.queryByLabelText('db-server')).not.toBeInTheDocument();
+ resourceGroupButton.click();
+
+ const checkbox = await screen.findByLabelText('db-server');
+ expect(checkbox).toBeInTheDocument();
+ expect(checkbox).not.toBeChecked();
+ userEvent.click(checkbox);
+ expect(checkbox).toBeChecked();
+ userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
+
+ expect(onChange).toBeCalledTimes(1);
+ expect(onChange).toBeCalledWith(
+ expect.objectContaining({
+ azureMonitor: expect.objectContaining({
+ resourceUri:
+ '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server',
+ metricNamespace: undefined,
+ metricName: undefined,
+ aggregation: undefined,
+ timeGrain: '',
+ dimensionFilters: [],
+ }),
+ })
+ );
+ });
+
+ it('should change the metric name when selected', async () => {
+ const mockDatasource = createMockDatasource({ resourcePickerData });
+ const onChange = jest.fn();
+ const mockQuery = createMockQuery();
+ mockDatasource.azureMonitorDatasource.newGetMetricNames = jest.fn().mockResolvedValue([
+ {
+ value: 'metric-a',
+ text: 'Metric A',
+ },
+ {
+ value: 'metric-b',
+ text: 'Metric B',
+ },
+ ]);
+
+ render(
+ {}}
+ />
+ );
+
+ const metrics = await screen.findByLabelText('Metric');
+ expect(metrics).toBeInTheDocument();
+ await selectOptionInTest(metrics, 'Metric B');
+
+ expect(onChange).toHaveBeenLastCalledWith({
+ ...mockQuery,
+ azureMonitor: {
+ ...mockQuery.azureMonitor,
+ metricName: 'metric-b',
+ aggregation: undefined,
+ timeGrain: '',
+ },
+ });
+ });
+
+ it('should change the aggregation type when selected', async () => {
+ const mockDatasource = createMockDatasource({ resourcePickerData });
+ const onChange = jest.fn();
+ const mockQuery = createMockQuery();
+
+ render(
+ {}}
+ />
+ );
+
+ const aggregation = await screen.findByLabelText('Aggregation');
+ expect(aggregation).toBeInTheDocument();
+ await selectOptionInTest(aggregation, 'Maximum');
+
+ expect(onChange).toHaveBeenLastCalledWith({
+ ...mockQuery,
+ azureMonitor: {
+ ...mockQuery.azureMonitor,
+ aggregation: 'Maximum',
+ },
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx
index 595d3203fd7..97e6bb3b0e1 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx
@@ -1,9 +1,113 @@
import React from 'react';
+import { InlineFieldRow } from '@grafana/ui';
+import AggregationField from '../MetricsQueryEditor/AggregationField';
+import MetricNameField from '../MetricsQueryEditor/MetricNameField';
+import MetricNamespaceField from '../MetricsQueryEditor/MetricNamespaceField';
+import TimeGrainField from '../MetricsQueryEditor/TimeGrainField';
+import DimensionFields from '../MetricsQueryEditor/DimensionFields';
+import TopField from '../MetricsQueryEditor/TopField';
+import LegendFormatField from '../MetricsQueryEditor/LegendFormatField';
+import ResourceField from '../ResourceField';
+import { ResourceRowType } from '../ResourcePicker/types';
+import type Datasource from '../../datasource';
+import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types';
+import { useMetricNames, useMetricNamespaces, useMetricMetadata } from './dataHooks';
+import { setResource } from '../MetricsQueryEditor/setQueryValue';
-interface MetricsQueryEditorProps {}
+interface MetricsQueryEditorProps {
+ query: AzureMonitorQuery;
+ datasource: Datasource;
+ onChange: (newQuery: AzureMonitorQuery) => void;
+ variableOptionGroup: { label: string; options: AzureMonitorOption[] };
+ setError: (source: string, error: AzureMonitorErrorish | undefined) => void;
+}
-const MetricsQueryEditor: React.FC = ({}) => {
- return New Query Editor
;
+const MetricsQueryEditor: React.FC = ({
+ query,
+ datasource,
+ variableOptionGroup,
+ onChange,
+ setError,
+}) => {
+ const metricsMetadata = useMetricMetadata(query, datasource, onChange);
+ const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError);
+ const metricNames = useMetricNames(query, datasource, onChange, setError);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
};
export default MetricsQueryEditor;
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.test.ts
new file mode 100644
index 00000000000..7a784885c91
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.test.ts
@@ -0,0 +1,165 @@
+import { renderHook } from '@testing-library/react-hooks';
+
+import createMockDatasource from '../../__mocks__/datasource';
+import Datasource from '../../datasource';
+import { AzureMetricQuery, AzureMonitorOption, AzureMonitorQuery, AzureQueryType } from '../../types';
+import { DataHook } from '../MetricsQueryEditor/dataHooks';
+import { useMetricNames, useMetricNamespaces } from './dataHooks';
+
+const WAIT_OPTIONS = {
+ timeout: 1000,
+};
+
+const opt = (text: string, value: string) => ({ text, value });
+
+interface TestScenario {
+ name: string;
+ hook: DataHook;
+
+ // For convenience, only need to define the azureMonitor part of the query for some tests
+ emptyQueryPartial: AzureMetricQuery;
+ customProperties: AzureMetricQuery;
+ topLevelCustomProperties?: Partial;
+
+ expectedCustomPropertyResults?: Array>;
+ expectedOptions: AzureMonitorOption[];
+}
+
+describe('AzureMonitor: metrics dataHooks', () => {
+ const bareQuery = {
+ refId: 'A',
+ queryType: AzureQueryType.AzureMonitor,
+ subscription: 'sub-abc-123',
+ };
+
+ const testTable: TestScenario[] = [
+ {
+ name: 'useMetricNames',
+ hook: useMetricNames,
+ emptyQueryPartial: {
+ resourceUri:
+ '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
+ metricNamespace: 'azure/vm',
+ },
+ customProperties: {
+ resourceUri:
+ '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
+ metricNamespace: 'azure/vm',
+ metricName: 'metric-$ENVIRONMENT',
+ },
+ expectedOptions: [
+ {
+ label: 'Percentage CPU',
+ value: 'percentage-cpu',
+ },
+ {
+ label: 'Free memory',
+ value: 'free-memory',
+ },
+ ],
+ expectedCustomPropertyResults: [
+ { label: 'Percentage CPU', value: 'percentage-cpu' },
+ { label: 'Free memory', value: 'free-memory' },
+ { label: 'metric-$ENVIRONMENT', value: 'metric-$ENVIRONMENT' },
+ ],
+ },
+
+ {
+ name: 'useMetricNamespaces',
+ hook: useMetricNamespaces,
+ emptyQueryPartial: {
+ resourceUri:
+ '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
+ metricNamespace: 'azure/vm',
+ },
+ customProperties: {
+ resourceUri:
+ '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
+ metricNamespace: 'azure/vm-$ENVIRONMENT',
+ metricName: 'metric-name',
+ },
+ expectedOptions: [
+ {
+ label: 'Compute Virtual Machine',
+ value: 'azure/vmc',
+ },
+ {
+ label: 'Database NS',
+ value: 'azure/dbns',
+ },
+ {
+ label: 'azure/vm',
+ value: 'azure/vm',
+ },
+ ],
+ expectedCustomPropertyResults: [
+ { label: 'Compute Virtual Machine', value: 'azure/vmc' },
+ { label: 'Database NS', value: 'azure/dbns' },
+ { label: 'azure/vm-$ENVIRONMENT', value: 'azure/vm-$ENVIRONMENT' },
+ ],
+ },
+ ];
+
+ let datasource: Datasource;
+ let onChange: jest.Mock;
+ let setError: jest.Mock;
+
+ beforeEach(() => {
+ onChange = jest.fn();
+ setError = jest.fn();
+
+ datasource = createMockDatasource();
+ datasource.getVariables = jest.fn().mockReturnValue(['$sub', '$rg', '$rt', '$variable']);
+
+ datasource.azureMonitorDatasource.getSubscriptions = jest
+ .fn()
+ .mockResolvedValue([opt('sub-abc-123', 'sub-abc-123')]);
+
+ datasource.getResourceGroups = jest
+ .fn()
+ .mockResolvedValue([
+ opt('Web App - Production', 'web-app-production'),
+ opt('Web App - Development', 'web-app-development'),
+ ]);
+
+ datasource.getMetricDefinitions = jest
+ .fn()
+ .mockResolvedValue([opt('Virtual Machine', 'azure/vm'), opt('Database', 'azure/db')]);
+
+ datasource.getResourceNames = jest
+ .fn()
+ .mockResolvedValue([opt('Web server', 'web-server'), opt('Job server', 'job-server')]);
+
+ datasource.azureMonitorDatasource.newGetMetricNames = jest
+ .fn()
+ .mockResolvedValue([opt('Percentage CPU', 'percentage-cpu'), opt('Free memory', 'free-memory')]);
+
+ datasource.azureMonitorDatasource.newGetMetricNamespaces = jest
+ .fn()
+ .mockResolvedValue([opt('Compute Virtual Machine', 'azure/vmc'), opt('Database NS', 'azure/dbns')]);
+ });
+ describe.each(testTable)('scenario %#: $name', (scenario) => {
+ it('returns values', async () => {
+ const query = {
+ ...bareQuery,
+ azureMonitor: scenario.emptyQueryPartial,
+ };
+ const { result, waitForNextUpdate } = renderHook(() => scenario.hook(query, datasource, onChange, setError));
+ await waitForNextUpdate(WAIT_OPTIONS);
+
+ expect(result.current).toEqual(scenario.expectedOptions);
+ });
+
+ it('adds custom properties as a valid option', async () => {
+ const query = {
+ ...bareQuery,
+ azureMonitor: scenario.customProperties,
+ ...scenario.topLevelCustomProperties,
+ };
+ const { result, waitForNextUpdate } = renderHook(() => scenario.hook(query, datasource, onChange, setError));
+ await waitForNextUpdate(WAIT_OPTIONS);
+
+ expect(result.current).toEqual(scenario.expectedCustomPropertyResults);
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.ts
new file mode 100644
index 00000000000..a8c11ebd38b
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.ts
@@ -0,0 +1,144 @@
+import { useEffect, useState } from 'react';
+
+import Datasource from '../../datasource';
+import { AzureMonitorOption, AzureMonitorQuery } from '../../types';
+import { toOption } from '../../utils/common';
+import { useAsyncState } from '../../utils/useAsyncState';
+import { DataHook } from '../MetricsQueryEditor/dataHooks';
+import { setMetricNamespace } from '../MetricsQueryEditor/setQueryValue';
+
+export interface MetricMetadata {
+ aggOptions: AzureMonitorOption[];
+ timeGrains: AzureMonitorOption[];
+ dimensions: AzureMonitorOption[];
+ isLoading: boolean;
+
+ // These two properties are only used within the hook, and not elsewhere
+ supportedAggTypes: string[];
+ primaryAggType: string | undefined;
+}
+
+type OnChangeFn = (newQuery: AzureMonitorQuery) => void;
+
+export const useMetricNamespaces: DataHook = (query, datasource, onChange, setError) => {
+ const { metricNamespace, resourceUri } = query.azureMonitor ?? {};
+
+ const metricNamespaces = useAsyncState(
+ async () => {
+ if (!resourceUri) {
+ return;
+ }
+
+ const results = await datasource.azureMonitorDatasource.newGetMetricNamespaces(resourceUri);
+ const options = formatOptions(results, metricNamespace);
+
+ // Do some cleanup of the query state if need be
+ if (!metricNamespace && options.length) {
+ onChange(setMetricNamespace(query, options[0].value));
+ }
+
+ return options;
+ },
+ setError,
+ [resourceUri]
+ );
+
+ return metricNamespaces;
+};
+
+export const useMetricNames: DataHook = (query, datasource, onChange, setError) => {
+ const { metricNamespace, metricName, resourceUri } = query.azureMonitor ?? {};
+
+ return useAsyncState(
+ async () => {
+ if (!(metricNamespace && resourceUri)) {
+ return;
+ }
+
+ const results = await datasource.azureMonitorDatasource.newGetMetricNames(resourceUri, metricNamespace);
+ const options = formatOptions(results, metricName);
+
+ return options;
+ },
+ setError,
+ [resourceUri, metricNamespace]
+ );
+};
+
+const defaultMetricMetadata: MetricMetadata = {
+ aggOptions: [],
+ timeGrains: [],
+ dimensions: [],
+ isLoading: false,
+ supportedAggTypes: [],
+ primaryAggType: undefined,
+};
+
+export const useMetricMetadata = (query: AzureMonitorQuery, datasource: Datasource, onChange: OnChangeFn) => {
+ const [metricMetadata, setMetricMetadata] = useState(defaultMetricMetadata);
+
+ const { resourceUri, metricNamespace, metricName, aggregation, timeGrain } = query.azureMonitor ?? {};
+
+ // Fetch new metric metadata when the fields change
+ useEffect(() => {
+ if (!(resourceUri && metricNamespace && metricName)) {
+ setMetricMetadata(defaultMetricMetadata);
+ return;
+ }
+
+ datasource.azureMonitorDatasource
+ .newGetMetricMetadata(resourceUri, metricNamespace, metricName)
+ .then((metadata) => {
+ // TODO: Move the aggregationTypes and timeGrain defaults into `getMetricMetadata`
+ const aggregations = (metadata.supportedAggTypes || [metadata.primaryAggType]).map((v) => ({
+ label: v,
+ value: v,
+ }));
+
+ setMetricMetadata({
+ aggOptions: aggregations,
+ timeGrains: metadata.supportedTimeGrains,
+ dimensions: metadata.dimensions,
+ isLoading: false,
+ supportedAggTypes: metadata.supportedAggTypes ?? [],
+ primaryAggType: metadata.primaryAggType,
+ });
+ });
+ }, [datasource, resourceUri, metricNamespace, metricName]);
+
+ // Update the query state in response to the meta data changing
+ useEffect(() => {
+ const newAggregation = aggregation || metricMetadata.primaryAggType;
+ const newTimeGrain = timeGrain || 'auto';
+
+ if (newAggregation !== aggregation || newTimeGrain !== timeGrain) {
+ onChange({
+ ...query,
+ azureMonitor: {
+ ...query.azureMonitor,
+ aggregation: newAggregation,
+ timeGrain: newTimeGrain,
+ },
+ });
+ }
+ }, [onChange, metricMetadata, aggregation, timeGrain, query]);
+
+ return metricMetadata;
+};
+
+function formatOptions(
+ rawResults: Array<{
+ text: string;
+ value: string;
+ }>,
+ selectedValue?: string
+) {
+ const options = rawResults.map(toOption);
+
+ // account for custom values that might have been set in json file like ones crafted with a template variable (ex: "cloud-datasource-resource-$Environment")
+ if (selectedValue && !options.find((option) => option.value === selectedValue)) {
+ options.push({ label: selectedValue, value: selectedValue });
+ }
+
+ return options;
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx
index 85382626a38..dbc618bc6bf 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx
@@ -99,7 +99,15 @@ const EditorForQueryType: React.FC = ({
switch (query.queryType) {
case AzureQueryType.AzureMonitor:
if (config.featureToggles.azureMonitorResourcePickerForMetrics) {
- return ;
+ return (
+
+ );
}
return (
= ({ query, datasource, onQueryChange }) => {
+interface ResourceFieldProps extends AzureQueryEditorFieldProps {
+ setResource: (query: AzureMonitorQuery, resourceURI?: string) => AzureMonitorQuery;
+ selectableEntryTypes: ResourceRowType[];
+ resourceUri?: string;
+}
+
+const ResourceField: React.FC = ({
+ query,
+ datasource,
+ onQueryChange,
+ setResource,
+ selectableEntryTypes,
+ resourceUri,
+}) => {
const styles = useStyles2(getStyles);
- const { resource } = query.azureLogAnalytics ?? {};
const [pickerIsOpen, setPickerIsOpen] = useState(false);
const handleOpenPicker = useCallback(() => {
@@ -44,7 +55,7 @@ const ResourceField: React.FC = ({ query, datasource
onQueryChange(setResource(query, resourceURI));
closePicker();
},
- [closePicker, onQueryChange, query]
+ [closePicker, onQueryChange, query, setResource]
);
return (
@@ -60,21 +71,16 @@ const ResourceField: React.FC = ({ query, datasource
>
-
+
>
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/index.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/index.tsx
new file mode 100644
index 00000000000..5e5d95b4814
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/index.tsx
@@ -0,0 +1 @@
+export { default } from './ResourceField';
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts
index 22c208d3cea..86c9a2df88c 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts
@@ -36,6 +36,7 @@ export interface AzureMonitorQuery extends DeprecatedAzureMonitorQuery {
* Azure Monitor Metrics sub-query properties
*/
export interface AzureMetricQuery {
+ resourceUri?: string;
resourceGroup?: string;
/** Resource type */
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts
index 4e91064b7e0..9a6666fcf77 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts
@@ -95,6 +95,23 @@ export interface AzureMonitorMetricMetadataItem {
metricAvailabilities?: AzureMonitorMetricAvailabilityMetadata[];
}
+export interface AzureMonitorMetricNamespacesResponse {
+ value: AzureMonitorMetricNamespaceItem[];
+}
+
+export interface AzureMonitorMetricNamespaceItem {
+ name: string;
+ properties: { metricNamespacename: string };
+}
+
+export interface AzureMonitorMetricNamesResponse {
+ value: AzureMonitorMetricNameItem[];
+}
+
+export interface AzureMonitorMetricNameItem {
+ name: { value: string; localizedValue: string };
+}
+
export interface AzureMonitorMetricAvailabilityMetadata {
timeGrain: string;
retention: string;
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useAsyncState.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useAsyncState.test.ts
new file mode 100644
index 00000000000..ddbfb2b5cc0
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useAsyncState.test.ts
@@ -0,0 +1,65 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { useAsyncState } from './useAsyncState';
+
+interface WaitableMock extends jest.Mock {
+ waitToBeCalled(): Promise;
+}
+
+function createWaitableMock() {
+ let resolve: Function;
+
+ const mock = jest.fn() as WaitableMock;
+ mock.mockImplementation(() => {
+ resolve && resolve();
+ });
+
+ mock.waitToBeCalled = () => {
+ return new Promise((_resolve) => (resolve = _resolve));
+ };
+
+ return mock;
+}
+
+describe('useAsyncState', () => {
+ const MOCKED_RANDOM_VALUE = 0.42069;
+
+ beforeEach(() => {
+ jest.spyOn(global.Math, 'random').mockReturnValue(MOCKED_RANDOM_VALUE);
+ });
+
+ afterEach(() => {
+ jest.spyOn(global.Math, 'random').mockRestore();
+ });
+
+ it('should return data from an async function', async () => {
+ const apiCall = () => Promise.resolve(['a', 'b', 'c']);
+ const setError = jest.fn();
+
+ const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
+ await waitForNextUpdate();
+
+ expect(result.current).toEqual(['a', 'b', 'c']);
+ });
+
+ it('should report errors through setError', async () => {
+ const error = new Error();
+ const apiCall = () => Promise.reject(error);
+ const setError = createWaitableMock();
+
+ const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
+ await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
+
+ expect(result.current).toEqual([]);
+ expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, error);
+ });
+
+ it('should clear the error once the request is successful', async () => {
+ const apiCall = () => Promise.resolve(['a', 'b', 'c']);
+ const setError = createWaitableMock();
+
+ const { waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
+ await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
+
+ expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, undefined);
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useAsyncState.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useAsyncState.ts
new file mode 100644
index 00000000000..fa84157bfc6
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useAsyncState.ts
@@ -0,0 +1,24 @@
+import { useEffect, useMemo, useState } from 'react';
+
+export function useAsyncState(asyncFn: () => Promise, setError: Function, dependencies: unknown[]) {
+ // Use the lazy initial state functionality of useState to assign a random ID to the API call
+ // to track where errors come from. See useLastError.
+ const [errorSource] = useState(() => Math.random());
+ const [value, setValue] = useState();
+
+ const finalValue = useMemo(() => value ?? [], [value]);
+
+ useEffect(() => {
+ asyncFn()
+ .then((results) => {
+ setValue(results);
+ setError(errorSource, undefined);
+ })
+ .catch((err) => {
+ setError(errorSource, err);
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, dependencies);
+
+ return finalValue;
+}