From 9e47114c45bac0acf921c6811b40967e025dc917 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 2 Jul 2020 10:33:45 +0300 Subject: [PATCH 01/78] Forgot password: Fix styling (#26002) --- .../core/components/ForgottenPassword/ForgottenPassword.tsx | 2 +- public/app/core/components/Login/LoginLayout.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx b/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx index e0c1eb4b82b..39069dd376d 100644 --- a/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx +++ b/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx @@ -45,7 +45,7 @@ export const ForgottenPassword: FC = () => { Reset password diff --git a/public/app/core/components/Login/LoginLayout.tsx b/public/app/core/components/Login/LoginLayout.tsx index 8a00e39bff4..e0ccd7f86cf 100644 --- a/public/app/core/components/Login/LoginLayout.tsx +++ b/public/app/core/components/Login/LoginLayout.tsx @@ -49,10 +49,11 @@ export const getLoginStyles = (theme: GrafanaTheme) => { min-height: 100vh; background-position: center; background-repeat: no-repeat; + background-color: ${theme.palette.black}; min-width: 100%; margin-left: 0; - background-color: $black; display: flex; + flex-direction: column; align-items: center; justify-content: center; `, @@ -76,7 +77,7 @@ export const getLoginStyles = (theme: GrafanaTheme) => { text-align: center; `, mainTitle: css` - font-size: '32px'; + font-size: 32px; `, subTitle: css` font-size: ${theme.typography.size.md}; From 085b2f3dbf85c7a4ce03d675215ab1d8ab6428ad Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 2 Jul 2020 01:42:44 -0700 Subject: [PATCH 02/78] Monaco: check suggestions against current word (#25992) * trigger on current word * proper index * test suggestsions * test suggestsions * fix test --- .../src/components/Monaco/suggestions.test.ts | 38 +++++++++ .../src/components/Monaco/suggestions.ts | 77 +++++++++++-------- 2 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 packages/grafana-ui/src/components/Monaco/suggestions.test.ts diff --git a/packages/grafana-ui/src/components/Monaco/suggestions.test.ts b/packages/grafana-ui/src/components/Monaco/suggestions.test.ts new file mode 100644 index 00000000000..61be3eb69d5 --- /dev/null +++ b/packages/grafana-ui/src/components/Monaco/suggestions.test.ts @@ -0,0 +1,38 @@ +import { findInsertIndex } from './suggestions'; + +describe('Check suggestion index', () => { + it('find last $ sign', () => { + const line = ' hello $123'; + const { index, prefix } = findInsertIndex(line); + expect(index).toEqual(line.indexOf('$')); + expect(prefix).toEqual('$123'); + }); + + it('insert into empty line', () => { + const line = ''; + const { index, prefix } = findInsertIndex(line); + expect(index).toEqual(0); + expect(prefix).toEqual(''); + }); + + it('insert new word', () => { + const line = 'this is a new '; + const { index, prefix } = findInsertIndex(line); + expect(index).toEqual(line.length); + expect(prefix).toEqual(''); + }); + + it('complte a simple word', () => { + const line = 'SELECT * FROM tab'; + const { index, prefix } = findInsertIndex(line); + expect(index).toEqual(line.lastIndexOf(' ') + 1); + expect(prefix).toEqual('tab'); + }); + + it('complete a quoted word', () => { + const line = 'SELECT "hello", "wo'; + const { index, prefix } = findInsertIndex(line); + expect(index).toEqual(line.lastIndexOf('"') + 1); + expect(prefix).toEqual('wo'); + }); +}); diff --git a/packages/grafana-ui/src/components/Monaco/suggestions.ts b/packages/grafana-ui/src/components/Monaco/suggestions.ts index 1070789943b..dc79f467557 100644 --- a/packages/grafana-ui/src/components/Monaco/suggestions.ts +++ b/packages/grafana-ui/src/components/Monaco/suggestions.ts @@ -2,6 +2,33 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import { CodeEditorSuggestionItem, CodeEditorSuggestionItemKind, CodeEditorSuggestionProvider } from './types'; +/** + * @internal -- only exported for tests + */ +export function findInsertIndex(line: string): { index: number; prefix: string } { + for (let i = line.length - 1; i > 0; i--) { + const ch = line.charAt(i); + if (ch === '$') { + return { + index: i, + prefix: line.substring(i), + }; + } + + // Keep these seperators + if (ch === ' ' || ch === '\t' || ch === '"' || ch === "'") { + return { + index: i + 1, + prefix: line.substring(i + 1), + }; + } + } + return { + index: 0, + prefix: line, + }; +} + function getCompletionItems( prefix: string, suggestions: CodeEditorSuggestionItem[], @@ -53,51 +80,39 @@ export function registerSuggestions( triggerCharacters: ['$'], provideCompletionItems: (model, position, context) => { + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column, + endColumn: position.column, + }; + + // Simple check if this was triggered by pressing `$` if (context.triggerCharacter === '$') { - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: position.column - 1, - endColumn: position.column, - }; + range.startColumn = position.column - 1; return { suggestions: getCompletionItems('$', getSuggestions(), range), }; } - // find out if we are completing a property in the 'dependencies' object. - const lineText = model.getValueInRange({ + // Find the replacement region + const currentLine = model.getValueInRange({ startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column, }); - const idx = lineText.lastIndexOf('$'); - if (idx >= 0) { - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: idx, // the last $ we found - endColumn: position.column, - }; - return { - suggestions: getCompletionItems(lineText.substr(idx), getSuggestions(), range), - }; + const { index, prefix } = findInsertIndex(currentLine); + range.startColumn = index + 1; + + const suggestions = getCompletionItems(prefix, getSuggestions(), range); + if (suggestions.length) { + // NOTE, this will replace any language provided suggestions + return { suggestions }; } - // Empty line that asked for suggestion - if (lineText.trim().length < 1) { - return { - suggestions: getCompletionItems('', getSuggestions(), { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: position.column, - endColumn: position.column, - }), - }; - } - // console.log('complete?', lineText, context); + // Default language suggestions return undefined; }, }); From 3e9e2db384361e847d15842c489bf93f33ab2164 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Thu, 2 Jul 2020 11:28:37 +0200 Subject: [PATCH 03/78] CircleCI: Upgrade build pipeline tool (#26006) Signed-off-by: Arve Knudsen --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ebacf0aba33..d878d72bd88 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,7 +56,7 @@ commands: - run: name: "Install Grafana build pipeline tool" command: | - VERSION=0.4.17 + VERSION=0.4.18 curl -fLO https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v${VERSION}/grabpl chmod +x grabpl mv grabpl /tmp From c3d4e69a3229906f5c6d028a7ee48152590d1c1b Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 2 Jul 2020 14:17:42 +0300 Subject: [PATCH 04/78] Grafana UI: Make FileUpload button size customizable (#26013) --- .../components/FileUpload/FileUpload.story.tsx | 4 ++++ .../src/components/FileUpload/FileUpload.tsx | 16 ++++++++++++---- .../grafana-ui/src/utils/storybook/useSize.ts | 7 +++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 packages/grafana-ui/src/utils/storybook/useSize.ts diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx index d3e44096404..398295d3e43 100644 --- a/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { FileUpload } from './FileUpload'; import mdx from './FileUpload.mdx'; +import { useSize } from '../../utils/storybook/useSize'; +import { ComponentSize } from '../../types/size'; export default { title: 'Forms/FileUpload', @@ -15,8 +17,10 @@ export default { }; export const single = () => { + const size = useSize(); return ( console.log('file', currentTarget?.files && currentTarget.files[0])} /> ); diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx index 776fdec5f45..5525001112d 100644 --- a/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx @@ -3,12 +3,14 @@ import { GrafanaTheme } from '@grafana/data'; import { css, cx } from 'emotion'; import { getFormStyles, Icon } from '../index'; import { stylesFactory, useTheme } from '../../themes'; +import { ComponentSize } from '../../types/size'; export interface Props { onFileUpload: (event: FormEvent) => void; /** Accepted file extensions */ accept?: string; className?: string; + size?: ComponentSize; } function trimFileName(fileName: string) { @@ -24,9 +26,15 @@ function trimFileName(fileName: string) { return `${file.substring(0, nameLength)}...${extension}`; } -export const FileUpload: FC = ({ onFileUpload, className, children = 'Upload file', accept = '*' }) => { +export const FileUpload: FC = ({ + onFileUpload, + className, + children = 'Upload file', + accept = '*', + size = 'md', +}) => { const theme = useTheme(); - const style = getStyles(theme); + const style = getStyles(theme, size); const [fileName, setFileName] = useState(''); const onChange = useCallback((event: FormEvent) => { @@ -60,8 +68,8 @@ export const FileUpload: FC = ({ onFileUpload, className, children = 'Upl ); }; -const getStyles = stylesFactory((theme: GrafanaTheme) => { - const buttonFormStyle = getFormStyles(theme, { variant: 'primary', invalid: false, size: 'md' }).button.button; +const getStyles = stylesFactory((theme: GrafanaTheme, size: ComponentSize) => { + const buttonFormStyle = getFormStyles(theme, { variant: 'primary', invalid: false, size }).button.button; return { fileUpload: css` display: none; diff --git a/packages/grafana-ui/src/utils/storybook/useSize.ts b/packages/grafana-ui/src/utils/storybook/useSize.ts new file mode 100644 index 00000000000..d7a5d4e8064 --- /dev/null +++ b/packages/grafana-ui/src/utils/storybook/useSize.ts @@ -0,0 +1,7 @@ +import { select } from '@storybook/addon-knobs'; +import { ComponentSize } from '../../types/size'; + +export function useSize(size: ComponentSize = 'md') { + const sizes = ['xs', 'sm', 'md', 'lg']; + return select('Size', sizes, size); +} From 2d4bcbeff617bb921348536c652fecc410960875 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Thu, 2 Jul 2020 14:29:37 +0300 Subject: [PATCH 05/78] Small fix in provisioning docs (#26004) * Small fix in provisioning docs * Update docs/sources/administration/provisioning.md Co-authored-by: Arve Knudsen Co-authored-by: Arve Knudsen --- docs/sources/administration/provisioning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 75a0eb868da..4c5010dcd03 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -327,11 +327,11 @@ providers: updateIntervalSeconds: 30 options: path: /etc/dashboards - foldersFromFileStructure: true + foldersFromFilesStructure: true ``` `server` and `application` will become new folders in Grafana menu. -> **Note.** `folder` and `folderUid` options should be empty or missing to make `foldersFromFileStructure` works. +> **Note.** `folder` and `folderUid` options should be empty or missing to make `foldersFromFilesStructure` work. ## Alert Notification Channels From 3ef06a0c8872945ee867e90cc0b2b9c88a83f1ef Mon Sep 17 00:00:00 2001 From: Dhananjay <45629183+gdhananjay@users.noreply.github.com> Date: Thu, 2 Jul 2020 18:54:36 +0530 Subject: [PATCH 06/78] Cloudwatch: Add Support for external ID in assume role (#23685) Co-authored by: Arve Knudsen --- .../features/datasources/cloudwatch.md | 1 + go.mod | 1 + go.sum | 7 + pkg/tsdb/cloudwatch/cloudwatch.go | 1 + pkg/tsdb/cloudwatch/credentials.go | 45 +- pkg/tsdb/cloudwatch/credentials_test.go | 125 ++++- pkg/tsdb/cloudwatch/metric_find_query.go | 2 +- pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go | 436 ++++++++++++++++++ .../components/ConfigEditor.test.tsx | 1 + .../cloudwatch/components/ConfigEditor.tsx | 17 + .../plugins/datasource/cloudwatch/types.ts | 1 + 11 files changed, 604 insertions(+), 33 deletions(-) create mode 100644 pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index d3a7286ae58..a8623b17027 100755 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -34,6 +34,7 @@ build dashboards or use Explore with CloudWatch metrics and CloudWatch Logs. | _Auth Provider_ | Specify the provider to get credentials. | | _Credentials_ profile name | Specify the name of the profile to use (if you use `~/.aws/credentials` file), leave blank for default. | | _Assume Role Arn_ | Specify the ARN of the role to assume | +| _External ID_ | If you are assuming a role in another account, that has been created with an external ID, specify the exterrnal ID here. | ## Authentication diff --git a/go.mod b/go.mod index fe0585e63da..9898e7e8a75 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/go-sql-driver/mysql v1.5.0 github.com/go-stack/stack v1.8.0 github.com/gobwas/glob v0.2.3 + github.com/golang/mock v1.4.3 github.com/golang/protobuf v1.4.0 github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 diff --git a/go.sum b/go.sum index 7da750ff1a7..69a9229faa8 100644 --- a/go.sum +++ b/go.sum @@ -108,7 +108,10 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -437,6 +440,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -450,6 +454,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190802220118-1d1727260058/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= @@ -526,6 +531,8 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index ff858076e30..92df32b3c08 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -32,6 +32,7 @@ type DatasourceInfo struct { Region string AuthType string AssumeRoleArn string + ExternalID string Namespace string AccessKey string diff --git a/pkg/tsdb/cloudwatch/credentials.go b/pkg/tsdb/cloudwatch/credentials.go index 3a1a901f40c..d66fa4e9e7f 100644 --- a/pkg/tsdb/cloudwatch/credentials.go +++ b/pkg/tsdb/cloudwatch/credentials.go @@ -7,6 +7,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds" @@ -18,6 +19,7 @@ import ( "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/sts" + "github.com/aws/aws-sdk-go/service/sts/stsiface" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -30,7 +32,25 @@ type cache struct { var awsCredentialCache = make(map[string]cache) var credentialCacheLock sync.RWMutex -func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { +// Session factory. +// Stubbable by tests. +var newSession = func(cfgs ...*aws.Config) (*session.Session, error) { + return session.NewSession(cfgs...) +} + +// STS service factory. +// Stubbable by tests. +var newSTSService = func(p client.ConfigProvider, cfgs ...*aws.Config) stsiface.STSAPI { + return sts.New(p, cfgs...) +} + +// EC2Metadata service factory. +// Stubbable by tests. +var newEC2Metadata = func(p client.ConfigProvider, cfgs ...*aws.Config) *ec2metadata.EC2Metadata { + return ec2metadata.New(p, cfgs...) +} + +func getCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { cacheKey := fmt.Sprintf("%s:%s:%s:%s", dsInfo.AuthType, dsInfo.AccessKey, dsInfo.Profile, dsInfo.AssumeRoleArn) credentialCacheLock.RLock() if _, ok := awsCredentialCache[cacheKey]; ok { @@ -53,8 +73,11 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { RoleSessionName: aws.String("GrafanaSession"), DurationSeconds: aws.Int64(900), } + if dsInfo.ExternalID != "" { + params.ExternalId = aws.String(dsInfo.ExternalID) + } - stsSess, err := session.NewSession() + stsSess, err := newSession() if err != nil { return nil, err } @@ -70,11 +93,11 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { Credentials: stsCreds, } - sess, err := session.NewSession(stsConfig) + sess, err := newSession(stsConfig) if err != nil { return nil, err } - svc := sts.New(sess, stsConfig) + svc := newSTSService(sess, stsConfig) resp, err := svc.AssumeRole(params) if err != nil { return nil, err @@ -91,7 +114,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { expiration = &e } - sess, err := session.NewSession() + sess, err := newSession() if err != nil { return nil, err } @@ -123,7 +146,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { } func webIdentityProvider(sess *session.Session) credentials.Provider { - svc := sts.New(sess) + svc := newSTSService(sess) roleARN := os.Getenv("AWS_ROLE_ARN") tokenFilepath := os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE") @@ -152,7 +175,7 @@ func ecsCredProvider(sess *session.Session, uri string) credentials.Provider { } func ec2RoleProvider(sess *session.Session) credentials.Provider { - return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute} + return &ec2rolecreds.EC2RoleProvider{Client: newEC2Metadata(sess), ExpiryWindow: 5 * time.Minute} } func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo { @@ -167,6 +190,7 @@ func retrieveDsInfo(datasource *models.DataSource, region string) *DatasourceInf authType := datasource.JsonData.Get("authType").MustString() assumeRoleArn := datasource.JsonData.Get("assumeRoleArn").MustString() + externalID := datasource.JsonData.Get("externalId").MustString() decrypted := datasource.DecryptedValues() accessKey := decrypted["accessKey"] secretKey := decrypted["secretKey"] @@ -176,6 +200,7 @@ func retrieveDsInfo(datasource *models.DataSource, region string) *DatasourceInf Profile: datasource.Database, AuthType: authType, AssumeRoleArn: assumeRoleArn, + ExternalID: externalID, AccessKey: accessKey, SecretKey: secretKey, } @@ -184,7 +209,7 @@ func retrieveDsInfo(datasource *models.DataSource, region string) *DatasourceInf } func getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config, error) { - creds, err := GetCredentials(dsInfo) + creds, err := getCredentials(dsInfo) if err != nil { return nil, err } @@ -204,7 +229,7 @@ func (e *CloudWatchExecutor) getClient(region string) (*cloudwatch.CloudWatch, e return nil, err } - sess, err := session.NewSession(cfg) + sess, err := newSession(cfg) if err != nil { return nil, err } @@ -224,7 +249,7 @@ func retrieveLogsClient(datasourceInfo *DatasourceInfo) (*cloudwatchlogs.CloudWa return nil, err } - sess, err := session.NewSession(cfg) + sess, err := newSession(cfg) if err != nil { return nil, err } diff --git a/pkg/tsdb/cloudwatch/credentials_test.go b/pkg/tsdb/cloudwatch/credentials_test.go index a0b51ec4669..e3d5f200ba7 100644 --- a/pkg/tsdb/cloudwatch/credentials_test.go +++ b/pkg/tsdb/cloudwatch/credentials_test.go @@ -4,39 +4,120 @@ import ( "os" "testing" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds" + "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/session" - . "github.com/smartystreets/goconvey/convey" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/aws/aws-sdk-go/service/sts/stsiface" + "github.com/golang/mock/gomock" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/mock_stsiface" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestECSCredProvider(t *testing.T) { - Convey("Running in an ECS container task", t, func() { - defer os.Clearenv() - os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/abc/123") - - sess, _ := session.NewSession() - provider := remoteCredProvider(sess) - - So(provider, ShouldNotBeNil) - - ecsProvider, ok := provider.(*endpointcreds.Provider) - So(ecsProvider, ShouldNotBeNil) - So(ok, ShouldBeTrue) - - So(ecsProvider.Client.Endpoint, ShouldEqual, "http://169.254.170.2/abc/123") + os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/abc/123") + t.Cleanup(func() { + os.Unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") }) + + sess, err := session.NewSession() + require.NoError(t, err) + provider := remoteCredProvider(sess) + require.NotNil(t, provider) + + ecsProvider, ok := provider.(*endpointcreds.Provider) + require.NotNil(t, ecsProvider) + require.True(t, ok) + + assert.Equal(t, "http://169.254.170.2/abc/123", ecsProvider.Client.Endpoint) } func TestDefaultEC2RoleProvider(t *testing.T) { - Convey("Running outside an ECS container task", t, func() { - sess, _ := session.NewSession() - provider := remoteCredProvider(sess) + sess, err := session.NewSession() + require.NoError(t, err) + provider := remoteCredProvider(sess) + require.NotNil(t, provider) - So(provider, ShouldNotBeNil) + ec2Provider, ok := provider.(*ec2rolecreds.EC2RoleProvider) + require.NotNil(t, ec2Provider) + require.True(t, ok) +} - ec2Provider, ok := provider.(*ec2rolecreds.EC2RoleProvider) - So(ec2Provider, ShouldNotBeNil) - So(ok, ShouldBeTrue) +func TestGetCredentials_ARNAuthType(t *testing.T) { + ctrl := gomock.NewController(t) + var stsMock *mock_stsiface.MockSTSAPI + + origNewSession := newSession + origNewSTSService := newSTSService + origNewEC2Metadata := newEC2Metadata + t.Cleanup(func() { + newSession = origNewSession + newSTSService = origNewSTSService + newEC2Metadata = origNewEC2Metadata + }) + newSession = func(cfgs ...*aws.Config) (*session.Session, error) { + return &session.Session{}, nil + } + newSTSService = func(p client.ConfigProvider, cfgs ...*aws.Config) stsiface.STSAPI { + return stsMock + } + newEC2Metadata = func(p client.ConfigProvider, cfgs ...*aws.Config) *ec2metadata.EC2Metadata { + return nil + } + + t.Run("Without external ID", func(t *testing.T) { + stsMock = mock_stsiface.NewMockSTSAPI(ctrl) + stsMock. + EXPECT(). + AssumeRole(gomock.Eq(&sts.AssumeRoleInput{ + RoleArn: aws.String(""), + DurationSeconds: aws.Int64(900), + RoleSessionName: aws.String("GrafanaSession"), + })). + Return(&sts.AssumeRoleOutput{ + Credentials: &sts.Credentials{ + AccessKeyId: aws.String("id"), + SecretAccessKey: aws.String("secret"), + SessionToken: aws.String("token"), + }, + }, nil). + Times(1) + + creds, err := getCredentials(&DatasourceInfo{ + AuthType: "arn", + }) + require.NoError(t, err) + require.NotNil(t, creds) + }) + + t.Run("With external ID", func(t *testing.T) { + stsMock = mock_stsiface.NewMockSTSAPI(ctrl) + stsMock. + EXPECT(). + AssumeRole(gomock.Eq(&sts.AssumeRoleInput{ + RoleArn: aws.String(""), + DurationSeconds: aws.Int64(900), + RoleSessionName: aws.String("GrafanaSession"), + ExternalId: aws.String("external-id"), + })). + Return(&sts.AssumeRoleOutput{ + Credentials: &sts.Credentials{ + AccessKeyId: aws.String("id"), + SecretAccessKey: aws.String("secret"), + SessionToken: aws.String("token"), + }, + }, nil). + Times(1) + + creds, err := getCredentials(&DatasourceInfo{ + AuthType: "arn", + ExternalID: "external-id", + }) + require.NoError(t, err) + require.NotNil(t, creds) }) } diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 22e157f26f3..825d2ae12e8 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -740,7 +740,7 @@ func (e *CloudWatchExecutor) resourceGroupsGetResources(region string, filters [ } func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) { - creds, err := GetCredentials(cwData) + creds, err := getCredentials(cwData) if err != nil { return cloudwatch.ListMetricsOutput{}, err } diff --git a/pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go b/pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go new file mode 100644 index 00000000000..f7937852a91 --- /dev/null +++ b/pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go @@ -0,0 +1,436 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/aws/aws-sdk-go/service/sts/stsiface (interfaces: STSAPI) + +// Package mock_stsiface is a generated GoMock package. +package mock_stsiface + +import ( + context "context" + request "github.com/aws/aws-sdk-go/aws/request" + sts "github.com/aws/aws-sdk-go/service/sts" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockSTSAPI is a mock of STSAPI interface +type MockSTSAPI struct { + ctrl *gomock.Controller + recorder *MockSTSAPIMockRecorder +} + +// MockSTSAPIMockRecorder is the mock recorder for MockSTSAPI +type MockSTSAPIMockRecorder struct { + mock *MockSTSAPI +} + +// NewMockSTSAPI creates a new mock instance +func NewMockSTSAPI(ctrl *gomock.Controller) *MockSTSAPI { + mock := &MockSTSAPI{ctrl: ctrl} + mock.recorder = &MockSTSAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockSTSAPI) EXPECT() *MockSTSAPIMockRecorder { + return m.recorder +} + +// AssumeRole mocks base method +func (m *MockSTSAPI) AssumeRole(arg0 *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssumeRole", arg0) + ret0, _ := ret[0].(*sts.AssumeRoleOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssumeRole indicates an expected call of AssumeRole +func (mr *MockSTSAPIMockRecorder) AssumeRole(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRole", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRole), arg0) +} + +// AssumeRoleRequest mocks base method +func (m *MockSTSAPI) AssumeRoleRequest(arg0 *sts.AssumeRoleInput) (*request.Request, *sts.AssumeRoleOutput) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssumeRoleRequest", arg0) + ret0, _ := ret[0].(*request.Request) + ret1, _ := ret[1].(*sts.AssumeRoleOutput) + return ret0, ret1 +} + +// AssumeRoleRequest indicates an expected call of AssumeRoleRequest +func (mr *MockSTSAPIMockRecorder) AssumeRoleRequest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleRequest), arg0) +} + +// AssumeRoleWithContext mocks base method +func (m *MockSTSAPI) AssumeRoleWithContext(arg0 context.Context, arg1 *sts.AssumeRoleInput, arg2 ...request.Option) (*sts.AssumeRoleOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AssumeRoleWithContext", varargs...) + ret0, _ := ret[0].(*sts.AssumeRoleOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssumeRoleWithContext indicates an expected call of AssumeRoleWithContext +func (mr *MockSTSAPIMockRecorder) AssumeRoleWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithContext), varargs...) +} + +// AssumeRoleWithSAML mocks base method +func (m *MockSTSAPI) AssumeRoleWithSAML(arg0 *sts.AssumeRoleWithSAMLInput) (*sts.AssumeRoleWithSAMLOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssumeRoleWithSAML", arg0) + ret0, _ := ret[0].(*sts.AssumeRoleWithSAMLOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssumeRoleWithSAML indicates an expected call of AssumeRoleWithSAML +func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAML(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAML", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAML), arg0) +} + +// AssumeRoleWithSAMLRequest mocks base method +func (m *MockSTSAPI) AssumeRoleWithSAMLRequest(arg0 *sts.AssumeRoleWithSAMLInput) (*request.Request, *sts.AssumeRoleWithSAMLOutput) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssumeRoleWithSAMLRequest", arg0) + ret0, _ := ret[0].(*request.Request) + ret1, _ := ret[1].(*sts.AssumeRoleWithSAMLOutput) + return ret0, ret1 +} + +// AssumeRoleWithSAMLRequest indicates an expected call of AssumeRoleWithSAMLRequest +func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAMLRequest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAMLRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAMLRequest), arg0) +} + +// AssumeRoleWithSAMLWithContext mocks base method +func (m *MockSTSAPI) AssumeRoleWithSAMLWithContext(arg0 context.Context, arg1 *sts.AssumeRoleWithSAMLInput, arg2 ...request.Option) (*sts.AssumeRoleWithSAMLOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AssumeRoleWithSAMLWithContext", varargs...) + ret0, _ := ret[0].(*sts.AssumeRoleWithSAMLOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssumeRoleWithSAMLWithContext indicates an expected call of AssumeRoleWithSAMLWithContext +func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAMLWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAMLWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAMLWithContext), varargs...) +} + +// AssumeRoleWithWebIdentity mocks base method +func (m *MockSTSAPI) AssumeRoleWithWebIdentity(arg0 *sts.AssumeRoleWithWebIdentityInput) (*sts.AssumeRoleWithWebIdentityOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentity", arg0) + ret0, _ := ret[0].(*sts.AssumeRoleWithWebIdentityOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssumeRoleWithWebIdentity indicates an expected call of AssumeRoleWithWebIdentity +func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentity(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentity", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentity), arg0) +} + +// AssumeRoleWithWebIdentityRequest mocks base method +func (m *MockSTSAPI) AssumeRoleWithWebIdentityRequest(arg0 *sts.AssumeRoleWithWebIdentityInput) (*request.Request, *sts.AssumeRoleWithWebIdentityOutput) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentityRequest", arg0) + ret0, _ := ret[0].(*request.Request) + ret1, _ := ret[1].(*sts.AssumeRoleWithWebIdentityOutput) + return ret0, ret1 +} + +// AssumeRoleWithWebIdentityRequest indicates an expected call of AssumeRoleWithWebIdentityRequest +func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentityRequest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentityRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentityRequest), arg0) +} + +// AssumeRoleWithWebIdentityWithContext mocks base method +func (m *MockSTSAPI) AssumeRoleWithWebIdentityWithContext(arg0 context.Context, arg1 *sts.AssumeRoleWithWebIdentityInput, arg2 ...request.Option) (*sts.AssumeRoleWithWebIdentityOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentityWithContext", varargs...) + ret0, _ := ret[0].(*sts.AssumeRoleWithWebIdentityOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssumeRoleWithWebIdentityWithContext indicates an expected call of AssumeRoleWithWebIdentityWithContext +func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentityWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentityWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentityWithContext), varargs...) +} + +// DecodeAuthorizationMessage mocks base method +func (m *MockSTSAPI) DecodeAuthorizationMessage(arg0 *sts.DecodeAuthorizationMessageInput) (*sts.DecodeAuthorizationMessageOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DecodeAuthorizationMessage", arg0) + ret0, _ := ret[0].(*sts.DecodeAuthorizationMessageOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DecodeAuthorizationMessage indicates an expected call of DecodeAuthorizationMessage +func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessage", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessage), arg0) +} + +// DecodeAuthorizationMessageRequest mocks base method +func (m *MockSTSAPI) DecodeAuthorizationMessageRequest(arg0 *sts.DecodeAuthorizationMessageInput) (*request.Request, *sts.DecodeAuthorizationMessageOutput) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DecodeAuthorizationMessageRequest", arg0) + ret0, _ := ret[0].(*request.Request) + ret1, _ := ret[1].(*sts.DecodeAuthorizationMessageOutput) + return ret0, ret1 +} + +// DecodeAuthorizationMessageRequest indicates an expected call of DecodeAuthorizationMessageRequest +func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessageRequest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessageRequest", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessageRequest), arg0) +} + +// DecodeAuthorizationMessageWithContext mocks base method +func (m *MockSTSAPI) DecodeAuthorizationMessageWithContext(arg0 context.Context, arg1 *sts.DecodeAuthorizationMessageInput, arg2 ...request.Option) (*sts.DecodeAuthorizationMessageOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DecodeAuthorizationMessageWithContext", varargs...) + ret0, _ := ret[0].(*sts.DecodeAuthorizationMessageOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DecodeAuthorizationMessageWithContext indicates an expected call of DecodeAuthorizationMessageWithContext +func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessageWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessageWithContext", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessageWithContext), varargs...) +} + +// GetAccessKeyInfo mocks base method +func (m *MockSTSAPI) GetAccessKeyInfo(arg0 *sts.GetAccessKeyInfoInput) (*sts.GetAccessKeyInfoOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccessKeyInfo", arg0) + ret0, _ := ret[0].(*sts.GetAccessKeyInfoOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAccessKeyInfo indicates an expected call of GetAccessKeyInfo +func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfo", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfo), arg0) +} + +// GetAccessKeyInfoRequest mocks base method +func (m *MockSTSAPI) GetAccessKeyInfoRequest(arg0 *sts.GetAccessKeyInfoInput) (*request.Request, *sts.GetAccessKeyInfoOutput) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccessKeyInfoRequest", arg0) + ret0, _ := ret[0].(*request.Request) + ret1, _ := ret[1].(*sts.GetAccessKeyInfoOutput) + return ret0, ret1 +} + +// GetAccessKeyInfoRequest indicates an expected call of GetAccessKeyInfoRequest +func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfoRequest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfoRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfoRequest), arg0) +} + +// GetAccessKeyInfoWithContext mocks base method +func (m *MockSTSAPI) GetAccessKeyInfoWithContext(arg0 context.Context, arg1 *sts.GetAccessKeyInfoInput, arg2 ...request.Option) (*sts.GetAccessKeyInfoOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAccessKeyInfoWithContext", varargs...) + ret0, _ := ret[0].(*sts.GetAccessKeyInfoOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAccessKeyInfoWithContext indicates an expected call of GetAccessKeyInfoWithContext +func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfoWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfoWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfoWithContext), varargs...) +} + +// GetCallerIdentity mocks base method +func (m *MockSTSAPI) GetCallerIdentity(arg0 *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCallerIdentity", arg0) + ret0, _ := ret[0].(*sts.GetCallerIdentityOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCallerIdentity indicates an expected call of GetCallerIdentity +func (mr *MockSTSAPIMockRecorder) GetCallerIdentity(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentity", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentity), arg0) +} + +// GetCallerIdentityRequest mocks base method +func (m *MockSTSAPI) GetCallerIdentityRequest(arg0 *sts.GetCallerIdentityInput) (*request.Request, *sts.GetCallerIdentityOutput) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCallerIdentityRequest", arg0) + ret0, _ := ret[0].(*request.Request) + ret1, _ := ret[1].(*sts.GetCallerIdentityOutput) + return ret0, ret1 +} + +// GetCallerIdentityRequest indicates an expected call of GetCallerIdentityRequest +func (mr *MockSTSAPIMockRecorder) GetCallerIdentityRequest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentityRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentityRequest), arg0) +} + +// GetCallerIdentityWithContext mocks base method +func (m *MockSTSAPI) GetCallerIdentityWithContext(arg0 context.Context, arg1 *sts.GetCallerIdentityInput, arg2 ...request.Option) (*sts.GetCallerIdentityOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetCallerIdentityWithContext", varargs...) + ret0, _ := ret[0].(*sts.GetCallerIdentityOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCallerIdentityWithContext indicates an expected call of GetCallerIdentityWithContext +func (mr *MockSTSAPIMockRecorder) GetCallerIdentityWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentityWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentityWithContext), varargs...) +} + +// GetFederationToken mocks base method +func (m *MockSTSAPI) GetFederationToken(arg0 *sts.GetFederationTokenInput) (*sts.GetFederationTokenOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFederationToken", arg0) + ret0, _ := ret[0].(*sts.GetFederationTokenOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFederationToken indicates an expected call of GetFederationToken +func (mr *MockSTSAPIMockRecorder) GetFederationToken(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationToken", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationToken), arg0) +} + +// GetFederationTokenRequest mocks base method +func (m *MockSTSAPI) GetFederationTokenRequest(arg0 *sts.GetFederationTokenInput) (*request.Request, *sts.GetFederationTokenOutput) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFederationTokenRequest", arg0) + ret0, _ := ret[0].(*request.Request) + ret1, _ := ret[1].(*sts.GetFederationTokenOutput) + return ret0, ret1 +} + +// GetFederationTokenRequest indicates an expected call of GetFederationTokenRequest +func (mr *MockSTSAPIMockRecorder) GetFederationTokenRequest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationTokenRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationTokenRequest), arg0) +} + +// GetFederationTokenWithContext mocks base method +func (m *MockSTSAPI) GetFederationTokenWithContext(arg0 context.Context, arg1 *sts.GetFederationTokenInput, arg2 ...request.Option) (*sts.GetFederationTokenOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetFederationTokenWithContext", varargs...) + ret0, _ := ret[0].(*sts.GetFederationTokenOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFederationTokenWithContext indicates an expected call of GetFederationTokenWithContext +func (mr *MockSTSAPIMockRecorder) GetFederationTokenWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationTokenWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationTokenWithContext), varargs...) +} + +// GetSessionToken mocks base method +func (m *MockSTSAPI) GetSessionToken(arg0 *sts.GetSessionTokenInput) (*sts.GetSessionTokenOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSessionToken", arg0) + ret0, _ := ret[0].(*sts.GetSessionTokenOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSessionToken indicates an expected call of GetSessionToken +func (mr *MockSTSAPIMockRecorder) GetSessionToken(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionToken", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionToken), arg0) +} + +// GetSessionTokenRequest mocks base method +func (m *MockSTSAPI) GetSessionTokenRequest(arg0 *sts.GetSessionTokenInput) (*request.Request, *sts.GetSessionTokenOutput) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSessionTokenRequest", arg0) + ret0, _ := ret[0].(*request.Request) + ret1, _ := ret[1].(*sts.GetSessionTokenOutput) + return ret0, ret1 +} + +// GetSessionTokenRequest indicates an expected call of GetSessionTokenRequest +func (mr *MockSTSAPIMockRecorder) GetSessionTokenRequest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionTokenRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionTokenRequest), arg0) +} + +// GetSessionTokenWithContext mocks base method +func (m *MockSTSAPI) GetSessionTokenWithContext(arg0 context.Context, arg1 *sts.GetSessionTokenInput, arg2 ...request.Option) (*sts.GetSessionTokenOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSessionTokenWithContext", varargs...) + ret0, _ := ret[0].(*sts.GetSessionTokenOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSessionTokenWithContext indicates an expected call of GetSessionTokenWithContext +func (mr *MockSTSAPIMockRecorder) GetSessionTokenWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionTokenWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionTokenWithContext), varargs...) +} diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx index 560035a1a91..5f510208a8b 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx @@ -42,6 +42,7 @@ const setup = (propOverrides?: object) => { }, jsonData: { assumeRoleArn: '', + externalId: '', database: '', customMetricsNamespaces: '', authType: 'keys', diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx index cd36315f3f3..89cb8bc6d19 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx @@ -130,6 +130,7 @@ export class ConfigEditor extends PureComponent { onChange={option => { if (options.jsonData.authType === 'arn' && option.value !== 'arn') { delete this.props.options.jsonData.assumeRoleArn; + delete this.props.options.jsonData.externalId; } onUpdateDatasourceJsonDataOptionSelect(this.props, 'authType')(option); }} @@ -239,6 +240,22 @@ export class ConfigEditor extends PureComponent { /> +
+ + External ID + +
+ +
+
)}
diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index 8bbb9bf911d..5bab1b6957d 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -59,6 +59,7 @@ export type SelectableStrings = Array>; export interface CloudWatchJsonData extends DataSourceJsonData { timeField?: string; assumeRoleArn?: string; + externalId?: string; database?: string; customMetricsNamespaces?: string; } From e25c6db7e98ebe64cfe5db3208e6bcef88b6cd88 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 2 Jul 2020 15:43:12 +0200 Subject: [PATCH 07/78] Docker: Make sure to create default plugin provisioning directory (#26017) --- packaging/docker/Dockerfile | 1 + packaging/docker/ubuntu.Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index fc84033f4b1..fed434719fd 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -49,6 +49,7 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \ mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ "$GF_PATHS_PROVISIONING/dashboards" \ "$GF_PATHS_PROVISIONING/notifiers" \ + "$GF_PATHS_PROVISIONING/plugins" \ "$GF_PATHS_LOGS" \ "$GF_PATHS_PLUGINS" \ "$GF_PATHS_DATA" && \ diff --git a/packaging/docker/ubuntu.Dockerfile b/packaging/docker/ubuntu.Dockerfile index dbeef849d6c..c474db5919e 100644 --- a/packaging/docker/ubuntu.Dockerfile +++ b/packaging/docker/ubuntu.Dockerfile @@ -39,6 +39,7 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \ mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ "$GF_PATHS_PROVISIONING/dashboards" \ "$GF_PATHS_PROVISIONING/notifiers" \ + "$GF_PATHS_PROVISIONING/plugins" \ "$GF_PATHS_LOGS" \ "$GF_PATHS_PLUGINS" \ "$GF_PATHS_DATA" && \ From 66a00ee5c948dd48b39e850e0b38f7463fd765e1 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Thu, 2 Jul 2020 16:48:30 +0300 Subject: [PATCH 08/78] Chore: added changelog for 7.1.0-beta2 (#26015) * Chore: added changelog for 7.1.0-beta2 * Update Changelog.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cba577323b8..c2672cecf4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 7.1.0-beta 2 (2020-07-02) + +### Features / Enhancements +* **Loki**: Allow aliasing Loki queries in dashboard. [#25706](https://github.com/grafana/grafana/pull/25706), [@bastjan](https://github.com/bastjan) + +### Bug Fixes +* **Explore**: Fix href when jumping from Explore to Add data source. [#25991](https://github.com/grafana/grafana/pull/25991), [@ivanahuckova](https://github.com/ivanahuckova) +* **Fix**: Build-in plugins failed to load in windows. [#25982](https://github.com/grafana/grafana/pull/25982), [@papagian](https://github.com/papagian) + # 7.1.0-beta 1 (2020-07-01) ### Features / Enhancements From 3720c2563843a083d77dbda81f71c5d67f758123 Mon Sep 17 00:00:00 2001 From: Maksim Nabokikh <32434187+nabokihms@users.noreply.github.com> Date: Thu, 2 Jul 2020 18:29:10 +0400 Subject: [PATCH 09/78] grafana-cli: Add ability to read password from stdin to reset admin password (#26016) * grafana-cli: Add ability to read password from stdin to reset admin password Signed-off-by: m.nabokikh Co-authored-by: Arve Knudsen --- pkg/cmd/grafana-cli/commands/commands.go | 7 +++++++ .../commands/reset_password_command.go | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 269a0c917fb..61c8dd38d05 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -111,6 +111,13 @@ var adminCommands = []*cli.Command{ Name: "reset-admin-password", Usage: "reset-admin-password ", Action: runDbCommand(resetPasswordCommand), + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "password-from-stdin", + Usage: "Read the password from stdin", + Value: false, + }, + }, }, { Name: "data-migration", diff --git a/pkg/cmd/grafana-cli/commands/reset_password_command.go b/pkg/cmd/grafana-cli/commands/reset_password_command.go index 8b99f85814d..1faa6ba9088 100644 --- a/pkg/cmd/grafana-cli/commands/reset_password_command.go +++ b/pkg/cmd/grafana-cli/commands/reset_password_command.go @@ -1,7 +1,9 @@ package commands import ( + "bufio" "fmt" + "os" "github.com/fatih/color" "github.com/grafana/grafana/pkg/bus" @@ -16,7 +18,22 @@ import ( const AdminUserId = 1 func resetPasswordCommand(c utils.CommandLine, sqlStore *sqlstore.SqlStore) error { - newPassword := c.Args().First() + newPassword := "" + + if c.Bool("password-from-stdin") { + logger.Infof("New Password: ") + + scanner := bufio.NewScanner(os.Stdin) + if ok := scanner.Scan(); !ok { + if err := scanner.Err(); err != nil { + return fmt.Errorf("can't read password from stdin: %w", err) + } + return fmt.Errorf("can't read password from stdin") + } + newPassword = scanner.Text() + } else { + newPassword = c.Args().First() + } password := models.Password(newPassword) if password.IsWeak() { From b765b4130f8f29356fd9f6755660608f8818aff4 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Thu, 2 Jul 2020 17:49:42 +0300 Subject: [PATCH 10/78] Chore: updated testing in latest.json to 7.1.0-beta2 (#26018) --- latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest.json b/latest.json index 1e11931a0e2..3998cdbff30 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { "stable": "7.0.5", - "testing": "7.1.0-beta1" + "testing": "7.1.0-beta2" } From 45bbee2dea4bedb21568768b328dc5434b70610d Mon Sep 17 00:00:00 2001 From: ChrisDGH <61090940+ChrisDGH@users.noreply.github.com> Date: Thu, 2 Jul 2020 15:03:03 -0400 Subject: [PATCH 11/78] Update _index.md --- docs/sources/developers/plugins/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/developers/plugins/_index.md b/docs/sources/developers/plugins/_index.md index 025fe98ed7a..0bfe60d81b8 100644 --- a/docs/sources/developers/plugins/_index.md +++ b/docs/sources/developers/plugins/_index.md @@ -1,5 +1,5 @@ +++ -title = "Build a plugin" +title = "Build a plugin." type = "docs" +++ From 02a46a5d613b0797d4452fd916ec33c64e2af934 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 3 Jul 2020 08:53:54 +0200 Subject: [PATCH 12/78] Migrate: Error page (404) (#26010) * first things * simple migration and remove angular part --- .../core/components/ErrorPage/ErrorPage.tsx | 90 +++++++++++++++++++ public/app/core/controllers/all.ts | 1 - public/app/core/controllers/error_ctrl.ts | 24 ----- public/app/core/reducers/navModel.ts | 17 +++- public/app/partials/error.html | 50 ----------- public/app/routes/ReactContainer.tsx | 2 +- public/app/routes/routes.ts | 8 +- 7 files changed, 112 insertions(+), 80 deletions(-) create mode 100644 public/app/core/components/ErrorPage/ErrorPage.tsx delete mode 100644 public/app/core/controllers/error_ctrl.ts delete mode 100644 public/app/partials/error.html diff --git a/public/app/core/components/ErrorPage/ErrorPage.tsx b/public/app/core/components/ErrorPage/ErrorPage.tsx new file mode 100644 index 00000000000..c9740f40eb7 --- /dev/null +++ b/public/app/core/components/ErrorPage/ErrorPage.tsx @@ -0,0 +1,90 @@ +import React, { PureComponent } from 'react'; +import { connect, MapStateToProps } from 'react-redux'; +import { NavModel } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { Icon } from '@grafana/ui'; +import Page from '../Page/Page'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { StoreState } from 'app/types'; + +interface ConnectedProps { + navModel: NavModel; +} + +interface OwnProps {} + +type Props = ConnectedProps; + +export class ErrorPage extends PureComponent { + render() { + const { navModel } = this.props; + return ( + + +
+
+
+
+
+

100%

+

80%

+

60%

+

40%

+

20%

+

0%

+
+
+ graph +
+

Then

+

Now

+
+
+
+
+
+
+

current

+
+
+ +
+
+

Chances you are on the page you are looking for.

+

0%

+
+
+

Sorry for the inconvenience

+

+ Please go back to your{' '} + + home dashboard + {' '} + and try again. +

+

+ If the error persists, seek help on the{' '} + + community site + + . +

+
+
+
+
+
+
+
+
+ ); + } +} + +const mapStateToProps: MapStateToProps = state => { + return { + navModel: getNavModel(state.navIndex, 'not-found'), + }; +}; + +export default connect(mapStateToProps)(ErrorPage); diff --git a/public/app/core/controllers/all.ts b/public/app/core/controllers/all.ts index ab658d75acf..f7f9f4c0486 100644 --- a/public/app/core/controllers/all.ts +++ b/public/app/core/controllers/all.ts @@ -1,5 +1,4 @@ import './invited_ctrl'; import './signup_ctrl'; import './reset_password_ctrl'; -import './error_ctrl'; import './json_editor_ctrl'; diff --git a/public/app/core/controllers/error_ctrl.ts b/public/app/core/controllers/error_ctrl.ts deleted file mode 100644 index 780b0ecfce0..00000000000 --- a/public/app/core/controllers/error_ctrl.ts +++ /dev/null @@ -1,24 +0,0 @@ -import config from 'app/core/config'; -import coreModule from '../core_module'; -import appEvents from 'app/core/app_events'; -import { CoreEvents } from 'app/types'; - -export class ErrorCtrl { - /** @ngInject */ - constructor($scope: any, contextSrv: any, navModelSrv: any) { - $scope.navModel = navModelSrv.getNotFoundNav(); - $scope.appSubUrl = config.appSubUrl; - - if (!contextSrv.isSignedIn) { - appEvents.emit(CoreEvents.toggleSidemenuHidden); - } - - $scope.$on('destroy', () => { - if (!contextSrv.isSignedIn) { - appEvents.emit(CoreEvents.toggleSidemenuHidden); - } - }); - } -} - -coreModule.controller('ErrorCtrl', ErrorCtrl); diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 2c89a509e9a..d974e82efc7 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -1,5 +1,5 @@ import { AnyAction, createAction } from '@reduxjs/toolkit'; -import { NavIndex, NavModelItem } from '@grafana/data'; +import { NavIndex, NavModel, NavModelItem } from '@grafana/data'; import config from 'app/core/config'; @@ -21,6 +21,21 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem? buildNavIndex(navIndex, node.children, node); } } + + navIndex['not-found'] = { ...buildWarningNav('Page not found', '404 Error').node }; +} + +function buildWarningNav(text: string, subTitle?: string): NavModel { + const node = { + text, + subTitle, + icon: 'exclamation-triangle', + }; + return { + breadcrumbs: [node], + node: node, + main: node, + }; } export const initialState: NavIndex = {}; diff --git a/public/app/partials/error.html b/public/app/partials/error.html deleted file mode 100644 index c25addeba51..00000000000 --- a/public/app/partials/error.html +++ /dev/null @@ -1,50 +0,0 @@ - - -
-
-
-
-
-

