From bec500b69f2169703902b72fa6cbd5ac02418b40 Mon Sep 17 00:00:00 2001
From: Polina Boneva <13227501+polibb@users.noreply.github.com>
Date: Fri, 15 Jul 2022 12:06:16 +0300
Subject: [PATCH 001/116] Chore: Test
`grafana/public/app/plugins/panel/text/TextPanel.tsx` (#52244)
* move sanitize test to its own test file
* add a test for renderTextPanelMarkdown to always sanitize
* setup TextPanel tests
* add tests to always sanitize Text Panel contents and always convert correctly to html/markdown
---
.../grafana-data/src/text/markdown.test.ts | 13 +-
.../grafana-data/src/text/sanitize.test.ts | 12 ++
.../app/plugins/panel/text/TextPanel.test.tsx | 120 ++++++++++++++++++
public/app/plugins/panel/text/TextPanel.tsx | 8 +-
4 files changed, 142 insertions(+), 11 deletions(-)
create mode 100644 packages/grafana-data/src/text/sanitize.test.ts
create mode 100644 public/app/plugins/panel/text/TextPanel.test.tsx
diff --git a/packages/grafana-data/src/text/markdown.test.ts b/packages/grafana-data/src/text/markdown.test.ts
index b9e7052c3ef..20a5bab4611 100644
--- a/packages/grafana-data/src/text/markdown.test.ts
+++ b/packages/grafana-data/src/text/markdown.test.ts
@@ -1,5 +1,4 @@
-import { renderMarkdown } from './markdown';
-import { sanitizeTextPanelContent } from './sanitize';
+import { renderMarkdown, renderTextPanelMarkdown } from './markdown';
describe('Markdown wrapper', () => {
it('should be able to handle undefined value', () => {
@@ -12,12 +11,8 @@ describe('Markdown wrapper', () => {
expect(str).toBe('<script>alert()</script>');
});
- it('should allow whitelisted styles in text panel', () => {
- const html =
- '
';
- const str = sanitizeTextPanelContent(html);
- expect(str).toBe(
- ''
- );
+ it('should sanitize content in text panel by default', () => {
+ const str = renderTextPanelMarkdown('');
+ expect(str).toBe('<script>alert()</script>');
});
});
diff --git a/packages/grafana-data/src/text/sanitize.test.ts b/packages/grafana-data/src/text/sanitize.test.ts
new file mode 100644
index 00000000000..8d9ff47d4d9
--- /dev/null
+++ b/packages/grafana-data/src/text/sanitize.test.ts
@@ -0,0 +1,12 @@
+import { sanitizeTextPanelContent } from './sanitize';
+
+describe('Sanitize wrapper', () => {
+ it('should allow whitelisted styles in text panel', () => {
+ const html =
+ '';
+ const str = sanitizeTextPanelContent(html);
+ expect(str).toBe(
+ ''
+ );
+ });
+});
diff --git a/public/app/plugins/panel/text/TextPanel.test.tsx b/public/app/plugins/panel/text/TextPanel.test.tsx
new file mode 100644
index 00000000000..99a245dff33
--- /dev/null
+++ b/public/app/plugins/panel/text/TextPanel.test.tsx
@@ -0,0 +1,120 @@
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+
+import { dateTime, LoadingState, EventBusSrv } from '@grafana/data';
+
+import { Props, TextPanel } from './TextPanel';
+import { TextMode } from './models.gen';
+
+const replaceVariablesMock = jest.fn();
+const defaultProps: Props = {
+ id: 1,
+ data: {
+ state: LoadingState.Done,
+ series: [
+ {
+ fields: [],
+ length: 0,
+ },
+ ],
+ timeRange: {
+ from: dateTime('2022-01-01T15:55:00Z'),
+ to: dateTime('2022-07-12T15:55:00Z'),
+ raw: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ },
+ },
+ timeRange: {
+ from: dateTime('2022-07-11T15:55:00Z'),
+ to: dateTime('2022-07-12T15:55:00Z'),
+ raw: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ },
+ timeZone: 'utc',
+ transparent: false,
+ width: 120,
+ height: 120,
+ fieldConfig: {
+ defaults: {},
+ overrides: [],
+ },
+ renderCounter: 1,
+ title: 'Test Text Panel',
+ eventBus: new EventBusSrv(),
+ options: { content: '', mode: TextMode.Markdown },
+ onOptionsChange: jest.fn(),
+ onFieldConfigChange: jest.fn(),
+ replaceVariables: replaceVariablesMock,
+ onChangeTimeRange: jest.fn(),
+};
+
+const setup = (props: Props = defaultProps) => {
+ render();
+};
+
+describe('TextPanel', () => {
+ it('should render panel without content', () => {
+ expect(() => setup()).not.toThrow();
+ });
+
+ it('sanitizes content in html mode', () => {
+ const contentTest = '\n';
+ replaceVariablesMock.mockReturnValueOnce(contentTest);
+ const props = Object.assign({}, defaultProps, {
+ options: { content: contentTest, mode: TextMode.HTML },
+ });
+
+ setup(props);
+
+ expect(screen.getByTestId('TextPanel-converted-content').innerHTML).toEqual(
+ '<form>Form tags are sanitized.
</form>\n<script>Script tags are sanitized.</script>'
+ );
+ });
+
+ it('sanitizes content in markdown mode', () => {
+ const contentTest = '\n';
+ replaceVariablesMock.mockReturnValueOnce(contentTest);
+
+ const props = Object.assign({}, defaultProps, {
+ options: { content: contentTest, mode: TextMode.Markdown },
+ });
+
+ setup(props);
+
+ expect(screen.getByTestId('TextPanel-converted-content').innerHTML).toEqual(
+ '<form>Form tags are sanitized.
</form>\n<script>Script tags are sanitized.</script>'
+ );
+ });
+
+ it('converts content to markdown when in markdown mode', async () => {
+ const contentTest = 'We begin by a simple sentence.\n```code block```';
+ replaceVariablesMock.mockReturnValueOnce(contentTest);
+
+ const props = Object.assign({}, defaultProps, {
+ options: { content: contentTest, mode: TextMode.Markdown },
+ });
+
+ setup(props);
+
+ const waited = await screen.getByTestId('TextPanel-converted-content');
+ expect(waited.innerHTML).toEqual('We begin by a simple sentence.\ncode block
\n');
+ });
+
+ it('converts content to html when in html mode', () => {
+ const contentTest = 'We begin by a simple sentence.\n```This is a code block\n```';
+ replaceVariablesMock.mockReturnValueOnce(contentTest);
+ const props = Object.assign({}, defaultProps, {
+ options: { content: contentTest, mode: TextMode.HTML },
+ });
+
+ setup(props);
+
+ expect(screen.getByTestId('TextPanel-converted-content').innerHTML).toEqual(
+ 'We begin by a simple sentence.\n```This is a code block\n```'
+ );
+ });
+});
diff --git a/public/app/plugins/panel/text/TextPanel.tsx b/public/app/plugins/panel/text/TextPanel.tsx
index c30ba779ec9..8494f3a76c8 100644
--- a/public/app/plugins/panel/text/TextPanel.tsx
+++ b/public/app/plugins/panel/text/TextPanel.tsx
@@ -12,7 +12,7 @@ import config from 'app/core/config';
// Types
import { PanelOptions, TextMode } from './models.gen';
-interface Props extends PanelProps {}
+export interface Props extends PanelProps {}
interface State {
html: string;
@@ -90,7 +90,11 @@ export class TextPanel extends PureComponent {
const styles = getStyles();
return (
-
+
);
}
From f3ee57abefe14962a50a3cd2e462f21fac1e735f Mon Sep 17 00:00:00 2001
From: Jo
Date: Fri, 15 Jul 2022 09:21:09 +0000
Subject: [PATCH 002/116] Fix: Choose Lookup params per auth module (#395)
(#52312)
Co-authored-by: Karl Persson
Fix: Prefer pointer to struct in lookup
Co-authored-by: Karl Persson
Fix: user email for ldap
Co-authored-by: Karl Persson
Fix: Use only login for lookup in LDAP
Co-authored-by: Karl Persson
Fix: use user email for ldap
Co-authored-by: Karl Persson
fix remaining test
fix nit picks
---
pkg/api/ldap_debug.go | 5 ++
pkg/api/login_oauth.go | 5 ++
pkg/api/user_test.go | 3 +-
pkg/login/ldap_login.go | 8 ++-
pkg/models/user_auth.go | 16 ++++--
pkg/services/contexthandler/auth_jwt.go | 5 ++
.../contexthandler/authproxy/authproxy.go | 10 ++++
pkg/services/login/authinfoservice/service.go | 25 +++++-----
.../login/authinfoservice/user_auth_test.go | 49 +++++++++++++------
.../login/loginservice/loginservice.go | 8 ++-
.../login/loginservice/loginservice_test.go | 6 ++-
pkg/services/login/logintest/logintest.go | 6 ++-
12 files changed, 104 insertions(+), 42 deletions(-)
diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go
index cc4ef546405..48015002395 100644
--- a/pkg/api/ldap_debug.go
+++ b/pkg/api/ldap_debug.go
@@ -220,6 +220,11 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon
ReqContext: c,
ExternalUser: user,
SignupAllowed: hs.Cfg.LDAPAllowSignup,
+ UserLookupParams: models.UserLookupParams{
+ UserID: &query.Result.ID, // Upsert by ID only
+ Email: nil,
+ Login: nil,
+ },
}
err = hs.Login.UpsertUser(c.Req.Context(), upsertCmd)
diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go
index 7beff805990..8495f08de71 100644
--- a/pkg/api/login_oauth.go
+++ b/pkg/api/login_oauth.go
@@ -305,6 +305,11 @@ func (hs *HTTPServer) SyncUser(
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: connect.IsSignupAllowed(),
+ UserLookupParams: models.UserLookupParams{
+ Email: &extUser.Email,
+ UserID: nil,
+ Login: nil,
+ },
}
if err := hs.Login.UpsertUser(ctx.Req.Context(), cmd); err != nil {
diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go
index bdd5cd6e071..c7ec173f524 100644
--- a/pkg/api/user_test.go
+++ b/pkg/api/user_test.go
@@ -76,7 +76,8 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
}
idToken := "testidtoken"
token = token.WithExtra(map[string]interface{}{"id_token": idToken})
- query := &models.GetUserByAuthInfoQuery{Login: "loginuser", AuthModule: "test", AuthId: "test"}
+ login := "loginuser"
+ query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: models.UserLookupParams{Login: &login}}
cmd := &models.UpdateAuthInfoCommand{
UserId: user.ID,
AuthId: query.AuthId,
diff --git a/pkg/login/ldap_login.go b/pkg/login/ldap_login.go
index 23d77224423..4e06ddc72ce 100644
--- a/pkg/login/ldap_login.go
+++ b/pkg/login/ldap_login.go
@@ -57,9 +57,13 @@ var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, log
ReqContext: query.ReqContext,
ExternalUser: externalUser,
SignupAllowed: setting.LDAPAllowSignup,
+ UserLookupParams: models.UserLookupParams{
+ Login: &externalUser.Login,
+ Email: &externalUser.Email,
+ UserID: nil,
+ },
}
- err = loginService.UpsertUser(ctx, upsert)
- if err != nil {
+ if err = loginService.UpsertUser(ctx, upsert); err != nil {
return true, err
}
query.User = upsert.Result
diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go
index c9bd9ab0859..0732c81f47d 100644
--- a/pkg/models/user_auth.go
+++ b/pkg/models/user_auth.go
@@ -57,8 +57,9 @@ type RequestURIKey struct{}
// COMMANDS
type UpsertUserCommand struct {
- ReqContext *ReqContext
- ExternalUser *ExternalUserInfo
+ ReqContext *ReqContext
+ ExternalUser *ExternalUserInfo
+ UserLookupParams
SignupAllowed bool
Result *user.User
@@ -98,9 +99,14 @@ type LoginUserQuery struct {
type GetUserByAuthInfoQuery struct {
AuthModule string
AuthId string
- UserId int64
- Email string
- Login string
+ UserLookupParams
+}
+
+type UserLookupParams struct {
+ // Describes lookup order as well
+ UserID *int64 // if set, will try to find the user by id
+ Email *string // if set, will try to find the user by email
+ Login *string // if set, will try to find the user by login
}
type GetExternalUserInfoByLoginQuery struct {
diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go
index f0bddf8d6f2..1b1856426ba 100644
--- a/pkg/services/contexthandler/auth_jwt.go
+++ b/pkg/services/contexthandler/auth_jwt.go
@@ -66,6 +66,11 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
ReqContext: ctx,
SignupAllowed: h.Cfg.JWTAuthAutoSignUp,
ExternalUser: extUser,
+ UserLookupParams: models.UserLookupParams{
+ UserID: nil,
+ Login: &query.Login,
+ Email: &query.Email,
+ },
}
if err := h.loginService.UpsertUser(ctx.Req.Context(), upsert); err != nil {
ctx.Logger.Error("Failed to upsert JWT user", "error", err)
diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go
index 79eb395b004..b9c8cefd89a 100644
--- a/pkg/services/contexthandler/authproxy/authproxy.go
+++ b/pkg/services/contexthandler/authproxy/authproxy.go
@@ -241,6 +241,11 @@ func (auth *AuthProxy) LoginViaLDAP(reqCtx *models.ReqContext) (int64, error) {
ReqContext: reqCtx,
SignupAllowed: auth.cfg.LDAPAllowSignup,
ExternalUser: extUser,
+ UserLookupParams: models.UserLookupParams{
+ Login: &extUser.Login,
+ Email: &extUser.Email,
+ UserID: nil,
+ },
}
if err := auth.loginService.UpsertUser(reqCtx.Req.Context(), upsert); err != nil {
return 0, err
@@ -298,6 +303,11 @@ func (auth *AuthProxy) loginViaHeader(reqCtx *models.ReqContext) (int64, error)
ReqContext: reqCtx,
SignupAllowed: auth.cfg.AuthProxyAutoSignUp,
ExternalUser: extUser,
+ UserLookupParams: models.UserLookupParams{
+ UserID: nil,
+ Login: &extUser.Login,
+ Email: &extUser.Email,
+ },
}
err := auth.loginService.UpsertUser(reqCtx.Req.Context(), upsert)
diff --git a/pkg/services/login/authinfoservice/service.go b/pkg/services/login/authinfoservice/service.go
index 1e8c79bc39a..ddcc1385352 100644
--- a/pkg/services/login/authinfoservice/service.go
+++ b/pkg/services/login/authinfoservice/service.go
@@ -44,11 +44,12 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser
}
// if user id was specified and doesn't match the user_auth entry, remove it
- if query.UserId != 0 && query.UserId != authQuery.Result.UserId {
- err := s.authInfoStore.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{
+ if query.UserLookupParams.UserID != nil &&
+ *query.UserLookupParams.UserID != 0 &&
+ *query.UserLookupParams.UserID != authQuery.Result.UserId {
+ if err := s.authInfoStore.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{
UserAuth: authQuery.Result,
- })
- if err != nil {
+ }); err != nil {
s.logger.Error("Error removing user_auth entry", "error", err)
}
@@ -78,29 +79,29 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser
return false, nil, nil, models.ErrUserNotFound
}
-func (s *Implementation) LookupByOneOf(ctx context.Context, userId int64, email string, login string) (*user.User, error) {
+func (s *Implementation) LookupByOneOf(ctx context.Context, params *models.UserLookupParams) (*user.User, error) {
var user *user.User
var err error
// If not found, try to find the user by id
- if userId != 0 {
- user, err = s.authInfoStore.GetUserById(ctx, userId)
+ if params.UserID != nil && *params.UserID != 0 {
+ user, err = s.authInfoStore.GetUserById(ctx, *params.UserID)
if err != nil && !errors.Is(err, models.ErrUserNotFound) {
return nil, err
}
}
// If not found, try to find the user by email address
- if user == nil && email != "" {
- user, err = s.authInfoStore.GetUserByEmail(ctx, email)
+ if user == nil && params.Email != nil && *params.Email != "" {
+ user, err = s.authInfoStore.GetUserByEmail(ctx, *params.Email)
if err != nil && !errors.Is(err, models.ErrUserNotFound) {
return nil, err
}
}
// If not found, try to find the user by login
- if user == nil && login != "" {
- user, err = s.authInfoStore.GetUserByLogin(ctx, login)
+ if user == nil && params.Login != nil && *params.Login != "" {
+ user, err = s.authInfoStore.GetUserByLogin(ctx, *params.Login)
if err != nil && !errors.Is(err, models.ErrUserNotFound) {
return nil, err
}
@@ -139,7 +140,7 @@ func (s *Implementation) LookupAndUpdate(ctx context.Context, query *models.GetU
// 2. FindByUserDetails
if !foundUser {
- user, err = s.LookupByOneOf(ctx, query.UserId, query.Email, query.Login)
+ user, err = s.LookupByOneOf(ctx, &query.UserLookupParams)
if err != nil {
return nil, err
}
diff --git a/pkg/services/login/authinfoservice/user_auth_test.go b/pkg/services/login/authinfoservice/user_auth_test.go
index 0c96d819e7b..c67ea0f5b4d 100644
--- a/pkg/services/login/authinfoservice/user_auth_test.go
+++ b/pkg/services/login/authinfoservice/user_auth_test.go
@@ -43,7 +43,7 @@ func TestUserAuth(t *testing.T) {
// By Login
login := "loginuser0"
- query := &models.GetUserByAuthInfoQuery{Login: login}
+ query := &models.GetUserByAuthInfoQuery{UserLookupParams: models.UserLookupParams{Login: &login}}
user, err := srv.LookupAndUpdate(context.Background(), query)
require.Nil(t, err)
@@ -52,7 +52,9 @@ func TestUserAuth(t *testing.T) {
// By ID
id := user.ID
- user, err = srv.LookupByOneOf(context.Background(), id, "", "")
+ user, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{
+ UserID: &id,
+ })
require.Nil(t, err)
require.Equal(t, user.ID, id)
@@ -60,7 +62,9 @@ func TestUserAuth(t *testing.T) {
// By Email
email := "user1@test.com"
- user, err = srv.LookupByOneOf(context.Background(), 0, email, "")
+ user, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{
+ Email: &email,
+ })
require.Nil(t, err)
require.Equal(t, user.Email, email)
@@ -68,7 +72,9 @@ func TestUserAuth(t *testing.T) {
// Don't find nonexistent user
email = "nonexistent@test.com"
- user, err = srv.LookupByOneOf(context.Background(), 0, email, "")
+ user, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{
+ Email: &email,
+ })
require.Equal(t, models.ErrUserNotFound, err)
require.Nil(t, user)
@@ -85,7 +91,7 @@ func TestUserAuth(t *testing.T) {
// create user_auth entry
login := "loginuser0"
- query.Login = login
+ query.UserLookupParams.Login = &login
user, err = srv.LookupAndUpdate(context.Background(), query)
require.Nil(t, err)
@@ -99,9 +105,9 @@ func TestUserAuth(t *testing.T) {
require.Equal(t, user.Login, login)
// get with non-matching id
- id := user.ID
+ idPlusOne := user.ID + 1
- query.UserId = id + 1
+ query.UserLookupParams.UserID = &idPlusOne
user, err = srv.LookupAndUpdate(context.Background(), query)
require.Nil(t, err)
@@ -143,7 +149,9 @@ func TestUserAuth(t *testing.T) {
login := "loginuser0"
// Calling GetUserByAuthInfoQuery on an existing user will populate an entry in the user_auth table
- query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test", AuthId: "test"}
+ query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: models.UserLookupParams{
+ Login: &login,
+ }}
user, err := srv.LookupAndUpdate(context.Background(), query)
require.Nil(t, err)
@@ -192,7 +200,9 @@ func TestUserAuth(t *testing.T) {
// Calling srv.LookupAndUpdateQuery on an existing user will populate an entry in the user_auth table
// Make the first log-in during the past
database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
- query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test1", AuthId: "test1"}
+ query := &models.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: models.UserLookupParams{
+ Login: &login,
+ }}
user, err := srv.LookupAndUpdate(context.Background(), query)
database.GetTime = time.Now
@@ -202,7 +212,9 @@ func TestUserAuth(t *testing.T) {
// Add a second auth module for this user
// Have this module's last log-in be more recent
database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -1) }
- query = &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test2", AuthId: "test2"}
+ query = &models.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: models.UserLookupParams{
+ Login: &login,
+ }}
user, err = srv.LookupAndUpdate(context.Background(), query)
database.GetTime = time.Now
@@ -257,7 +269,9 @@ func TestUserAuth(t *testing.T) {
// Calling srv.LookupAndUpdateQuery on an existing user will populate an entry in the user_auth table
// Make the first log-in during the past
database.GetTime = func() time.Time { return fixedTime.AddDate(0, 0, -2) }
- queryOne := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test1", AuthId: "test1"}
+ queryOne := &models.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: models.UserLookupParams{
+ Login: &login,
+ }}
user, err := srv.LookupAndUpdate(context.Background(), queryOne)
database.GetTime = time.Now
@@ -267,7 +281,9 @@ func TestUserAuth(t *testing.T) {
// Add a second auth module for this user
// Have this module's last log-in be more recent
database.GetTime = func() time.Time { return fixedTime.AddDate(0, 0, -1) }
- queryTwo := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test2", AuthId: "test2"}
+ queryTwo := &models.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: models.UserLookupParams{
+ Login: &login,
+ }}
user, err = srv.LookupAndUpdate(context.Background(), queryTwo)
require.Nil(t, err)
require.Equal(t, user.Login, login)
@@ -333,16 +349,21 @@ func TestUserAuth(t *testing.T) {
// Expect to pass since there's a matching login user
database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
- query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: genericOAuthModule, AuthId: ""}
+ query := &models.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: models.UserLookupParams{
+ Login: &login,
+ }}
user, err := srv.LookupAndUpdate(context.Background(), query)
database.GetTime = time.Now
require.Nil(t, err)
require.Equal(t, user.Login, login)
+ otherLoginUser := "aloginuser"
// Should throw a "user not found" error since there's no matching login user
database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
- query = &models.GetUserByAuthInfoQuery{Login: "aloginuser", AuthModule: genericOAuthModule, AuthId: ""}
+ query = &models.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: models.UserLookupParams{
+ Login: &otherLoginUser,
+ }}
user, err = srv.LookupAndUpdate(context.Background(), query)
database.GetTime = time.Now
diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go
index f49532c64d0..b138c6e5393 100644
--- a/pkg/services/login/loginservice/loginservice.go
+++ b/pkg/services/login/loginservice/loginservice.go
@@ -49,11 +49,9 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser
extUser := cmd.ExternalUser
usr, err := ls.AuthInfoService.LookupAndUpdate(ctx, &models.GetUserByAuthInfoQuery{
- AuthModule: extUser.AuthModule,
- AuthId: extUser.AuthId,
- UserId: extUser.UserId,
- Email: extUser.Email,
- Login: extUser.Login,
+ AuthModule: extUser.AuthModule,
+ AuthId: extUser.AuthId,
+ UserLookupParams: cmd.UserLookupParams,
})
if err != nil {
if !errors.Is(err, models.ErrUserNotFound) {
diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go
index dd9328b2d91..1bd5be21c7b 100644
--- a/pkg/services/login/loginservice/loginservice_test.go
+++ b/pkg/services/login/loginservice/loginservice_test.go
@@ -69,10 +69,12 @@ func Test_teamSync(t *testing.T) {
AuthInfoService: authInfoMock,
}
- upserCmd := &models.UpsertUserCommand{ExternalUser: &models.ExternalUserInfo{Email: "test_user@example.org"}}
+ email := "test_user@example.org"
+ upserCmd := &models.UpsertUserCommand{ExternalUser: &models.ExternalUserInfo{Email: email},
+ UserLookupParams: models.UserLookupParams{Email: &email}}
expectedUser := &user.User{
ID: 1,
- Email: "test_user@example.org",
+ Email: email,
Name: "test_user",
Login: "test_user",
}
diff --git a/pkg/services/login/logintest/logintest.go b/pkg/services/login/logintest/logintest.go
index 16691823323..d4a9e37c3c6 100644
--- a/pkg/services/login/logintest/logintest.go
+++ b/pkg/services/login/logintest/logintest.go
@@ -29,7 +29,11 @@ type AuthInfoServiceFake struct {
}
func (a *AuthInfoServiceFake) LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) {
- a.LatestUserID = query.UserId
+ if query.UserLookupParams.UserID != nil {
+ a.LatestUserID = *query.UserLookupParams.UserID
+ } else {
+ a.LatestUserID = 0
+ }
return a.ExpectedUser, a.ExpectedError
}
From 1c48f443f06aabaea2845d0b333f7f263d095d0b Mon Sep 17 00:00:00 2001
From: Andreas Christou
Date: Fri, 15 Jul 2022 10:46:30 +0100
Subject: [PATCH 003/116] Upgrade grafana-azure-sdk-go package (#52248)
- Includes fix for appropriate selection of system assigned identity when using managed identity credential for Azure Monitor auth
---
go.mod | 2 +-
go.sum | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/go.mod b/go.mod
index 78270aecc63..35ad2f3d040 100644
--- a/go.mod
+++ b/go.mod
@@ -53,7 +53,7 @@ require (
github.com/gosimple/slug v1.12.0
github.com/grafana/cuetsy v0.0.3
github.com/grafana/grafana-aws-sdk v0.10.7
- github.com/grafana/grafana-azure-sdk-go v1.2.0
+ github.com/grafana/grafana-azure-sdk-go v1.3.0
github.com/grafana/grafana-plugin-sdk-go v0.138.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/hashicorp/go-hclog v1.0.0
diff --git a/go.sum b/go.sum
index df4a323117f..94fa610c55f 100644
--- a/go.sum
+++ b/go.sum
@@ -1338,6 +1338,8 @@ github.com/grafana/grafana-aws-sdk v0.10.7 h1:kXOuWCI+fV561/9sOU0DnzlFwqblfW36Xp
github.com/grafana/grafana-aws-sdk v0.10.7/go.mod h1:5Iw3xY7iXJfNaYHrRHMXa/kaB2lWoyntg71PPLGvSs8=
github.com/grafana/grafana-azure-sdk-go v1.2.0 h1:f/7BjCHGIU0JYOsLIt4oJztDy0fOPBRHB5R0Xe9++ew=
github.com/grafana/grafana-azure-sdk-go v1.2.0/go.mod h1:rgrnK9m6CgKlgx4rH3FFP/6dTdyRO6LYC2mVZov35yo=
+github.com/grafana/grafana-azure-sdk-go v1.3.0 h1:zboQpq/ljBjqHo/6UQNZAUwqGTtnEGRYSEnqIQvLuAo=
+github.com/grafana/grafana-azure-sdk-go v1.3.0/go.mod h1:rgrnK9m6CgKlgx4rH3FFP/6dTdyRO6LYC2mVZov35yo=
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2ud7NNM7LrGPO4x0NFR8qLq68CqI4SmB7I2yRN2w9oE=
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
From 91fd0223a4e1211ba383b33044ada4df11ebd560 Mon Sep 17 00:00:00 2001
From: George Robinson
Date: Fri, 15 Jul 2022 10:48:52 +0100
Subject: [PATCH 004/116] Datasources: Allow configuration of the TTL (#52161)
---
pkg/services/datasources/service/cache_service.go | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/pkg/services/datasources/service/cache_service.go b/pkg/services/datasources/service/cache_service.go
index bcfcac1315b..38327f7127f 100644
--- a/pkg/services/datasources/service/cache_service.go
+++ b/pkg/services/datasources/service/cache_service.go
@@ -12,9 +12,14 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore"
)
+const (
+ DefaultCacheTTL = 5 * time.Second
+)
+
func ProvideCacheService(cacheService *localcache.CacheService, sqlStore *sqlstore.SQLStore) *CacheServiceImpl {
return &CacheServiceImpl{
logger: log.New("datasources"),
+ cacheTTL: DefaultCacheTTL,
CacheService: cacheService,
SQLStore: sqlStore,
}
@@ -22,6 +27,7 @@ func ProvideCacheService(cacheService *localcache.CacheService, sqlStore *sqlsto
type CacheServiceImpl struct {
logger log.Logger
+ cacheTTL time.Duration
CacheService *localcache.CacheService
SQLStore *sqlstore.SQLStore
}
@@ -56,7 +62,7 @@ func (dc *CacheServiceImpl) GetDatasource(
if ds.Uid != "" {
dc.CacheService.Set(uidKey(ds.OrgId, ds.Uid), ds, time.Second*5)
}
- dc.CacheService.Set(cacheKey, ds, time.Second*5)
+ dc.CacheService.Set(cacheKey, ds, dc.cacheTTL)
return ds, nil
}
@@ -92,8 +98,8 @@ func (dc *CacheServiceImpl) GetDatasourceByUID(
ds := query.Result
- dc.CacheService.Set(uidCacheKey, ds, time.Second*5)
- dc.CacheService.Set(idKey(ds.Id), ds, time.Second*5)
+ dc.CacheService.Set(uidCacheKey, ds, dc.cacheTTL)
+ dc.CacheService.Set(idKey(ds.Id), ds, dc.cacheTTL)
return ds, nil
}
From 8fc51932f5e3066c2838d25cfd97cd6247da8d28 Mon Sep 17 00:00:00 2001
From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
Date: Fri, 15 Jul 2022 13:03:14 +0200
Subject: [PATCH 005/116] Loki: Fix incorrect TopK value type in query builder
(#52226)
* Loki: Fix incorrect TopK value type in query builder
* Simplify code
* Remove bracket
* Brackets are back
---
.../shared/operationUtils.test.ts | 39 +++++++++++++++++++
.../querybuilder/shared/operationUtils.ts | 16 ++++----
2 files changed, 46 insertions(+), 9 deletions(-)
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.test.ts
index 3b6f532237e..72a9c178545 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.test.ts
+++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.test.ts
@@ -124,4 +124,43 @@ describe('createAggregationOperationWithParams', () => {
},
]);
});
+ it('returns correct query string using aggregation definitions with overrides and number type param', () => {
+ const def = createAggregationOperationWithParam(
+ 'test_aggregation',
+ {
+ params: [{ name: 'K-value', type: 'number' }],
+ defaultParams: [5],
+ },
+ { category: 'test_category' }
+ );
+
+ const topKByDefinition = def[1];
+ expect(
+ topKByDefinition.renderer(
+ { id: '__topk_by', params: ['5', 'source', 'place'] },
+ def[1],
+ 'rate({place="luna"} |= `` [5m])'
+ )
+ ).toBe('test_aggregation by(source, place) (5, rate({place="luna"} |= `` [5m]))');
+ });
+
+ it('returns correct query string using aggregation definitions with overrides and string type param', () => {
+ const def = createAggregationOperationWithParam(
+ 'test_aggregation',
+ {
+ params: [{ name: 'Identifier', type: 'string' }],
+ defaultParams: ['count'],
+ },
+ { category: 'test_category' }
+ );
+
+ const countValueDefinition = def[1];
+ expect(
+ countValueDefinition.renderer(
+ { id: 'count_values', params: ['5', 'source', 'place'] },
+ def[1],
+ 'rate({place="luna"} |= `` [5m])'
+ )
+ ).toBe('test_aggregation by(source, place) ("5", rate({place="luna"} |= `` [5m]))');
+ });
});
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts
index ef6689763d3..024c441bc47 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts
+++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts
@@ -279,15 +279,13 @@ function getAggregationExplainer(aggregationName: string, mode: 'by' | 'without'
function getAggregationByRendererWithParameter(aggregation: string) {
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
- function mapType(p: QueryBuilderOperationParamValue) {
- if (typeof p === 'string') {
- return `\"${p}\"`;
- }
- return p;
- }
- const params = model.params.slice(0, -1);
- const restParams = model.params.slice(1);
- return `${aggregation} by(${restParams.join(', ')}) (${params.map(mapType).join(', ')}, ${innerExpr})`;
+ const restParamIndex = def.params.findIndex((param) => param.restParam);
+ const params = model.params.slice(0, restParamIndex);
+ const restParams = model.params.slice(restParamIndex);
+
+ return `${aggregation} by(${restParams.join(', ')}) (${params
+ .map((param, idx) => (def.params[idx].type === 'string' ? `\"${param}\"` : param))
+ .join(', ')}, ${innerExpr})`;
};
}
From 10b9830cece0cb8745b0555482a02580c40a6bd4 Mon Sep 17 00:00:00 2001
From: Andres Martinez Gotor
Date: Fri, 15 Jul 2022 13:10:03 +0200
Subject: [PATCH 006/116] Azure Monitor: Add template variables for namespaces
and resource names (#52247)
---
.../azure_monitor_datasource.test.ts | 41 +++++++--
.../azure_monitor/azure_monitor_datasource.ts | 20 +++--
.../azure_monitor/response_parser.ts | 4 +-
.../azure_monitor/url_builder.test.ts | 8 +-
.../azure_monitor/url_builder.ts | 2 +-
.../VariableEditor/VariableEditor.test.tsx | 78 +++++++++++++----
.../VariableEditor/VariableEditor.tsx | 84 +++++++++++++++++--
.../datasource.ts | 10 ++-
.../types/query.ts | 6 ++
.../variables.test.ts | 46 ++++++++++
.../variables.ts | 20 ++++-
11 files changed, 278 insertions(+), 41 deletions(-)
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 979e3112d79..3f638ba2ef9 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
@@ -64,7 +64,7 @@ describe('AzureMonitorDatasource', () => {
const expected =
basePath +
'/providers/microsoft.insights/components/resource1' +
- '/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview';
+ '/providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-12-01-preview';
expect(path).toBe(expected);
return Promise.resolve(response);
});
@@ -80,7 +80,7 @@ describe('AzureMonitorDatasource', () => {
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].text).toEqual('microsoft.insights/components');
expect(results[1].value).toEqual('microsoft.insights/components');
});
});
@@ -405,7 +405,7 @@ describe('AzureMonitorDatasource', () => {
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`
+ `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricDefinition}'`
);
return Promise.resolve(response);
});
@@ -456,7 +456,7 @@ describe('AzureMonitorDatasource', () => {
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
expect(path).toBe(
basePath +
- `/${resourceGroup}/resources?$filter=resourceType eq '${validMetricDefinition}'&api-version=2021-04-01`
+ `/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${validMetricDefinition}'`
);
return Promise.resolve(response);
});
@@ -467,7 +467,7 @@ describe('AzureMonitorDatasource', () => {
expect(results[0].text).toEqual('storagetest/default');
expect(results[0].value).toEqual('storagetest/default');
expect(ctx.ds.azureMonitorDatasource.getResource).toHaveBeenCalledWith(
- `azuremonitor/subscriptions/${subscription}/resourceGroups/${resourceGroup}/resources?$filter=resourceType eq '${validMetricDefinition}'&api-version=2021-04-01`
+ `azuremonitor/subscriptions/${subscription}/resourceGroups/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${validMetricDefinition}'`
);
});
});
@@ -497,7 +497,7 @@ describe('AzureMonitorDatasource', () => {
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`;
+ const expectedPath = `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricDefinition}'`;
// first page
fn.mockImplementationOnce((path: string) => {
expect(path).toBe(expectedPath);
@@ -520,6 +520,35 @@ describe('AzureMonitorDatasource', () => {
});
});
});
+
+ describe('without a resource group or a metric definition', () => {
+ 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}/resources?api-version=2021-04-01`;
+ expect(path).toBe(basePath);
+ return Promise.resolve(response);
+ });
+ });
+
+ it('should return list of Resource Names', () => {
+ return ctx.ds.getResourceNames(subscription).then((results: Array<{ text: string; value: string }>) => {
+ expect(results.length).toEqual(2);
+ });
+ });
+ });
});
describe('When performing getMetricNames', () => {
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 f1c06ce1a70..125a83a74b9 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
@@ -204,14 +204,18 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend {
- return ResponseParser.parseResponseValues(result, 'name', 'properties.metricNamespaceName');
+ return ResponseParser.parseResponseValues(
+ result,
+ 'properties.metricNamespaceName',
+ 'properties.metricNamespaceName'
+ );
})
.then((result) => {
if (url.includes('Microsoft.Storage/storageAccounts')) {
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts
index 640a127591e..121cf8f54fa 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts
@@ -33,7 +33,7 @@ export default class ResponseParser {
return list;
}
- static parseResourceNames(result: any, metricDefinition: string): Array<{ text: string; value: string }> {
+ static parseResourceNames(result: any, metricDefinition?: string): Array<{ text: string; value: string }> {
const list: Array<{ text: string; value: string }> = [];
if (!result) {
@@ -43,7 +43,7 @@ export default class ResponseParser {
for (let i = 0; i < result.value.length; i++) {
if (
typeof result.value[i].type === 'string' &&
- result.value[i].type.toLocaleLowerCase() === metricDefinition.toLocaleLowerCase()
+ (!metricDefinition || result.value[i].type.toLocaleLowerCase() === metricDefinition.toLocaleLowerCase())
) {
list.push({
text: result.value[i].name,
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 a7103ef1046..93f1b68cca3 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
@@ -92,7 +92,7 @@ describe('AzureMonitorUrlBuilder', () => {
templateSrv
);
expect(url).toBe(
- '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
+ '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-05-01-preview'
);
});
});
@@ -130,7 +130,7 @@ describe('AzureMonitorUrlBuilder', () => {
);
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'
+ 'providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-05-01-preview'
);
});
});
@@ -150,7 +150,7 @@ describe('AzureMonitorUrlBuilder', () => {
);
expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
- 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
+ 'providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-05-01-preview'
);
});
});
@@ -170,7 +170,7 @@ describe('AzureMonitorUrlBuilder', () => {
);
expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
- 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
+ 'providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-05-01-preview'
);
});
});
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 d52df58b96e..b2a9b1212bb 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
@@ -57,7 +57,7 @@ export default class UrlBuilder {
);
}
- return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}`;
+ return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?region=global&api-version=${apiVersion}`;
}
static buildAzureMonitorGetMetricNamesUrl(
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx
index 63a649aae0b..7c4c6cef5a1 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx
@@ -29,7 +29,15 @@ const defaultProps = {
subscription: 'id',
},
onChange: jest.fn(),
- datasource: createMockDatasource(),
+ datasource: createMockDatasource({
+ getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]),
+ getResourceGroups: jest.fn().mockResolvedValue([{ text: 'rg', value: 'rg' }]),
+ getMetricNamespaces: jest.fn().mockResolvedValue([{ text: 'foo/bar', value: 'foo/bar' }]),
+ getVariablesRaw: jest.fn().mockReturnValue([
+ { label: 'query0', name: 'sub0' },
+ { label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } },
+ ]),
+ }),
};
const originalConfigValue = grafanaRuntime.config.featureToggles.azTemplateVars;
@@ -166,11 +174,8 @@ describe('VariableEditor:', () => {
it('should run the query if requesting resource groups', async () => {
grafanaRuntime.config.featureToggles.azTemplateVars = true;
- const ds = createMockDatasource({
- getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]),
- });
const onChange = jest.fn();
- const { rerender } = render();
+ const { rerender } = render();
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
// Select RGs variable
@@ -195,14 +200,7 @@ describe('VariableEditor:', () => {
it('should show template variables as options ', async () => {
const onChange = jest.fn();
grafanaRuntime.config.featureToggles.azTemplateVars = true;
- const ds = createMockDatasource({
- getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]),
- getVariablesRaw: jest.fn().mockReturnValue([
- { label: 'query0', name: 'sub0' },
- { label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } },
- ]),
- });
- const { rerender } = render();
+ const { rerender } = render();
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
// Select RGs variable
@@ -210,7 +208,7 @@ describe('VariableEditor:', () => {
screen.getByText('Resource Groups').click();
// Simulate onChange behavior
const newQuery = onChange.mock.calls.at(-1)[0];
- rerender();
+ rerender();
await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument());
// Select a subscription
openMenu(screen.getByLabelText('select subscription'));
@@ -218,10 +216,60 @@ describe('VariableEditor:', () => {
screen.getByText('Template Variables').click();
// Simulate onChange behavior
const lastQuery = onChange.mock.calls.at(-1)[0];
- rerender();
+ rerender();
await waitFor(() => expect(screen.getByText('query0')).toBeInTheDocument());
// Template variables of the same type than the current one should not appear
expect(screen.queryByText('query1')).not.toBeInTheDocument();
});
+
+ it('should run the query if requesting namespaces', async () => {
+ grafanaRuntime.config.featureToggles.azTemplateVars = true;
+ const onChange = jest.fn();
+ const { rerender } = render();
+ // wait for initial load
+ await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
+ // Select RGs variable
+ openMenu(screen.getByLabelText('select query type'));
+ screen.getByText('Namespaces').click();
+ // Simulate onChange behavior
+ const newQuery = onChange.mock.calls.at(-1)[0];
+ rerender();
+ await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument());
+ // Select a subscription
+ openMenu(screen.getByLabelText('select subscription'));
+ screen.getByText('Primary Subscription').click();
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ queryType: AzureQueryType.NamespacesQuery,
+ subscription: 'sub',
+ refId: 'A',
+ })
+ );
+ });
+
+ it('should run the query if requesting resource names', async () => {
+ grafanaRuntime.config.featureToggles.azTemplateVars = true;
+ const onChange = jest.fn();
+ const { rerender } = render();
+ // wait for initial load
+ await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
+ // Select RGs variable
+ openMenu(screen.getByLabelText('select query type'));
+ screen.getByText('Resource Names').click();
+ // Simulate onChange behavior
+ const newQuery = onChange.mock.calls.at(-1)[0];
+ rerender();
+ await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument());
+ // Select a subscription
+ openMenu(screen.getByLabelText('select subscription'));
+ screen.getByText('Primary Subscription').click();
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ queryType: AzureQueryType.ResourceNamesQuery,
+ subscription: 'sub',
+ refId: 'A',
+ })
+ );
+ });
});
});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx
index 08cc4aae721..c2abb163768 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx
@@ -21,6 +21,8 @@ type Props = {
datasource: DataSource;
};
+const removeOption: SelectableValue = { label: '-', value: '' };
+
const VariableEditor = (props: Props) => {
const { query, onChange, datasource } = props;
const AZURE_QUERY_VARIABLE_TYPE_OPTIONS = [
@@ -30,13 +32,19 @@ const VariableEditor = (props: Props) => {
if (config.featureToggles.azTemplateVars) {
AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Subscriptions', value: AzureQueryType.SubscriptionsQuery });
AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Resource Groups', value: AzureQueryType.ResourceGroupsQuery });
+ AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Namespaces', value: AzureQueryType.NamespacesQuery });
+ AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Resource Names', value: AzureQueryType.ResourceNamesQuery });
}
const [variableOptionGroup, setVariableOptionGroup] = useState<{ label: string; options: AzureMonitorOption[] }>({
label: 'Template Variables',
options: [],
});
const [requireSubscription, setRequireSubscription] = useState(false);
+ const [hasResourceGroup, setHasResourceGroup] = useState(false);
+ const [hasNamespace, setHasNamespace] = useState(false);
const [subscriptions, setSubscriptions] = useState([]);
+ const [resourceGroups, setResourceGroups] = useState([]);
+ const [namespaces, setNamespaces] = useState([]);
const [errorMessage, setError] = useLastError();
const queryType = typeof query === 'string' ? '' : query.queryType;
@@ -47,12 +55,22 @@ const VariableEditor = (props: Props) => {
}, [query, datasource, onChange]);
useEffect(() => {
+ setRequireSubscription(false);
+ setHasResourceGroup(false);
+ setHasNamespace(false);
switch (queryType) {
case AzureQueryType.ResourceGroupsQuery:
setRequireSubscription(true);
break;
- default:
- setRequireSubscription(false);
+ case AzureQueryType.NamespacesQuery:
+ setRequireSubscription(true);
+ setHasResourceGroup(true);
+ break;
+ case AzureQueryType.ResourceNamesQuery:
+ setRequireSubscription(true);
+ setHasResourceGroup(true);
+ setHasNamespace(true);
+ break;
}
}, [queryType]);
@@ -75,6 +93,24 @@ const VariableEditor = (props: Props) => {
});
});
+ const subscription = typeof query === 'object' && query.subscription;
+ useEffect(() => {
+ if (subscription) {
+ datasource.getResourceGroups(subscription).then((rgs) => {
+ setResourceGroups(rgs.map((s) => ({ label: s.text, value: s.value })));
+ });
+ }
+ }, [datasource, subscription]);
+
+ const resourceGroup = (typeof query === 'object' && query.resourceGroup) || '';
+ useEffect(() => {
+ if (subscription) {
+ datasource.getMetricNamespaces(subscription, resourceGroup).then((rgs) => {
+ setNamespaces(rgs.map((s) => ({ label: s.text, value: s.value })));
+ });
+ }
+ }, [datasource, subscription, resourceGroup]);
+
if (typeof query === 'string') {
// still migrating the query
return null;
@@ -98,6 +134,20 @@ const VariableEditor = (props: Props) => {
}
};
+ const onChangeResourceGroup = (selectableValue: SelectableValue) => {
+ onChange({
+ ...query,
+ resourceGroup: selectableValue.value,
+ });
+ };
+
+ const onChangeNamespace = (selectableValue: SelectableValue) => {
+ onChange({
+ ...query,
+ namespace: selectableValue.value,
+ });
+ };
+
const onLogsQueryChange = (queryChange: AzureMonitorQuery) => {
onChange(queryChange);
};
@@ -113,7 +163,7 @@ const VariableEditor = (props: Props) => {
value={queryType}
/>
- {typeof query === 'object' && query.queryType === AzureQueryType.LogAnalytics && (
+ {query.queryType === AzureQueryType.LogAnalytics && (
<>
{
)}
>
)}
- {typeof query === 'object' && query.queryType === AzureQueryType.GrafanaTemplateVariableFn && (
+ {query.queryType === AzureQueryType.GrafanaTemplateVariableFn && (
)}
- {typeof query === 'object' && requireSubscription && (
+ {requireSubscription && (
)}
+ {hasResourceGroup && (
+
+
+
+ )}
+ {hasNamespace && (
+
+
+
+ )}
>
);
};
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts
index 0366efec881..49f5b6618fb 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts
@@ -154,7 +154,15 @@ export default class Datasource extends DataSourceWithBackend {
done();
});
});
+
+ it('can fetch namespaces', (done) => {
+ const expectedResults = ['test'];
+ const variableSupport = new VariableSupport(
+ createMockDatasource({
+ getMetricNamespaces: jest.fn().mockResolvedValueOnce(expectedResults),
+ })
+ );
+ const mockRequest = {
+ targets: [
+ {
+ refId: 'A',
+ queryType: AzureQueryType.NamespacesQuery,
+ subscription: 'sub',
+ } as AzureMonitorQuery,
+ ],
+ } as DataQueryRequest;
+ const observables = variableSupport.query(mockRequest);
+ observables.subscribe((result: DataQueryResponseData) => {
+ expect(result.data[0].source).toEqual(expectedResults);
+ done();
+ });
+ });
+
+ it('can fetch resource names', (done) => {
+ const expectedResults = ['test'];
+ const variableSupport = new VariableSupport(
+ createMockDatasource({
+ getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
+ })
+ );
+ const mockRequest = {
+ targets: [
+ {
+ refId: 'A',
+ queryType: AzureQueryType.ResourceNamesQuery,
+ subscription: 'sub',
+ } as AzureMonitorQuery,
+ ],
+ } as DataQueryRequest;
+ const observables = variableSupport.query(mockRequest);
+ observables.subscribe((result: DataQueryResponseData) => {
+ expect(result.data[0].source).toEqual(expectedResults);
+ done();
+ });
+ });
});
});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts
index 991eca7ae99..d3dd1d6d73b 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts
@@ -43,6 +43,24 @@ export class VariableSupport extends CustomVariableSupport
Date: Fri, 15 Jul 2022 14:26:15 +0200
Subject: [PATCH 007/116] Logs: Fixed incorrect highlighting on empty line
filter (#52214)
* fixed hightlighting searchwords
* do not add empty searchWords
---
.../datasource/loki/query_utils.test.ts | 30 +++++++++++++++++++
.../plugins/datasource/loki/query_utils.ts | 14 ++++++---
2 files changed, 40 insertions(+), 4 deletions(-)
diff --git a/public/app/plugins/datasource/loki/query_utils.test.ts b/public/app/plugins/datasource/loki/query_utils.test.ts
index 34c8425b1dd..870cc256f02 100644
--- a/public/app/plugins/datasource/loki/query_utils.test.ts
+++ b/public/app/plugins/datasource/loki/query_utils.test.ts
@@ -12,6 +12,36 @@ describe('getHighlighterExpressionsFromQuery', () => {
expect(getHighlighterExpressionsFromQuery('')).toEqual([]);
});
+ it('returns no expression for query with empty filter ', () => {
+ expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= ``')).toEqual([]);
+ });
+
+ it('returns no expression for query with empty filter and parser', () => {
+ expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= `` | json count="counter" | __error__=``')).toEqual([]);
+ });
+
+ it('returns no expression for query with empty filter and chained filter', () => {
+ expect(
+ getHighlighterExpressionsFromQuery('{foo="bar"} |= `` |= `highlight` | json count="counter" | __error__=``')
+ ).toEqual(['highlight']);
+ });
+
+ it('returns no expression for query with empty filter, chained and regex filter', () => {
+ expect(
+ getHighlighterExpressionsFromQuery(
+ '{foo="bar"} |= `` |= `highlight` |~ `high.ight` | json count="counter" | __error__=``'
+ )
+ ).toEqual(['highlight', 'high.ight']);
+ });
+
+ it('returns no expression for query with empty filter, chained and regex quotes filter', () => {
+ expect(
+ getHighlighterExpressionsFromQuery(
+ '{foo="bar"} |= `` |= `highlight` |~ "highlight\\\\d" | json count="counter" | __error__=``'
+ )
+ ).toEqual(['highlight', 'highlight\\d']);
+ });
+
it('returns an expression for query with filter using quotes', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x"')).toEqual(['x']);
});
diff --git a/public/app/plugins/datasource/loki/query_utils.ts b/public/app/plugins/datasource/loki/query_utils.ts
index a501b729042..3afa1bb8df2 100644
--- a/public/app/plugins/datasource/loki/query_utils.ts
+++ b/public/app/plugins/datasource/loki/query_utils.ts
@@ -32,8 +32,8 @@ export function getHighlighterExpressionsFromQuery(input: string): string[] {
if (skip) {
continue;
}
- // Check if there is more chained
- const filterEnd = expression.search(/\|=|\|~|!=|!~/);
+ // Check if there is more chained, by just looking for the next pipe-operator
+ const filterEnd = expression.search(/\|/);
let filterTerm;
if (filterEnd === -1) {
filterTerm = expression.trim();
@@ -50,14 +50,20 @@ export function getHighlighterExpressionsFromQuery(input: string): string[] {
const unwrappedFilterTerm = term[1];
const regexOperator = filterOperator === '|~';
+ let resultTerm = '';
+
// Only filter expressions with |~ operator are treated as regular expressions
if (regexOperator) {
// When using backticks, Loki doesn't require to escape special characters and we can just push regular expression to highlights array
// When using quotes, we have extra backslash escaping and we need to replace \\ with \
- results.push(backtickedTerm ? unwrappedFilterTerm : unwrappedFilterTerm.replace(/\\\\/g, '\\'));
+ resultTerm = backtickedTerm ? unwrappedFilterTerm : unwrappedFilterTerm.replace(/\\\\/g, '\\');
} else {
// We need to escape this string so it is not matched as regular expression
- results.push(escapeRegExp(unwrappedFilterTerm));
+ resultTerm = escapeRegExp(unwrappedFilterTerm);
+ }
+
+ if (resultTerm) {
+ results.push(resultTerm);
}
} else {
return results;
From 0531e4efc0ed630036fcd914b9b7b88bf526b733 Mon Sep 17 00:00:00 2001
From: Sven Grossmann
Date: Fri, 15 Jul 2022 14:37:53 +0200
Subject: [PATCH 008/116] Elasticsearch: Added `modifyQuery` method to add
filters in Explore (#52313)
---
.../elasticsearch/datasource.test.ts | 48 +++++++++++++++++++
.../datasource/elasticsearch/datasource.ts | 21 ++++++++
2 files changed, 69 insertions(+)
diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts
index 887ff469840..78f622211cc 100644
--- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts
+++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts
@@ -1021,6 +1021,54 @@ describe('enhanceDataFrame', () => {
});
});
+describe('modifyQuery', () => {
+ let ds: ElasticDatasource;
+ beforeEach(() => {
+ ds = getTestContext().ds;
+ });
+ describe('with empty query', () => {
+ let query: ElasticsearchQuery;
+ beforeEach(() => {
+ query = { query: '', refId: 'A' };
+ });
+
+ it('should add the filter', () => {
+ expect(ds.modifyQuery(query, { type: 'ADD_FILTER', key: 'foo', value: 'bar' }).query).toBe('foo:"bar"');
+ });
+
+ it('should add the negative filter', () => {
+ expect(ds.modifyQuery(query, { type: 'ADD_FILTER_OUT', key: 'foo', value: 'bar' }).query).toBe('-foo:"bar"');
+ });
+
+ it('should do nothing on unknown type', () => {
+ expect(ds.modifyQuery(query, { type: 'unknown', key: 'foo', value: 'bar' }).query).toBe(query.query);
+ });
+ });
+
+ describe('with non-empty query', () => {
+ let query: ElasticsearchQuery;
+ beforeEach(() => {
+ query = { query: 'test:"value"', refId: 'A' };
+ });
+
+ it('should add the filter', () => {
+ expect(ds.modifyQuery(query, { type: 'ADD_FILTER', key: 'foo', value: 'bar' }).query).toBe(
+ 'test:"value" AND foo:"bar"'
+ );
+ });
+
+ it('should add the negative filter', () => {
+ expect(ds.modifyQuery(query, { type: 'ADD_FILTER_OUT', key: 'foo', value: 'bar' }).query).toBe(
+ 'test:"value" AND -foo:"bar"'
+ );
+ });
+
+ it('should do nothing on unknown type', () => {
+ expect(ds.modifyQuery(query, { type: 'unknown', key: 'foo', value: 'bar' }).query).toBe(query.query);
+ });
+ });
+});
+
const createElasticQuery = (): DataQueryRequest => {
return {
requestId: '',
diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts
index e52fca2064f..4aa1df7f555 100644
--- a/public/app/plugins/datasource/elasticsearch/datasource.ts
+++ b/public/app/plugins/datasource/elasticsearch/datasource.ts
@@ -952,6 +952,27 @@ export class ElasticDatasource
return false;
}
+
+ modifyQuery(query: ElasticsearchQuery, action: { type: string; key: string; value: string }): ElasticsearchQuery {
+ let expression = query.query ?? '';
+ switch (action.type) {
+ case 'ADD_FILTER': {
+ if (expression.length > 0) {
+ expression += ' AND ';
+ }
+ expression += `${action.key}:"${action.value}"`;
+ break;
+ }
+ case 'ADD_FILTER_OUT': {
+ if (expression.length > 0) {
+ expression += ' AND ';
+ }
+ expression += `-${action.key}:"${action.value}"`;
+ break;
+ }
+ }
+ return { ...query, query: expression };
+ }
}
/**
From 57273d4846c05b0221ece7575c91b09789a755e3 Mon Sep 17 00:00:00 2001
From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com>
Date: Fri, 15 Jul 2022 14:38:14 +0100
Subject: [PATCH 009/116] Internationalisation: Translates "Inspect panel"
drawer (#52324)
---
.../components/Inspector/InspectContent.tsx | 14 +-
.../dashboard/components/Inspector/hooks.ts | 18 +-
.../features/inspector/InspectDataOptions.tsx | 40 +++--
.../app/features/inspector/InspectDataTab.tsx | 14 +-
.../app/features/inspector/InspectJSONTab.tsx | 26 ++-
.../features/inspector/InspectMetadataTab.tsx | 3 +-
.../features/inspector/InspectStatsTab.tsx | 30 +++-
public/locales/en/messages.po | 168 ++++++++++++++++++
public/locales/es/messages.po | 168 ++++++++++++++++++
public/locales/fr/messages.po | 168 ++++++++++++++++++
public/locales/pseudo-LOCALE/messages.po | 168 ++++++++++++++++++
11 files changed, 775 insertions(+), 42 deletions(-)
diff --git a/public/app/features/dashboard/components/Inspector/InspectContent.tsx b/public/app/features/dashboard/components/Inspector/InspectContent.tsx
index 5d3e5b9fc36..42af0e65e5d 100644
--- a/public/app/features/dashboard/components/Inspector/InspectContent.tsx
+++ b/public/app/features/dashboard/components/Inspector/InspectContent.tsx
@@ -1,3 +1,4 @@
+import { t } from '@lingui/macro';
import React, { useState } from 'react';
import { CoreApp, DataSourceApi, formattedValueToString, getValueFormat, PanelData, PanelPlugin } from '@grafana/data';
@@ -57,11 +58,15 @@ export const InspectContent: React.FC = ({
activeTab = InspectTab.JSON;
}
- const title = getTemplateSrv().replace(panel.title, panel.scopedVars, 'text');
+ const panelTitle = getTemplateSrv().replace(panel.title, panel.scopedVars, 'text') || 'Panel';
+ const title = t({
+ id: 'dashboard.inspect.title',
+ message: `Inspect: ${panelTitle}`,
+ });
return (
{
const tabs = [];
if (supportsDataQuery(plugin)) {
- tabs.push({ label: 'Data', value: InspectTab.Data });
- tabs.push({ label: 'Stats', value: InspectTab.Stats });
+ tabs.push({ label: t({ id: 'dashboard.inspect.data-tab', message: 'Data' }), value: InspectTab.Data });
+ tabs.push({ label: t({ id: 'dashboard.inspect.stats-tab', message: 'Stats' }), value: InspectTab.Stats });
}
if (metaDs) {
- tabs.push({ label: 'Meta Data', value: InspectTab.Meta });
+ tabs.push({ label: t({ id: 'dashboard.inspect.meta-tab', message: 'Meta Data' }), value: InspectTab.Meta });
}
- tabs.push({ label: 'JSON', value: InspectTab.JSON });
+ tabs.push({ label: t({ id: 'dashboard.inspect.json-tab', message: 'JSON' }), value: InspectTab.JSON });
if (error && error.message) {
- tabs.push({ label: 'Error', value: InspectTab.Error });
+ tabs.push({ label: t({ id: 'dashboard.inspect.error-tab', message: 'Error' }), value: InspectTab.Error });
}
// This is a quick internal hack to allow custom actions in inspect
// For 8.1, something like this should be exposed through grafana/runtime
const supplier = (window as any).grafanaPanelInspectActionSupplier as PanelInspectActionSupplier;
if (supplier && supplier.getActions(panel)) {
- tabs.push({ label: 'Actions', value: InspectTab.Actions });
+ tabs.push({
+ label: t({ id: 'dashboard.inspect.actions-tab', message: 'Actions' }),
+ value: InspectTab.Actions,
+ });
}
if (dashboard.meta.canEdit && supportsDataQuery(plugin)) {
- tabs.push({ label: 'Query', value: InspectTab.Query });
+ tabs.push({ label: t({ id: 'dashboard.inspect.query-tab', message: 'Query' }), value: InspectTab.Query });
}
return tabs;
}, [panel, plugin, metaDs, dashboard, error]);
diff --git a/public/app/features/inspector/InspectDataOptions.tsx b/public/app/features/inspector/InspectDataOptions.tsx
index f9431b4d10a..ae6dd627b9c 100644
--- a/public/app/features/inspector/InspectDataOptions.tsx
+++ b/public/app/features/inspector/InspectDataOptions.tsx
@@ -1,3 +1,4 @@
+import { t } from '@lingui/macro';
import React, { FC } from 'react';
import { DataFrame, DataTransformerID, getFrameDisplayName, SelectableValue } from '@grafana/data';
@@ -67,26 +68,26 @@ export const InspectDataOptions: FC = ({
const parts: string[] = [];
if (selectedDataFrame === DataTransformerID.seriesToColumns) {
- parts.push('Series joined by time');
+ parts.push(t({ id: 'dashboard.inspect-data.series-to-columns', message: 'Series joined by time' }));
} else if (data.length > 1) {
parts.push(getFrameDisplayName(data[selectedDataFrame as number]));
}
if (options.withTransforms || options.withFieldConfig) {
if (options.withTransforms) {
- parts.push('Panel transforms');
+ parts.push(t({ id: 'dashboard.inspect-data.panel-transforms', message: 'Panel transforms' }));
}
if (options.withTransforms && options.withFieldConfig) {
}
if (options.withFieldConfig) {
- parts.push('Formatted data');
+ parts.push(t({ id: 'dashboard.inspect-data.formatted', message: 'Formatted data' }));
}
}
if (downloadForExcel) {
- parts.push('Excel header');
+ parts.push(t({ id: 'dashboard.inspect-data.excel-header', message: 'Excel header' }));
}
return parts.join(', ');
@@ -97,20 +98,20 @@ export const InspectDataOptions: FC = ({
{getActiveString()}}
isOpen={false}
>
{data!.length > 1 && (
-
+
)}
@@ -118,8 +119,14 @@ export const InspectDataOptions: FC = ({
{showPanelTransformationsOption && onOptionsChange && (
= ({
)}
{showFieldConfigsOption && onOptionsChange && (
= ({
/>
)}
-
+
diff --git a/public/app/features/inspector/InspectDataTab.tsx b/public/app/features/inspector/InspectDataTab.tsx
index 4e6968fc8bd..850e4234116 100644
--- a/public/app/features/inspector/InspectDataTab.tsx
+++ b/public/app/features/inspector/InspectDataTab.tsx
@@ -1,4 +1,5 @@
import { css } from '@emotion/css';
+import { Trans, t } from '@lingui/macro';
import { saveAs } from 'file-saver';
import React, { PureComponent } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -286,7 +287,7 @@ export class InspectDataTab extends PureComponent {
margin-bottom: 10px;
`}
>
- Download CSV
+ Download CSV
{hasLogs && (
)}
{hasTraces && (
@@ -309,7 +310,7 @@ export class InspectDataTab extends PureComponent {
margin-left: 10px;
`}
>
- Download traces
+ Download traces
)}
{hasServiceGraph && (
@@ -321,7 +322,7 @@ export class InspectDataTab extends PureComponent {
margin-left: 10px;
`}
>
- Download service graph
+ Download service graph
)}
@@ -349,7 +350,10 @@ function buildTransformationOptions() {
const transformations: Array> = [
{
value: DataTransformerID.seriesToColumns,
- label: 'Series joined by time',
+ label: t({
+ id: 'dashboard.inspect-data.transformation',
+ message: 'Series joined by time',
+ }),
transformer: {
id: DataTransformerID.seriesToColumns,
options: { byField: 'Time' },
diff --git a/public/app/features/inspector/InspectJSONTab.tsx b/public/app/features/inspector/InspectJSONTab.tsx
index 3c9c1adfca9..e5dd4e0c3a9 100644
--- a/public/app/features/inspector/InspectJSONTab.tsx
+++ b/public/app/features/inspector/InspectJSONTab.tsx
@@ -1,3 +1,4 @@
+import { t } from '@lingui/macro';
import React, { PureComponent } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -17,18 +18,24 @@ enum ShowContent {
const options: Array> = [
{
- label: 'Panel JSON',
- description: 'The model saved in the dashboard JSON that configures how everything works.',
+ label: t({ id: 'dashboard.inspect-json.panel-json-label', message: 'Panel JSON' }),
+ description: t({
+ id: 'dashboard.inspect-json.panel-json-description',
+ message: 'The model saved in the dashboard JSON that configures how everything works.',
+ }),
value: ShowContent.PanelJSON,
},
{
- label: 'Panel data',
- description: 'The raw model passed to the panel visualization',
+ label: t({ id: 'dashboard.inspect-json.panel-data-label', message: 'Panel data' }),
+ description: t({
+ id: 'dashboard.inspect-json.panel-data-description',
+ message: 'The raw model passed to the panel visualization',
+ }),
value: ShowContent.PanelData,
},
{
- label: 'DataFrame JSON',
- description: 'JSON formatted DataFrames',
+ label: t({ id: 'dashboard.inspect-json.dataframe-label', message: 'DataFrame JSON' }),
+ description: t({ id: 'dashboard.inspect-json.dataframe-description', message: 'JSON formatted DataFrames' }),
value: ShowContent.DataFrames,
},
];
@@ -83,7 +90,7 @@ export class InspectJSONTab extends PureComponent {
return panel!.getSaveModel();
}
- return { note: `Unknown Object: ${show}` };
+ return { note: t({ id: 'dashboard.inspect-json.unknown', message: `Unknown Object: ${show}` }) };
}
onApplyPanelModel = () => {
@@ -120,7 +127,10 @@ export class InspectJSONTab extends PureComponent {
return (
-
+