100%

-

80%

-

60%

-

40%

-

20%

-

0%

-
-
- -
-

Then

-

Now

-
-
-
-
-
-
-

current

-
-
- -
-
-

Chances you are on the page you are looking for.

-

0%

-
-
-

Sorry for the inconvenience

-

Please go back to your home dashboard and try again.

-

- If the error persists, seek help on the - community site. -

-
-
-
-
- -
-
- -
diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index b5336eb4697..75dc116551f 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -63,7 +63,7 @@ export function reactContainer( $rootScope: $rootScope, $scope: scope, $contextSrv: contextSrv, - routeInfo: $route.current.$$route.routeInfo, + routeInfo: $route.current.$$route?.routeInfo, }; document.body.classList.add('is-react'); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 4280e818f58..bf54da8fb77 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -538,9 +538,11 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati reloadOnSearch: false, }) .otherwise({ - templateUrl: 'public/app/partials/error.html', - controller: 'ErrorCtrl', - reloadOnSearch: false, + template: '', + resolve: { + component: () => + SafeDynamicImport(import(/* webpackChunkName: "ErrorPage" */ 'app/core/components/ErrorPage/ErrorPage')), + }, }); applyRouteRegistrationHandlers($routeProvider); From b7792de16da66deb89ba673f5f018fdadd25b8ea Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 3 Jul 2020 00:07:17 -0700 Subject: [PATCH 13/78] grafana/data: do not bundle rxjs (#26039) --- packages/grafana-data/rollup.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts index dc5e576457b..68c5461fd47 100644 --- a/packages/grafana-data/rollup.config.ts +++ b/packages/grafana-data/rollup.config.ts @@ -21,7 +21,7 @@ const buildCjsPackage = ({ env }) => { globals: {}, }, ], - external: ['lodash', 'apache-arrow'], // Use Lodash & arrow from grafana + external: ['lodash', 'rxjs', 'apache-arrow'], // Use Lodash, rxjs & arrow from grafana plugins: [ json({ include: ['../../node_modules/moment-timezone/data/packed/latest.json'], From 8b4665536178209ff4493f367dd0ea119d1d8dff Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 3 Jul 2020 09:10:59 +0200 Subject: [PATCH 14/78] Elastic: Fix displaying of correct log message (#26020) * Fix default field, remove redundant line field check * Add comments --- public/app/core/logs_model.ts | 5 +---- public/app/plugins/datasource/loki/result_transformer.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index bb06277de65..ba0b3438a2f 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -319,10 +319,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi // Find the fields we care about and collect all labels const allSeries: LogFields[] = logSeries.map(series => { const fieldCache = new FieldCache(series); - - const stringField = fieldCache.hasFieldNamed('line') - ? fieldCache.getFieldByName('line') - : fieldCache.getFirstFieldOfType(FieldType.string); + const stringField = fieldCache.getFirstFieldOfType(FieldType.string); if (stringField?.labels) { allLabels.push(stringField.labels); } diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 8c2ba7409da..c8ea38fd9ec 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -78,7 +78,7 @@ function constructDataFrame( refId, fields: [ { name: 'ts', type: FieldType.time, config: { displayName: 'Time' }, values: times }, // Time - { name: 'line', type: FieldType.string, config: {}, values: lines, labels }, // Line + { 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: 'tsNs', type: FieldType.time, config: { displayName: 'Time ns' }, values: timesNs }, // Time ], From dec76b4556d5cba805178a57fe652bb2bb8f457d Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Fri, 3 Jul 2020 03:14:44 -0400 Subject: [PATCH 15/78] Graph panel: Move Stacking and null values before Hover tooltip options (#26037) --- .../panels/visualizations/graph-panel.md | 22 +++---- .../app/plugins/panel/graph/tab_display.html | 64 +++++++++---------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/docs/sources/panels/visualizations/graph-panel.md b/docs/sources/panels/visualizations/graph-panel.md index fd75ab98130..1059df67ad5 100644 --- a/docs/sources/panels/visualizations/graph-panel.md +++ b/docs/sources/panels/visualizations/graph-panel.md @@ -32,6 +32,17 @@ Use these settings to refine your visualization. - **Points -** Display points for values. - **Point radius -** Controls how large the points are. +### Stacking and null value + +- **Stack -** Each series is stacked on top of another. +- **Percent -** Available when **Stack** is selected. Each series is drawn as a percentage of the total of all series. +- **Null value -** How null values are displayed. _This is a very important setting._ See note below. + - **connected -** If there is a gap in the series, meaning a null value or values, then the line will skip the gap and connect to the next non-null value. + - **null -** (default) If there is a gap in the series, meaning a null value, then the line in the graph will be broken and show the gap. + - **null as zero -** If there is a gap in the series, meaning a null value, then it will be displayed as a zero value in the graph panel. + +> **Note:** If you are monitoring a server's CPU load and the load reaches 100%, then the server will lock up and the agent sending statistics will not be able to collect the load statistic. This leads to a gap in the metrics and having the default as _null_ means Grafana will show the gaps and indicate that something is wrong. If this is set to _connected_, then it would be easy to miss this signal. + ### Hover tooltip Use these settings to change the appearance of the tooltip that appears when you hover your cursor over the graph visualization. @@ -44,17 +55,6 @@ Use these settings to change the appearance of the tooltip that appears when you - **Increasing -** The series in the hover tooltip are sorted by value and in increasing order, with the lowest value at the top of the list. - **Decreasing -** The series in the hover tooltip are sorted by value and in decreasing order, with the highest value at the top of the list. -### Stacking and null value - -- **Stack -** Each series is stacked on top of another. -- **Percent -** Available when **Stack** is selected. Each series is drawn as a percentage of the total of all series. -- **Null value -** How null values are displayed. _This is a very important setting._ See note below. - - **connected -** If there is a gap in the series, meaning a null value or values, then the line will skip the gap and connect to the next non-null value. - - **null -** (default) If there is a gap in the series, meaning a null value, then the line in the graph will be broken and show the gap. - - **null as zero -** If there is a gap in the series, meaning a null value, then it will be displayed as a zero value in the graph panel. - -> **Note:** If you are monitoring a server's CPU load and the load reaches 100%, then the server will lock up and the agent sending statistics will not be able to collect the load statistic. This leads to a gap in the metrics and having the default as _null_ means Grafana will show the gaps and indicate that something is wrong. If this is set to _connected_, then it would be easy to miss this signal. - ## Series overrides Series overrides allow a series in a graph panel to be rendered differently from the others. You can customize display options on a per-series bases or by using regex rules. For example, one series can have a thicker line width to make it stand out or be moved to the right Y-axis. diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index dac0887fe57..c17177ac5c2 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -81,6 +81,38 @@
+
+
Stacking and null value
+ + + + +
+ +
+ +
+
+
+
Hover tooltip
@@ -117,35 +149,3 @@
- -
-
Stacking and null value
- - - - -
- -
- -
-
-
From b06d2cf30f5dc4e690263025d69852c2e1853962 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Fri, 3 Jul 2020 09:24:36 +0200 Subject: [PATCH 16/78] AdminUsersTable: Fix width (#26019) --- .../app/features/admin/UserListAdminPage.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index 8c676cccdf4..6c27070a7d1 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -99,14 +99,20 @@ const renderUser = (user: UserDTO) => { - - {user.login} + + + {user.login} + - - {user.email} + + + {user.email} + - - {user.name} + + + {user.name} + {user.lastSeenAtAge && {user.lastSeenAtAge}} From 634d8d60d6e4a10e7400c97ee82ef15b6c298096 Mon Sep 17 00:00:00 2001 From: Steven Vachon Date: Fri, 3 Jul 2020 04:22:56 -0400 Subject: [PATCH 17/78] @grafana/e2e: close options panel before interacting with the query form (#26036) ... it's logically better, but the real reason is to appease Cypress which was cause a consistent request error for a single plugin (datadog-datasource). An error which could not be reproduced manually. --- packages/grafana-e2e/src/flows/addPanel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-e2e/src/flows/addPanel.ts b/packages/grafana-e2e/src/flows/addPanel.ts index 298439c1670..8e4b403ba7d 100644 --- a/packages/grafana-e2e/src/flows/addPanel.ts +++ b/packages/grafana-e2e/src/flows/addPanel.ts @@ -68,6 +68,8 @@ export const addPanel = (config?: Partial): any => .click(); closeOptionsGroup('type'); + closeOptions(); + queriesForm(fullConfig); e2e().wait('@chartData'); @@ -77,8 +79,6 @@ export const addPanel = (config?: Partial): any => //e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content').contains('No data'); //e2e.components.QueryEditorRow.actionButton('Disable/enable query').click(); - closeOptions(); - e2e() .get('button[title="Apply changes and go back to dashboard"]') .click(); From 3acc2a6ac2e91fe485fb88c48dd71b0560e62324 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 3 Jul 2020 01:23:32 -0700 Subject: [PATCH 18/78] Table: JSON Cell should try to convert strings to JSON (#26024) --- .../src/components/Table/JSONViewCell.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx index b642be28efc..7784f540413 100644 --- a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx @@ -3,6 +3,7 @@ import { css, cx } from 'emotion'; import { TableCellProps } from './types'; import { Tooltip } from '../Tooltip/Tooltip'; import { JSONFormatter } from '../JSONFormatter/JSONFormatter'; +import { isString } from 'lodash'; export const JSONViewCell: FC = props => { const { field, cell, tableStyles } = props; @@ -16,8 +17,16 @@ export const JSONViewCell: FC = props => { font-family: monospace; `; - const displayValue = JSON.stringify(cell.value); - const content = ; + let value = cell.value; + let displayValue = value; + if (isString(value)) { + try { + value = JSON.parse(value); + } catch {} // ignore errors + } else { + displayValue = JSON.stringify(value); + } + const content = ; return (
From 66460ae740f04d2f167618931bcc04847a1196c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 3 Jul 2020 14:58:29 +0200 Subject: [PATCH 19/78] InfluxDB: Fixed new group by dropdown now showing (#26031) --- public/app/plugins/datasource/influxdb/query_builder.ts | 6 ++++++ public/app/plugins/datasource/influxdb/query_ctrl.ts | 2 ++ .../datasource/influxdb/specs/query_builder.test.ts | 9 +++++++++ 3 files changed, 17 insertions(+) diff --git a/public/app/plugins/datasource/influxdb/query_builder.ts b/public/app/plugins/datasource/influxdb/query_builder.ts index 49e17ab39ba..023dec10e40 100644 --- a/public/app/plugins/datasource/influxdb/query_builder.ts +++ b/public/app/plugins/datasource/influxdb/query_builder.ts @@ -90,6 +90,12 @@ export class InfluxQueryBuilder { if (tag.key === withKey) { return memo; } + + // value operators not supported in these types of queries + if (tag.operator === '>' || tag.operator === '<') { + return memo; + } + memo.push(renderTagCondition(tag, memo.length)); return memo; }, diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index e68d5830ee8..edfabdb9d09 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -167,6 +167,7 @@ export class InfluxQueryCtrl extends QueryCtrl { const plusButton = this.uiSegmentSrv.newPlusButton(); this.groupBySegment.value = plusButton.value; this.groupBySegment.html = plusButton.html; + this.groupBySegment.fake = true; this.panelCtrl.refresh(); } @@ -308,6 +309,7 @@ export class InfluxQueryCtrl extends QueryCtrl { if (segment.type === 'condition') { return Promise.resolve([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); } + if (segment.type === 'operator') { const nextValue = this.tagSegments[index + 1].value; if (/^\/.*\/$/.test(nextValue)) { diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts index d8917946eb5..6543d5a77d5 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts @@ -32,6 +32,15 @@ describe('InfluxQueryBuilder', () => { expect(query).toBe('SHOW TAG KEYS WHERE "host" = \'se1\''); }); + it('should ignore condition if operator is a value operator', () => { + const builder = new InfluxQueryBuilder({ + measurement: '', + tags: [{ key: 'value', value: '10', operator: '>' }], + }); + const query = builder.buildExploreQuery('TAG_KEYS'); + expect(query).toBe('SHOW TAG KEYS'); + }); + it('should have no conditions in measurement query for query with no tags', () => { const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); const query = builder.buildExploreQuery('MEASUREMENTS'); From 081f954a2b538edd6af56d42dba1b580f71a191e Mon Sep 17 00:00:00 2001 From: David Date: Fri, 3 Jul 2020 15:04:57 +0200 Subject: [PATCH 20/78] Explore: Don't run queries on datasource change (#26033) - more and more datasources are having long-running queries, automatically triggering is becoming more of a burden than a help. - some datasource queries might actually cost money, so running queries should be explicit. --- public/app/features/explore/state/actions.test.ts | 4 +++- public/app/features/explore/state/actions.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/public/app/features/explore/state/actions.test.ts b/public/app/features/explore/state/actions.test.ts index 492be9b5d42..c8c8d6b1750 100644 --- a/public/app/features/explore/state/actions.test.ts +++ b/public/app/features/explore/state/actions.test.ts @@ -259,7 +259,7 @@ describe('changing datasource', () => { jest.spyOn(Actions, 'importQueries').mockImplementationOnce(() => jest.fn); jest.spyOn(Actions, 'loadDatasource').mockImplementationOnce(() => jest.fn); - jest.spyOn(Actions, 'runQueries').mockImplementationOnce(() => jest.fn); + const runQueriesAction = jest.spyOn(Actions, 'runQueries').mockImplementationOnce(() => jest.fn); const dispatchedActions = await thunkTester(initialState) .givenThunk(changeDatasource) .whenThunkIsDispatched(exploreId, name); @@ -272,6 +272,8 @@ describe('changing datasource', () => { mode: ExploreMode.Logs, }), ]); + // Don't run queries just on datasource change + expect(runQueriesAction).toHaveBeenCalledTimes(0); }); }); diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 809d507a657..b4db347e723 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -157,7 +157,6 @@ export function changeDatasource(exploreId: ExploreId, datasourceName: string): } await dispatch(loadDatasource(exploreId, newDataSourceInstance, orgId)); - dispatch(runQueries(exploreId)); }; } @@ -265,11 +264,12 @@ export function loadExploreDatasourcesAndSetDatasource( exploreId: ExploreId, datasourceName: string ): ThunkResult { - return dispatch => { + return async dispatch => { const exploreDatasources = getExploreDatasources(); if (exploreDatasources.length >= 1) { - dispatch(changeDatasource(exploreId, datasourceName)); + await dispatch(changeDatasource(exploreId, datasourceName)); + dispatch(runQueries(exploreId)); } else { dispatch(loadDatasourceMissingAction({ exploreId })); } From 26852ca788b83a4c757d5495dd0b57103f314acb Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 3 Jul 2020 15:47:46 +0200 Subject: [PATCH 21/78] Instrument dashboard versions and annotation count (#26044) --- pkg/infra/metrics/metrics.go | 20 ++++++++++++++++++++ pkg/infra/usagestats/usage_stats.go | 4 ++++ pkg/infra/usagestats/usage_stats_test.go | 4 ++++ pkg/models/stats.go | 2 ++ pkg/services/sqlstore/stats.go | 2 ++ 5 files changed, 32 insertions(+) diff --git a/pkg/infra/metrics/metrics.go b/pkg/infra/metrics/metrics.go index f4bf21d66cd..4f51e4f36b6 100644 --- a/pkg/infra/metrics/metrics.go +++ b/pkg/infra/metrics/metrics.go @@ -159,6 +159,12 @@ var ( // StatsTotalDataSources is a metric total number of defined datasources, labeled by pluginId StatsTotalDataSources *prometheus.GaugeVec + // StatsTotalAnnotations is a metric of total number of annotations stored in Grafana. + StatsTotalAnnotations prometheus.Gauge + + // StatsTotalDashboardVersions is a metric of total number of dashboard versions stored in Grafana. + StatsTotalDashboardVersions prometheus.Gauge + // grafanaBuildVersion is a metric with a constant '1' value labeled by version, revision, branch, and goversion from which Grafana was built grafanaBuildVersion *prometheus.GaugeVec @@ -483,6 +489,18 @@ func init() { Help: "A metric with a constant '1' value labeled by pluginId, pluginType and version from which Grafana plugin was built", Namespace: ExporterName, }, []string{"plugin_id", "plugin_type", "version"}) + + StatsTotalDashboardVersions = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_totals_dashboard_versions", + Help: "total amount of dashboard versions in the database", + Namespace: ExporterName, + }) + + StatsTotalAnnotations = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_totals_annotations", + Help: "total amount of annotations in the database", + Namespace: ExporterName, + }) } // SetBuildInformation sets the build information for this binary @@ -550,6 +568,8 @@ func initMetricVars() { StatsTotalDataSources, grafanaBuildVersion, grafanaPluginBuildInfoDesc, + StatsTotalDashboardVersions, + StatsTotalAnnotations, ) } diff --git a/pkg/infra/usagestats/usage_stats.go b/pkg/infra/usagestats/usage_stats.go index 6d22e55d03c..36f5c4ebe8e 100644 --- a/pkg/infra/usagestats/usage_stats.go +++ b/pkg/infra/usagestats/usage_stats.go @@ -61,6 +61,8 @@ func (uss *UsageStatsService) sendUsageStats(oauthProviders map[string]bool) { 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.valid_license.count"] = getValidLicenseCount(uss.License.HasValidLicense()) metrics["stats.edition.oss.count"] = getOssEditionCount() metrics["stats.edition.enterprise.count"] = getEnterpriseEditionCount() @@ -212,6 +214,8 @@ func (uss *UsageStatsService) updateTotalStats() { 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)) dsStats := models.GetDataSourceStatsQuery{} if err := uss.Bus.Dispatch(&dsStats); err != nil { diff --git a/pkg/infra/usagestats/usage_stats_test.go b/pkg/infra/usagestats/usage_stats_test.go index afa49360cc4..917ccd6d3a2 100644 --- a/pkg/infra/usagestats/usage_stats_test.go +++ b/pkg/infra/usagestats/usage_stats_test.go @@ -50,6 +50,8 @@ func TestMetrics(t *testing.T) { Snapshots: 13, Teams: 14, AuthTokens: 15, + DashboardVersions: 16, + Annotations: 17, } getSystemStatsQuery = query return nil @@ -238,6 +240,8 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.teams.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Teams) So(metrics.Get("stats.total_auth_token.count").MustInt64(), ShouldEqual, 15) So(metrics.Get("stats.avg_auth_token_per_user.count").MustInt64(), ShouldEqual, 5) + So(metrics.Get("stats.dashboard_versions.count").MustInt64(), ShouldEqual, 16) + So(metrics.Get("stats.annotations.count").MustInt64(), ShouldEqual, 17) So(metrics.Get("stats.ds."+models.DS_ES+".count").MustInt(), ShouldEqual, 9) So(metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt(), ShouldEqual, 10) diff --git a/pkg/models/stats.go b/pkg/models/stats.go index 498cdd5c0bf..05df8182566 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -16,6 +16,8 @@ type SystemStats struct { Folders int64 ProvisionedDashboards int64 AuthTokens int64 + DashboardVersions int64 + Annotations int64 Admins int Editors int diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 6f80a8017e6..685a0783ee4 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -75,6 +75,8 @@ func GetSystemStats(query *models.GetSystemStatsQuery) error { sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`) + sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_version") + `) AS dashboard_versions,`) + sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("annotation") + `) AS annotations,`) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams,`) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("user_auth_token") + `) AS auth_tokens,`) From c0762b6ddc60dc2c251cd5c1428a031448c357fe Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 3 Jul 2020 08:49:29 -0700 Subject: [PATCH 22/78] Chore: reduce null check errors/warnigns (#25223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix a few null errors * more * flip logic * merge master * more * fewer changes * processor Co-authored-by: Torkel Ödegaard --- .../src/dataframe/DataFrameView.ts | 6 +-- .../datasource/mixed/MixedDataSource.ts | 4 +- public/app/plugins/panel/alertlist/module.ts | 6 +-- .../plugins/panel/annolist/AnnoListPanel.tsx | 45 +++++++++---------- .../plugins/panel/bargauge/BarGaugePanel.tsx | 9 +++- .../app/plugins/panel/graph/Legend/Legend.tsx | 25 ++++++----- .../panel/graph/Legend/LegendSeriesItem.tsx | 4 +- public/app/plugins/panel/singlestat/module.ts | 9 ++-- 8 files changed, 60 insertions(+), 48 deletions(-) diff --git a/packages/grafana-data/src/dataframe/DataFrameView.ts b/packages/grafana-data/src/dataframe/DataFrameView.ts index 0568bbc690c..b96055813c9 100644 --- a/packages/grafana-data/src/dataframe/DataFrameView.ts +++ b/packages/grafana-data/src/dataframe/DataFrameView.ts @@ -53,15 +53,15 @@ export class DataFrameView extends FunctionalVector { * Helper function to return the {@link DisplayProcessor} for a given field column. * @param colIndex - the field column index for the data frame. */ - getFieldDisplayProcessor(colIndex: number): DisplayProcessor | null { + getFieldDisplayProcessor(colIndex: number): DisplayProcessor | undefined { if (!this.dataFrame || !this.dataFrame.fields) { - return null; + return undefined; } const field = this.dataFrame.fields[colIndex]; if (!field || !field.display) { - return null; + return undefined; } return field.display; diff --git a/public/app/plugins/datasource/mixed/MixedDataSource.ts b/public/app/plugins/datasource/mixed/MixedDataSource.ts index 1f171e83a29..3fe232cc8b8 100644 --- a/public/app/plugins/datasource/mixed/MixedDataSource.ts +++ b/public/app/plugins/datasource/mixed/MixedDataSource.ts @@ -40,9 +40,9 @@ export class MixedDatasource extends DataSourceApi { const mixed: BatchedQueries[] = []; for (const key in sets) { const targets = sets[key]; - const dsName = targets[0].datasource; + const dsName: string | undefined = targets[0].datasource; mixed.push({ - datasource: getDataSourceSrv().get(dsName), + datasource: getDataSourceSrv().get(dsName, request.scopedVars), targets, }); } diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index d3faa5ca1f0..f165508f71a 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -90,10 +90,10 @@ class AlertListPanel extends PanelCtrl { if (this.panel.show === 'current') { getAlertsPromise = this.getCurrentAlertState(); - } - - if (this.panel.show === 'changes') { + } else if (this.panel.show === 'changes') { getAlertsPromise = this.getStateChanges(); + } else { + getAlertsPromise = Promise.resolve(); } getAlertsPromise.then(() => { diff --git a/public/app/plugins/panel/annolist/AnnoListPanel.tsx b/public/app/plugins/panel/annolist/AnnoListPanel.tsx index 5f7e9b31510..63342c6b4aa 100644 --- a/public/app/plugins/panel/annolist/AnnoListPanel.tsx +++ b/public/app/plugins/panel/annolist/AnnoListPanel.tsx @@ -4,20 +4,17 @@ import React, { PureComponent } from 'react'; import { AnnoOptions } from './types'; import { AnnotationEvent, AppEvents, dateTime, DurationUnit, PanelProps } from '@grafana/data'; import { Tooltip } from '@grafana/ui'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, getLocationSrv } from '@grafana/runtime'; import { AbstractList } from '@grafana/ui/src/components/List/AbstractList'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import appEvents from 'app/core/app_events'; - -import { updateLocation } from 'app/core/actions'; -import { store } from 'app/store/store'; import { css, cx } from 'emotion'; interface UserInfo { - id: number; - login: string; - email: string; + id?: number; + login?: string; + email?: string; } interface Props extends PanelProps {} @@ -108,6 +105,10 @@ export class AnnoListPanel extends PureComponent { onAnnoClick = (e: React.SyntheticEvent, anno: AnnotationEvent) => { e.stopPropagation(); + if (!anno.time) { + return; + } + const { options } = this.props; const dashboardSrv = getDashboardSrv(); const current = dashboardSrv.getCurrent(); @@ -122,12 +123,10 @@ export class AnnoListPanel extends PureComponent { } if (current.id === anno.dashboardId) { - store.dispatch( - updateLocation({ - query: params, - partial: true, - }) - ); + getLocationSrv().update({ + query: params, + partial: true, + }); return; } @@ -136,12 +135,10 @@ export class AnnoListPanel extends PureComponent { .then((res: any[]) => { if (res && res.length && res[0].id === anno.dashboardId) { const dash = res[0]; - store.dispatch( - updateLocation({ - query: params, - path: dash.url, - }) - ); + getLocationSrv().update({ + query: params, + path: dash.url, + }); return; } appEvents.emit(AppEvents.alertWarning, ['Unknown Dashboard: ' + anno.dashboardId]); @@ -164,7 +161,7 @@ export class AnnoListPanel extends PureComponent { return t.add(incr, unit as DurationUnit).valueOf(); } - onTagClick = (e: React.SyntheticEvent, tag: string, remove: boolean) => { + onTagClick = (e: React.SyntheticEvent, tag: string, remove?: boolean) => { e.stopPropagation(); const queryTags = remove ? this.state.queryTags.filter(item => item !== tag) : [...this.state.queryTags, tag]; @@ -188,7 +185,7 @@ export class AnnoListPanel extends PureComponent { }); }; - renderTags = (tags: string[], remove: boolean): JSX.Element | null => { + renderTags = (tags?: string[], remove?: boolean): JSX.Element | null => { if (!tags || !tags.length) { return null; } @@ -197,7 +194,7 @@ export class AnnoListPanel extends PureComponent { {tags.map(tag => { return ( this.onTagClick(e, tag, remove)} className="pointer"> - + ); })} @@ -251,7 +248,9 @@ export class AnnoListPanel extends PureComponent { {showTags && this.renderTags(anno.tags, false)} - {showTime && {dashboard.formatDate(anno.time)}} + + {showTime && anno.time && {dashboard.formatDate(anno.time)}} +
); diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index 4768c3ba40a..50e9a37eb8c 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -6,6 +6,7 @@ import { getFieldDisplayValues, PanelProps, FieldConfig, + DisplayProcessor, DisplayValue, } from '@grafana/data'; import { BarGauge, DataLinksContextMenu, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui'; @@ -13,6 +14,7 @@ import { BarGauge, DataLinksContextMenu, VizRepeater, VizRepeaterRenderValueProp import { config } from 'app/core/config'; import { BarGaugeOptions } from './types'; import { DataLinksContextMenuApi } from '@grafana/ui/src/components/DataLinks/DataLinksContextMenu'; +import { isNumber } from 'lodash'; export class BarGaugePanel extends PureComponent> { renderComponent = ( @@ -24,6 +26,11 @@ export class BarGaugePanel extends PureComponent> { const { field, display, view, colIndex } = value; const { openMenu, targetClassName } = menuProps; + let processor: DisplayProcessor | undefined = undefined; + if (view && isNumber(colIndex)) { + processor = view!.getFieldDisplayProcessor(colIndex as number); + } + return ( > { height={height} orientation={orientation} field={field} - display={view?.getFieldDisplayProcessor(colIndex)} + display={processor} theme={config.theme} itemSpacing={this.getItemSpacing()} displayMode={options.displayMode} diff --git a/public/app/plugins/panel/graph/Legend/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx index f4cd97a7b59..eb8832fc798 100644 --- a/public/app/plugins/panel/graph/Legend/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -286,18 +286,19 @@ class LegendTable extends PureComponent> { - {seriesList.map((series, i) => ( -