diff --git a/.circleci/config.yml b/.circleci/config.yml index 3dd8f800b94..dba6c5f8bd0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ version: 2 jobs: mysql-integration-test: docker: - - image: circleci/golang:1.11 + - image: circleci/golang:1.11.4 - image: circleci/mysql:5.6-ram environment: MYSQL_ROOT_PASSWORD: rootpass @@ -39,7 +39,7 @@ jobs: postgres-integration-test: docker: - - image: circleci/golang:1.11 + - image: circleci/golang:1.11.4 - image: circleci/postgres:9.3-ram environment: POSTGRES_USER: grafanatest @@ -74,7 +74,7 @@ jobs: gometalinter: docker: - - image: circleci/golang:1.11 + - image: circleci/golang:1.11.4 environment: # we need CGO because of go-sqlite3 CGO_ENABLED: 1 @@ -117,7 +117,7 @@ jobs: test-backend: docker: - - image: circleci/golang:1.11 + - image: circleci/golang:1.11.4 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -175,7 +175,7 @@ jobs: build: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f55381f5e2..5b420dcc374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ * **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483) * **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548) * **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard) +* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas) + +### Bug fixes +* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486) # 5.4.2 (2018-12-13) diff --git a/Dockerfile b/Dockerfile index 65260e1a6a8..c3af89b6092 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Golang build container -FROM golang:1.11 +FROM golang:1.11.4 WORKDIR $GOPATH/src/github.com/grafana/grafana diff --git a/README.md b/README.md index 03fe840d5f1..1ce4ffbe109 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Choose this option to build on platforms other than linux/amd64 and/or not have The resulting image will be tagged as `grafana/grafana:dev` -Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Perferences -> Advanced), otherwize you may faild at `grunt build` +Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Preferences -> Advanced), otherwize you may faild at `grunt build` ### Dev config diff --git a/appveyor.yml b/appveyor.yml index 4bbd3668e19..5f97784dd38 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana environment: nodejs_version: "8" GOPATH: C:\gopath - GOVERSION: 1.11 + GOVERSION: 1.11.4 install: - rmdir c:\go /s /q diff --git a/conf/defaults.ini b/conf/defaults.ini index 2ef2ad7942a..97c87edb8b2 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -335,6 +335,7 @@ tls_skip_verify_insecure = false tls_client_cert = tls_client_key = tls_client_ca = +send_client_credentials_via_post = false #################################### Basic Auth ########################## [auth.basic] diff --git a/conf/sample.ini b/conf/sample.ini index ba65727dc4b..473e4e8450c 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -284,6 +284,10 @@ log_queries = ;tls_client_key = ;tls_client_ca = +; Set to true to enable sending client_id and client_secret via POST body instead of Basic authentication HTTP header +; This might be required if the OAuth provider is not RFC6749 compliant, only supporting credentials passed via POST payload +;send_client_credentials_via_post = false + #################################### Grafana.com Auth #################### [auth.grafana_com] ;enabled = false diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md index 6fa6531fc98..c3c44426ba7 100644 --- a/docs/sources/auth/generic-oauth.md +++ b/docs/sources/auth/generic-oauth.md @@ -17,7 +17,7 @@ can find examples using Okta, BitBucket, OneLogin and Azure. This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/generic_oauth`. -You may have to set the `root_url` option of `[server]` for the callback URL to be +You may have to set the `root_url` option of `[server]` for the callback URL to be correct. For example in case you are serving Grafana behind a proxy. Example config: @@ -209,6 +209,17 @@ allowed_organizations = token_url = https://.my.centrify.com/OAuth2/Token/ ``` +## Set up OAuth2 with non-compliant providers + +Some OAuth2 providers might not support `client_id` and `client_secret` passed via Basic Authentication HTTP header, which +results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be +send via POST body, which can be enabled via the following settings: + + ```bash + [auth.generic_oauth] + send_client_credentials_via_post = true + ``` +
diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 455420e4688..0d54f0707a6 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -164,6 +164,14 @@ func GetPluginMarkdown(c *m.ReqContext) Response { return Error(500, "Could not get markdown file", err) } + // fallback try readme + if len(content) == 0 { + content, err = plugins.GetPluginMarkdown(pluginID, "readme") + if err != nil { + return Error(501, "Could not get markdown file", err) + } + } + resp := Respond(200, content) resp.Header("Content-Type", "text/plain; charset=utf-8") return resp diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go index 04b77a892c5..dd4ae6972aa 100644 --- a/pkg/plugins/datasource_plugin.go +++ b/pkg/plugins/datasource_plugin.go @@ -3,10 +3,8 @@ package plugins import ( "context" "encoding/json" - "os" "os/exec" "path" - "path/filepath" "time" "github.com/grafana/grafana-plugin-model/go/datasource" @@ -29,7 +27,6 @@ type DataSourcePlugin struct { QueryOptions map[string]bool `json:"queryOptions,omitempty"` BuiltIn bool `json:"builtIn,omitempty"` Mixed bool `json:"mixed,omitempty"` - HasQueryHelp bool `json:"hasQueryHelp,omitempty"` Routes []*AppPluginRoute `json:"routes"` Backend bool `json:"backend,omitempty"` @@ -48,15 +45,6 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error { return err } - // look for help markdown - helpPath := filepath.Join(p.PluginDir, "QUERY_HELP.md") - if _, err := os.Stat(helpPath); os.IsNotExist(err) { - helpPath = filepath.Join(p.PluginDir, "query_help.md") - } - if _, err := os.Stat(helpPath); err == nil { - p.HasQueryHelp = true - } - DataSources[p.Id] = p return nil } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 9ce50eadd6b..75c68615750 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -166,7 +166,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] var result notifierStateSlice for _, notification := range query.Result { - not, err := n.createNotifierFor(notification) + not, err := InitNotifier(notification) if err != nil { n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err) continue @@ -195,7 +195,8 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] return result, nil } -func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Notifier, error) { +// InitNotifier instantiate a new notifier based on the model +func InitNotifier(model *m.AlertNotification) (Notifier, error) { notifierPlugin, found := notifierFactories[model.Type] if !found { return nil, errors.New("Unsupported notification type") @@ -208,6 +209,7 @@ type NotifierFactory func(notification *m.AlertNotification) (Notifier, error) var notifierFactories = make(map[string]*NotifierPlugin) +// RegisterNotifier register an notifier func RegisterNotifier(plugin *NotifierPlugin) { notifierFactories[plugin.Type] = plugin } diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index 8aa1b80aa22..b6e59f694c8 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -32,7 +32,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error { Settings: cmd.Settings, } - notifiers, err := notifier.createNotifierFor(model) + notifiers, err := InitNotifier(model) if err != nil { log.Error2("Failed to create notifier", "error", err.Error()) diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 7f70e5c25fc..ccab1106880 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -53,14 +53,14 @@ func GetDataSourceByName(query *m.GetDataSourceByNameQuery) error { } func GetDataSources(query *m.GetDataSourcesQuery) error { - sess := x.Limit(1000, 0).Where("org_id=?", query.OrgId).Asc("name") + sess := x.Limit(5000, 0).Where("org_id=?", query.OrgId).Asc("name") query.Result = make([]*m.DataSource, 0) return sess.Find(&query.Result) } func GetAllDataSources(query *m.GetAllDataSourcesQuery) error { - sess := x.Limit(1000, 0).Asc("name") + sess := x.Limit(5000, 0).Asc("name") query.Result = make([]*m.DataSource, 0) return sess.Find(&query.Result) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index a3ccb93b30c..312877751c9 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -345,8 +345,12 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error { return err } +func newSignedInUserCacheKey(orgID, userID int64) string { + return fmt.Sprintf("signed-in-user-%d-%d", userID, orgID) +} + func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error { - cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId) + cacheKey := newSignedInUserCacheKey(query.OrgId, query.UserId) if cached, found := ss.CacheService.Get(cacheKey); found { query.Result = cached.(*m.SignedInUser) return nil @@ -357,6 +361,7 @@ func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) erro return err } + cacheKey = newSignedInUserCacheKey(query.Result.OrgId, query.UserId) ss.CacheService.Set(cacheKey, query.Result, time.Second*5) return nil } diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 627f2ab1ca5..526c17a8256 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -13,7 +13,7 @@ import ( func TestUserDataAccess(t *testing.T) { Convey("Testing DB", t, func() { - InitTestDB(t) + ss := InitTestDB(t) Convey("Creating a user", func() { cmd := &m.CreateUserCommand{ @@ -153,6 +153,27 @@ func TestUserDataAccess(t *testing.T) { So(prefsQuery.Result.UserId, ShouldEqual, 0) }) }) + + Convey("when retreiving signed in user for orgId=0 result should return active org id", func() { + ss.CacheService.Flush() + + query := &m.GetSignedInUserQuery{OrgId: users[1].OrgId, UserId: users[1].Id} + err := ss.GetSignedInUserWithCache(query) + So(err, ShouldBeNil) + So(query.Result, ShouldNotBeNil) + So(query.OrgId, ShouldEqual, users[1].OrgId) + err = SetUsingOrg(&m.SetUsingOrgCommand{UserId: users[1].Id, OrgId: users[0].OrgId}) + So(err, ShouldBeNil) + query = &m.GetSignedInUserQuery{OrgId: 0, UserId: users[1].Id} + err = ss.GetSignedInUserWithCache(query) + So(err, ShouldBeNil) + So(query.Result, ShouldNotBeNil) + So(query.Result.OrgId, ShouldEqual, users[0].OrgId) + + cacheKey := newSignedInUserCacheKey(query.Result.OrgId, query.UserId) + _, found := ss.CacheService.Get(cacheKey) + So(found, ShouldBeTrue) + }) }) }) diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go index 93b1ab6f101..f0a3beccb44 100644 --- a/pkg/setting/setting_oauth.go +++ b/pkg/setting/setting_oauth.go @@ -1,20 +1,21 @@ package setting type OAuthInfo struct { - ClientId, ClientSecret string - Scopes []string - AuthUrl, TokenUrl string - Enabled bool - EmailAttributeName string - AllowedDomains []string - HostedDomain string - ApiUrl string - AllowSignup bool - Name string - TlsClientCert string - TlsClientKey string - TlsClientCa string - TlsSkipVerify bool + ClientId, ClientSecret string + Scopes []string + AuthUrl, TokenUrl string + Enabled bool + EmailAttributeName string + AllowedDomains []string + HostedDomain string + ApiUrl string + AllowSignup bool + Name string + TlsClientCert string + TlsClientKey string + TlsClientCa string + TlsSkipVerify bool + SendClientCredentialsViaPost bool } type OAuther struct { diff --git a/pkg/social/social.go b/pkg/social/social.go index 8918507f3b9..60099a028d6 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -63,28 +63,34 @@ func NewOAuthService() { for _, name := range allOauthes { sec := setting.Raw.Section("auth." + name) info := &setting.OAuthInfo{ - ClientId: sec.Key("client_id").String(), - ClientSecret: sec.Key("client_secret").String(), - Scopes: util.SplitString(sec.Key("scopes").String()), - AuthUrl: sec.Key("auth_url").String(), - TokenUrl: sec.Key("token_url").String(), - ApiUrl: sec.Key("api_url").String(), - Enabled: sec.Key("enabled").MustBool(), - EmailAttributeName: sec.Key("email_attribute_name").String(), - AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()), - HostedDomain: sec.Key("hosted_domain").String(), - AllowSignup: sec.Key("allow_sign_up").MustBool(), - Name: sec.Key("name").MustString(name), - TlsClientCert: sec.Key("tls_client_cert").String(), - TlsClientKey: sec.Key("tls_client_key").String(), - TlsClientCa: sec.Key("tls_client_ca").String(), - TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), + ClientId: sec.Key("client_id").String(), + ClientSecret: sec.Key("client_secret").String(), + Scopes: util.SplitString(sec.Key("scopes").String()), + AuthUrl: sec.Key("auth_url").String(), + TokenUrl: sec.Key("token_url").String(), + ApiUrl: sec.Key("api_url").String(), + Enabled: sec.Key("enabled").MustBool(), + EmailAttributeName: sec.Key("email_attribute_name").String(), + AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()), + HostedDomain: sec.Key("hosted_domain").String(), + AllowSignup: sec.Key("allow_sign_up").MustBool(), + Name: sec.Key("name").MustString(name), + TlsClientCert: sec.Key("tls_client_cert").String(), + TlsClientKey: sec.Key("tls_client_key").String(), + TlsClientCa: sec.Key("tls_client_ca").String(), + TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), + SendClientCredentialsViaPost: sec.Key("send_client_credentials_via_post").MustBool(), } if !info.Enabled { continue } + // handle the clients that do not properly support Basic auth headers and require passing client_id/client_secret via POST payload + if info.SendClientCredentialsViaPost { + oauth2.RegisterBrokenAuthHeaderProvider(info.TokenUrl) + } + if name == "grafananet" { name = grafanaCom } diff --git a/public/app/core/components/PluginHelp/PluginHelp.tsx b/public/app/core/components/PluginHelp/PluginHelp.tsx new file mode 100644 index 00000000000..c37498afc45 --- /dev/null +++ b/public/app/core/components/PluginHelp/PluginHelp.tsx @@ -0,0 +1,83 @@ +import React, { PureComponent } from 'react'; +import Remarkable from 'remarkable'; +import { getBackendSrv } from '../../services/backend_srv'; + +interface Props { + plugin: { + name: string; + id: string; + }; + type: string; +} + +interface State { + isError: boolean; + isLoading: boolean; + help: string; +} + +export class PluginHelp extends PureComponent { + state = { + isError: false, + isLoading: false, + help: '', + }; + + componentDidMount(): void { + this.loadHelp(); + } + + constructPlaceholderInfo() { + return 'No plugin help or readme markdown file was found'; + } + + loadHelp = () => { + const { plugin, type } = this.props; + this.setState({ isLoading: true }); + + getBackendSrv() + .get(`/api/plugins/${plugin.id}/markdown/${type}`) + .then(response => { + const markdown = new Remarkable(); + const helpHtml = markdown.render(response); + + if (response === '' && type === 'help') { + this.setState({ + isError: false, + isLoading: false, + help: this.constructPlaceholderInfo(), + }); + } else { + this.setState({ + isError: false, + isLoading: false, + help: helpHtml, + }); + } + }) + .catch(() => { + this.setState({ + isError: true, + isLoading: false, + }); + }); + }; + + render() { + const { type } = this.props; + const { isError, isLoading, help } = this.state; + + if (isLoading) { + return

Loading help...

; + } + + if (isError) { + return

'Error occurred when loading help'

; + } + + if (type === 'panel_help' && help === '') { + } + + return
; + } +} diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 20a8384ee33..ca9023e8657 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -50,7 +50,7 @@ const DEFAULT_THEME_LIGHT = 'ace/theme/textmate'; const DEFAULT_MODE = 'text'; const DEFAULT_MAX_LINES = 10; const DEFAULT_TAB_SIZE = 2; -const DEFAULT_BEHAVIOURS = true; +const DEFAULT_BEHAVIORS = true; const DEFAULT_SNIPPETS = true; const editorTemplate = `
`; @@ -61,7 +61,7 @@ function link(scope, elem, attrs) { const maxLines = attrs.maxLines || DEFAULT_MAX_LINES; const showGutter = attrs.showGutter !== undefined; const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; - const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; + const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIORS; const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; // Initialize editor diff --git a/public/app/core/components/json_explorer/helpers.ts b/public/app/core/components/json_explorer/helpers.ts index c039d818281..65e7502a810 100644 --- a/public/app/core/components/json_explorer/helpers.ts +++ b/public/app/core/components/json_explorer/helpers.ts @@ -1,5 +1,5 @@ // Based on work https://github.com/mohsen1/json-formatter-js -// Licence MIT, Copyright (c) 2015 Mohsen Azimi +// License MIT, Copyright (c) 2015 Mohsen Azimi /* * Escapes `"` characters from string diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index 9a344d3195b..228154f9884 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -1,5 +1,5 @@ // Based on work https://github.com/mohsen1/json-formatter-js -// Licence MIT, Copyright (c) 2015 Mohsen Azimi +// License MIT, Copyright (c) 2015 Mohsen Azimi import { isObject, getObjectName, getType, getValuePreview, cssClass, createElement } from './helpers'; diff --git a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx index 8eaed4ca264..b52e5311dc5 100644 --- a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx +++ b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx @@ -36,7 +36,7 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); - it('should render organisation switcher', () => { + it('should render organization switcher', () => { const wrapper = setup({ link: { showOrgSwitcher: true, diff --git a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap index f3181b617ad..ae8c9c753aa 100644 --- a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap +++ b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap @@ -73,7 +73,7 @@ exports[`Render should render component 1`] = `
`; -exports[`Render should render organisation switcher 1`] = ` +exports[`Render should render organization switcher 1`] = `
diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 144567efeb9..854169ad4b0 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -5,7 +5,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; export class BackendSrv { private inFlightRequests = {}; - private HTTP_REQUEST_CANCELLED = -1; + private HTTP_REQUEST_CANCELED = -1; private noBackendCache: boolean; /** @ngInject */ @@ -178,7 +178,7 @@ export class BackendSrv { return response; }) .catch(err => { - if (err.status === this.HTTP_REQUEST_CANCELLED) { + if (err.status === this.HTTP_REQUEST_CANCELED) { throw { err, cancelled: true }; } diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index aa5e146530b..22d33921ebd 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -31,7 +31,7 @@ export class SearchSrv { } private queryForRecentDashboards() { - const dashIds = _.take(impressionSrv.getDashboardOpened(), 5); + const dashIds = _.take(impressionSrv.getDashboardOpened(), 30); if (dashIds.length === 0) { return Promise.resolve([]); } @@ -70,7 +70,7 @@ export class SearchSrv { return Promise.resolve(); } - return this.backendSrv.search({ starred: true, limit: 5 }).then(result => { + return this.backendSrv.search({ starred: true, limit: 30 }).then(result => { if (result.length > 0) { sections['starred'] = { title: 'Starred', diff --git a/public/app/core/services/timer.ts b/public/app/core/services/timer.ts index 8052b3f2e2c..8234b6288d4 100644 --- a/public/app/core/services/timer.ts +++ b/public/app/core/services/timer.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; // This service really just tracks a list of $timeout promises to give us a -// method for cancelling them all when we need to +// method for canceling them all when we need to export class Timer { timers = []; diff --git a/public/app/core/specs/file_export.test.ts b/public/app/core/specs/file_export.test.ts index 52ec4ccea19..98f5a5be742 100644 --- a/public/app/core/specs/file_export.test.ts +++ b/public/app/core/specs/file_export.test.ts @@ -73,6 +73,7 @@ describe('file_export', () => { ], rows: [ [123, 'some_string', 1.234, true], + [1000, 'some_string', 1.234567891, true], [0o765, 'some string with " in the middle', 1e-2, false], [0o765, 'some string with "" in the middle', 1e-2, false], [0o765, 'some string with """ in the middle', 1e-2, false], @@ -89,6 +90,7 @@ describe('file_export', () => { const expectedText = '"integer_value";"string_value";"float_value";"boolean_value"\r\n' + '123;"some_string";1.234;true\r\n' + + '1000;"some_string";1.234567891;true\r\n' + '501;"some string with "" in the middle";0.01;false\r\n' + '501;"some string with """" in the middle";0.01;false\r\n' + '501;"some string with """""" in the middle";0.01;false\r\n' + diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index fd99db69249..fa7170bed13 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -40,7 +40,7 @@ export default class TableModel { this.rows.sort((a, b) => { a = a[options.col]; b = b[options.col]; - // Sort null or undefined seperately from comparable values + // Sort null or undefined separately from comparable values return +(a == null) - +(b == null) || +(a > b) || -(a < b); }); diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 4fbdea0f953..1f999da72a5 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -41,10 +41,8 @@ function formatSpecialHeader(useExcelHeader) { function formatRow(row, addEndRowDelimiter = true) { let text = ''; for (let i = 0; i < row.length; i += 1) { - if (isBoolean(row[i]) || isNullOrUndefined(row[i])) { + if (isBoolean(row[i]) || isNumber(row[i]) || isNullOrUndefined(row[i])) { text += row[i]; - } else if (isNumber(row[i])) { - text += row[i].toLocaleString(); } else { text += `${QUOTE}${csvEscaped(htmlUnescaped(htmlDecoded(row[i])))}${QUOTE}`; } diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 30fdf2bac6f..b8539ea5013 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -484,6 +484,14 @@ kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); +// Floating Point Operations per Second +kbn.valueFormats.flops = kbn.formatBuilders.decimalSIPrefix('FLOP/s'); +kbn.valueFormats.mflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 2); +kbn.valueFormats.gflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 3); +kbn.valueFormats.tflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 4); +kbn.valueFormats.pflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 5); +kbn.valueFormats.eflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 6); + // Hash Rate kbn.valueFormats.Hs = kbn.formatBuilders.decimalSIPrefix('H/s'); kbn.valueFormats.KHs = kbn.formatBuilders.decimalSIPrefix('H/s', 1); @@ -1019,6 +1027,17 @@ kbn.getUnitFormats = () => { { text: 'exahashes/sec', value: 'EHs' }, ], }, + { + text: 'computation throughput', + submenu: [ + { text: 'FLOP/s', value: 'flops' }, + { text: 'MFLOP/s', value: 'mflops' }, + { text: 'GFLOP/s', value: 'gflops' }, + { text: 'TFLOP/s', value: 'tflops' }, + { text: 'PFLOP/s', value: 'pflops' }, + { text: 'EFLOP/s', value: 'eflops' }, + ], + }, { text: 'throughput', submenu: [ @@ -1085,7 +1104,7 @@ kbn.getUnitFormats = () => { { text: 'Watt (W)', value: 'watt' }, { text: 'Kilowatt (kW)', value: 'kwatt' }, { text: 'Milliwatt (mW)', value: 'mwatt' }, - { text: 'Watt per square metre (W/m²)', value: 'Wm2' }, + { text: 'Watt per square meter (W/m²)', value: 'Wm2' }, { text: 'Volt-ampere (VA)', value: 'voltamp' }, { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' }, { text: 'Volt-ampere reactive (var)', value: 'voltampreact' }, @@ -1182,14 +1201,14 @@ kbn.getUnitFormats = () => { submenu: [ { text: 'parts-per-million (ppm)', value: 'ppm' }, { text: 'parts-per-billion (ppb)', value: 'conppb' }, - { text: 'nanogram per cubic metre (ng/m³)', value: 'conngm3' }, - { text: 'nanogram per normal cubic metre (ng/Nm³)', value: 'conngNm3' }, - { text: 'microgram per cubic metre (μg/m³)', value: 'conμgm3' }, - { text: 'microgram per normal cubic metre (μg/Nm³)', value: 'conμgNm3' }, - { text: 'milligram per cubic metre (mg/m³)', value: 'conmgm3' }, - { text: 'milligram per normal cubic metre (mg/Nm³)', value: 'conmgNm3' }, - { text: 'gram per cubic metre (g/m³)', value: 'congm3' }, - { text: 'gram per normal cubic metre (g/Nm³)', value: 'congNm3' }, + { text: 'nanogram per cubic meter (ng/m³)', value: 'conngm3' }, + { text: 'nanogram per normal cubic meter (ng/Nm³)', value: 'conngNm3' }, + { text: 'microgram per cubic meter (μg/m³)', value: 'conμgm3' }, + { text: 'microgram per normal cubic meter (μg/Nm³)', value: 'conμgNm3' }, + { text: 'milligram per cubic meter (mg/m³)', value: 'conmgm3' }, + { text: 'milligram per normal cubic meter (mg/Nm³)', value: 'conmgNm3' }, + { text: 'gram per cubic meter (g/m³)', value: 'congm3' }, + { text: 'gram per normal cubic meter (g/Nm³)', value: 'congNm3' }, ], }, ]; diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 6ee83d6e5b9..481dd2d5e19 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -14,6 +14,7 @@ import { PanelEditor } from './PanelEditor'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { PanelPlugin } from 'app/types'; +import { PanelResizer } from './PanelResizer'; export interface Props { panel: PanelModel; @@ -158,10 +159,21 @@ export class DashboardPanel extends PureComponent { return (
-
- {plugin.exports.Panel && this.renderReactPanel()} - {plugin.exports.PanelCtrl && this.renderAngularPanel()} -
+ ( +
+ {plugin.exports.Panel && this.renderReactPanel()} + {plugin.exports.PanelCtrl && this.renderAngularPanel()} +
+ )} + /> {panel.isEditing && ( JSX.Element; + panel: PanelModel; +} + +interface State { + editorHeight: number; +} + +export class PanelResizer extends PureComponent { + initialHeight: number = Math.floor(document.documentElement.scrollHeight * 0.4); + prevEditorHeight: number; + throttledChangeHeight: (height: number) => void; + throttledResizeDone: () => void; + + constructor(props) { + super(props); + const { panel } = this.props; + + this.state = { + editorHeight: this.initialHeight, + }; + + this.throttledChangeHeight = throttle(this.changeHeight, 20, { trailing: true }); + this.throttledResizeDone = throttle(() => { + panel.resizeDone(); + }, 50); + } + + get largestHeight() { + return document.documentElement.scrollHeight * 0.9; + } + get smallestHeight() { + return 100; + } + + changeHeight = height => { + const sh = this.smallestHeight; + const lh = this.largestHeight; + height = height < sh ? sh : height; + height = height > lh ? lh : height; + + this.prevEditorHeight = this.state.editorHeight; + this.setState({ + editorHeight: height, + }); + }; + + onDrag = (evt, data) => { + const newHeight = this.state.editorHeight + data.y; + this.throttledChangeHeight(newHeight); + this.throttledResizeDone(); + }; + + render() { + const { render, isEditing } = this.props; + const { editorHeight } = this.state; + + return ( + <> + {render(isEditing ? editorHeight : 'inherit')} + {isEditing && ( +
+ +
+
+
+ +
+ )} + + ); + } +} diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 8513f061b74..9ad0bb3cadd 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -1,6 +1,5 @@ // Libraries import React, { SFC, PureComponent } from 'react'; -import Remarkable from 'remarkable'; import _ from 'lodash'; // Components @@ -22,6 +21,7 @@ import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { DataSourceSelectItem, DataQuery } from 'app/types'; +import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; interface Props { panel: PanelModel; @@ -128,43 +128,13 @@ export class QueriesTab extends PureComponent { }); }; - loadHelp = () => { - const { currentDS } = this.state; - const hasHelp = currentDS.meta.hasQueryHelp; - - if (hasHelp) { - this.setState({ - helpContent:

Loading help...

, - isLoadingHelp: true, - }); - - this.backendSrv - .get(`/api/plugins/${currentDS.meta.id}/markdown/query_help`) - .then(res => { - const md = new Remarkable(); - const helpHtml = md.render(res); - this.setState({ - helpContent:
, - isLoadingHelp: false, - }); - }) - .catch(() => { - this.setState({ - helpContent:

'Error occured when loading help'

, - isLoadingHelp: false, - }); - }); - } - }; - renderQueryInspector = () => { const { panel } = this.props; return ; }; renderHelp = () => { - const { helpContent, isLoadingHelp } = this.state; - return isLoadingHelp ? : helpContent; + return ; }; onAddQuery = (query?: Partial) => { @@ -233,7 +203,6 @@ export class QueriesTab extends PureComponent { render() { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; - const { hasQueryHelp } = currentDS.meta; const queryInspector = { title: 'Query Inspector', @@ -243,8 +212,6 @@ export class QueriesTab extends PureComponent { const dsHelp = { heading: 'Help', icon: 'fa fa-question', - disabled: !hasQueryHelp, - onClick: this.loadHelp, render: this.renderHelp, }; diff --git a/public/app/features/dashboard/dashgrid/QueryOptions.tsx b/public/app/features/dashboard/dashgrid/QueryOptions.tsx index c6d5fecb6d4..dd084418c40 100644 --- a/public/app/features/dashboard/dashgrid/QueryOptions.tsx +++ b/public/app/features/dashboard/dashgrid/QueryOptions.tsx @@ -38,7 +38,33 @@ interface Props { datasource: DataSourceSelectItem; } -export class QueryOptions extends PureComponent { +interface State { + relativeTime: string; + timeShift: string; +} + +export class QueryOptions extends PureComponent { + constructor(props) { + super(props); + + this.state = { + relativeTime: props.panel.timeFrom || '', + timeShift: props.panel.timeShift || '', + }; + } + + onRelativeTimeChange = event => { + this.setState({ + relativeTime: event.target.value, + }); + }; + + onTimeShiftChange = event => { + this.setState({ + timeShift: event.target.value, + }); + }; + onOverrideTime = (evt, status: InputStatus) => { const { value } = evt.target; const { panel } = this.props; @@ -128,8 +154,10 @@ export class QueryOptions extends PureComponent { }); } - render = () => { + render() { const hideTimeOverride = this.props.panel.hideTimeOverride; + const { relativeTime, timeShift } = this.state; + return (
{this.renderOptions()} @@ -140,9 +168,11 @@ export class QueryOptions extends PureComponent { type="text" className="width-6" placeholder="1h" + onChange={this.onRelativeTimeChange} onBlur={this.onOverrideTime} validationEvents={timeRangeValidationEvents} hideErrorMessage={true} + value={relativeTime} />
@@ -152,9 +182,11 @@ export class QueryOptions extends PureComponent { type="text" className="width-6" placeholder="1h" + onChange={this.onTimeShiftChange} onBlur={this.onTimeShift} validationEvents={timeRangeValidationEvents} hideErrorMessage={true} + value={timeShift} />
@@ -163,5 +195,5 @@ export class QueryOptions extends PureComponent {
); - }; + } } diff --git a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx index 0e31d7cdafc..42d9bf6a6eb 100644 --- a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx +++ b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx @@ -7,6 +7,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa // Components import { EditorTabBody } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; +import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; import { PanelOptionSection } from './PanelOptionSection'; @@ -105,6 +106,7 @@ export class VisualizationTab extends PureComponent { } const panelCtrl = scope.$$childHead.ctrl; + panelCtrl.initEditMode(); let template = ''; for (let i = 0; i < panelCtrl.editorTabs.length; i++) { @@ -198,12 +200,20 @@ export class VisualizationTab extends PureComponent { } }; + renderHelp = () => ; + render() { const { plugin } = this.props; const { isVizPickerOpen, searchQuery } = this.state; + const pluginHelp = { + heading: 'Help', + icon: 'fa fa-question', + render: this.renderHelp, + }; + return ( - + <>
-
-

Custom range

- - -
-
- -
-
- -
+
+
+ Quick ranges
- -
- +
+
    +
  • + +
  • +
+
- - -
-
- -
-
- -
+
+
+ Custom range
- -
- -
- - -
-
- + + +
+
+ +
+
+ +
-
- -
-
- -
-

Quick ranges

-
    -
  • - -
  • -
+
+ +
+ + + +
+
+ +
+
+ +
+
+ +
+ +
+ + +
+
+ +
+
+ +
+
+
diff --git a/public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap index 9da5904d1ae..bcd8237ff39 100644 --- a/public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap +++ b/public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap @@ -61,7 +61,10 @@ exports[`Render should render alpha info text 1`] = ` }, "description": "pretty decent plugin", "links": Array [ - "one link", + Object { + "name": "project", + "url": "one link", + }, ], "logos": Object { "large": "large/logo", @@ -160,7 +163,10 @@ exports[`Render should render beta info text 1`] = ` }, "description": "pretty decent plugin", "links": Array [ - "one link", + Object { + "name": "project", + "url": "one link", + }, ], "logos": Object { "large": "large/logo", @@ -254,7 +260,10 @@ exports[`Render should render component 1`] = ` }, "description": "pretty decent plugin", "links": Array [ - "one link", + Object { + "name": "project", + "url": "one link", + }, ], "logos": Object { "large": "large/logo", @@ -353,7 +362,10 @@ exports[`Render should render is ready only message 1`] = ` }, "description": "pretty decent plugin", "links": Array [ - "one link", + Object { + "name": "project", + "url": "one link", + }, ], "logos": Object { "large": "large/logo", diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index 3f59812a522..19fc422e3b8 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -73,7 +73,7 @@ export function getDataSourceLoadingNav(pageName: string): NavModel { url: '', }, description: '', - links: [''], + links: [{ name: '', url: '' }], logos: { large: '', small: '', diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index d2a81864ed1..e8897c6926a 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -482,7 +482,7 @@ export class Explore extends React.PureComponent { } else { // Modify query only at index nextQueries = initialQueries.map((query, i) => { - // Synchronise all queries with local query cache to ensure consistency + // Synchronize all queries with local query cache to ensure consistency // TODO still needed? return i === index ? { diff --git a/public/app/features/explore/PlaceholdersBuffer.ts b/public/app/features/explore/PlaceholdersBuffer.ts index 9a0db18ef04..461331daab4 100644 --- a/public/app/features/explore/PlaceholdersBuffer.ts +++ b/public/app/features/explore/PlaceholdersBuffer.ts @@ -88,7 +88,7 @@ export default class PlaceholdersBuffer { orders.push({ index: parts.length - 1, order }); textOffset += part.length + match.length; } - // Ensures string serialisation still works if no placeholders were parsed + // Ensures string serialization still works if no placeholders were parsed // and also accounts for the remainder of text with placeholders parts.push(text.slice(textOffset)); return { diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx index e27909ff6f2..b99618d257a 100644 --- a/public/app/features/explore/TimePicker.tsx +++ b/public/app/features/explore/TimePicker.tsx @@ -232,60 +232,60 @@ export default class TimePicker extends PureComponent -
-

Custom range

- - -
-
- -
+
+
+ Quick ranges
- - -
-
- -
-
- - {/* -
-
- -
-
*/} -
- +
+ {Object.keys(timeOptions).map(section => { + const group = timeOptions[section]; + return ( + + ); + })}
-
-

Quick ranges

- {Object.keys(timeOptions).map(section => { - const group = timeOptions[section]; - return ( - - ); - })} +
+
+ Custom range +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+ +
+
); diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index cd02915e586..a7adc71e2d8 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -112,7 +112,7 @@ export function updateFolderPermission(itemToUpdate: DashboardAcl, level: Permis const updated = toUpdateItem(item); - // if this is the item we want to update, update it's permisssion + // if this is the item we want to update, update it's permission if (itemToUpdate === item) { updated.permission = level; } diff --git a/public/app/features/org/state/actions.ts b/public/app/features/org/state/actions.ts index aeec8297ea6..52793698a45 100644 --- a/public/app/features/org/state/actions.ts +++ b/public/app/features/org/state/actions.ts @@ -5,7 +5,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; type ThunkResult = ThunkAction; export enum ActionTypes { - LoadOrganization = 'LOAD_ORGANISATION', + LoadOrganization = 'LOAD_ORGANIZATION', SetOrganizationName = 'SET_ORGANIZATION_NAME', } @@ -19,9 +19,9 @@ interface SetOrganizationNameAction { payload: string; } -const organisationLoaded = (organisation: Organization) => ({ +const organizationLoaded = (organization: Organization) => ({ type: ActionTypes.LoadOrganization, - payload: organisation, + payload: organization, }); export const setOrganizationName = (orgName: string) => ({ @@ -33,10 +33,10 @@ export type Action = LoadOrganizationAction | SetOrganizationNameAction; export function loadOrganization(): ThunkResult { return async dispatch => { - const organisationResponse = await getBackendSrv().get('/api/org'); - dispatch(organisationLoaded(organisationResponse)); + const organizationResponse = await getBackendSrv().get('/api/org'); + dispatch(organizationLoaded(organizationResponse)); - return organisationResponse; + return organizationResponse; }; } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 6754d27bd64..5557b477b8f 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -30,8 +30,6 @@ class MetricsPanelCtrl extends PanelCtrl { constructor($scope, $injector) { super($scope, $injector); - // make metrics tab the default - this.editorTabIndex = 1; this.$q = $injector.get('$q'); this.contextSrv = $injector.get('contextSrv'); this.datasourceSrv = $injector.get('datasourceSrv'); @@ -90,7 +88,7 @@ class MetricsPanelCtrl extends PanelCtrl { .then(this.issueQueries.bind(this)) .then(this.handleQueryResult.bind(this)) .catch(err => { - // if cancelled keep loading set to true + // if canceled keep loading set to true if (err.cancelled) { console.log('Panel request cancelled', err); return; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 86f80b114e3..2bd43ee2a29 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -18,7 +18,6 @@ export class PanelCtrl { panel: any; error: any; dashboard: any; - editorTabIndex: number; pluginName: string; pluginId: string; editorTabs: any; @@ -39,7 +38,7 @@ export class PanelCtrl { this.$location = $injector.get('$location'); this.$scope = $scope; this.$timeout = $injector.get('$timeout'); - this.editorTabIndex = 0; + this.editorTabs = []; this.events = this.panel.events; this.timing = {}; @@ -90,10 +89,10 @@ export class PanelCtrl { } initEditMode() { - this.editorTabs = []; - - this.editModeInitiated = true; - this.events.emit('init-edit-mode', null); + if (!this.editModeInitiated) { + this.editModeInitiated = true; + this.events.emit('init-edit-mode', null); + } } addEditorTab(title, directiveFn, index?, icon?) { @@ -212,11 +211,6 @@ export class PanelCtrl { this.containerHeight = $(window).height(); } - // hacky solution - if (this.panel.isEditing && !this.editModeInitiated) { - this.initEditMode(); - } - this.height = this.containerHeight - (PANEL_BORDER + PANEL_HEADER_HEIGHT); } diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts index 358adb112b3..b922840cb43 100644 --- a/public/app/features/plugins/__mocks__/pluginMocks.ts +++ b/public/app/features/plugins/__mocks__/pluginMocks.ts @@ -70,7 +70,7 @@ export const getMockPlugin = () => { url: 'url/to/GrafanaLabs', }, description: 'pretty decent plugin', - links: ['one link'], + links: [{ name: 'project', url: 'one link' }], logos: { small: 'small/logo', large: 'large/logo' }, screenshots: [{ path: `screenshot` }], updated: '2018-09-26', diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index bda5b6aa577..0df6209bfdc 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -77,8 +77,8 @@ describe('VariableSrv init', function(this: any) { { name: 'apps', type: type, - current: { text: 'test', value: 'test' }, - options: [{ text: 'test', value: 'test' }], + current: { text: 'Test', value: 'test' }, + options: [{ text: 'Test', value: 'test' }], }, ]; scenario.urlParams['var-apps'] = 'new'; @@ -161,11 +161,11 @@ describe('VariableSrv init', function(this: any) { name: 'apps', type: 'query', multi: true, - current: { text: 'val1', value: 'val1' }, + current: { text: 'Val1', value: 'val1' }, options: [ - { text: 'val1', value: 'val1' }, - { text: 'val2', value: 'val2' }, - { text: 'val3', value: 'val3', selected: true }, + { text: 'Val1', value: 'val1' }, + { text: 'Val2', value: 'val2' }, + { text: 'Val3', value: 'val3', selected: true }, ], }, ]; @@ -177,7 +177,7 @@ describe('VariableSrv init', function(this: any) { expect(variable.current.value.length).toBe(2); expect(variable.current.value[0]).toBe('val2'); expect(variable.current.value[1]).toBe('val1'); - expect(variable.current.text).toBe('val2 + val1'); + expect(variable.current.text).toBe('Val2 + Val1'); expect(variable.options[0].selected).toBe(true); expect(variable.options[1].selected).toBe(true); }); @@ -188,6 +188,30 @@ describe('VariableSrv init', function(this: any) { }); }); + describeInitScenario( + 'when template variable is present in url multiple times and variables have no text', + scenario => { + scenario.setup(() => { + scenario.variables = [ + { + name: 'apps', + type: 'query', + multi: true, + }, + ]; + scenario.urlParams['var-apps'] = ['val1', 'val2']; + }); + + it('should display concatenated values in text', () => { + const variable = ctx.variableSrv.variables[0]; + expect(variable.current.value.length).toBe(2); + expect(variable.current.value[0]).toBe('val1'); + expect(variable.current.value[1]).toBe('val2'); + expect(variable.current.text).toBe('val1 + val2'); + }); + } + ); + describeInitScenario('when template variable is present in url multiple times using key/values', scenario => { scenario.setup(() => { scenario.variables = [ diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index a676f9c2848..bc0362f0678 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -237,8 +237,10 @@ export class VariableSrv { setOptionAsCurrent(variable, option) { variable.current = _.cloneDeep(option); - if (_.isArray(variable.current.text)) { + if (_.isArray(variable.current.text) && variable.current.text.length > 0) { variable.current.text = variable.current.text.join(' + '); + } else if (_.isArray(variable.current.value) && variable.current.value[0] !== '$__all') { + variable.current.text = variable.current.value.join(' + '); } this.selectOptionsForCurrentValue(variable); diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index 182292261a8..c23a370ab81 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -35,7 +35,7 @@ describe('getLoglevel()', () => { }); describe('parseLabels()', () => { - it('returns no labels on emtpy labels string', () => { + it('returns no labels on empty labels string', () => { expect(parseLabels('')).toEqual({}); expect(parseLabels('{}')).toEqual({}); }); @@ -46,7 +46,7 @@ describe('parseLabels()', () => { }); describe('formatLabels()', () => { - it('returns no labels on emtpy label set', () => { + it('returns no labels on empty label set', () => { expect(formatLabels({})).toEqual(''); expect(formatLabels({}, 'foo')).toEqual('foo'); }); diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index 1b260107587..ba6f1a8b4f4 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -60,7 +60,6 @@ class DashListCtrl extends PanelCtrl { } onInitEditMode() { - this.editorTabIndex = 1; this.modes = ['starred', 'search', 'recently viewed']; this.addEditorTab('Options', 'public/app/plugins/panel/dashlist/editor.html'); } diff --git a/public/app/plugins/panel/graph/README.md b/public/app/plugins/panel/graph/README.md index 2dc8682f0e3..e1184beb8e7 100644 --- a/public/app/plugins/panel/graph/README.md +++ b/public/app/plugins/panel/graph/README.md @@ -1,7 +1,7 @@ -# Graph Panel - Native Plugin +# Graph Panel -The Graph is the main graph panel and is **included** with Grafana. It provides a very rich set of graphing options. +This is the main Graph panel and is **included** with Grafana. It provides a very rich set of graphing options. -Read more about it here: +For full reference documentation: -[http://docs.grafana.org/reference/graph/](http://docs.grafana.org/reference/graph/) \ No newline at end of file +[http://docs.grafana.org/reference/graph/](http://docs.grafana.org/reference/graph/) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index ff248d68201..86a6fd1dfd2 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -737,7 +737,7 @@ class GraphElement { if (min && max && ticks) { const range = max - min; const secPerTick = range / ticks / 1000; - // Need have 10 milisecond margin on the day range + // Need have 10 millisecond margin on the day range // As sometimes last 24 hour dashboard evaluates to more than 86400000 const oneDay = 86400010; const oneYear = 31536000000; diff --git a/public/app/plugins/panel/graph/jquery.flot.events.ts b/public/app/plugins/panel/graph/jquery.flot.events.ts index ed2b2dab92a..a5d7f658ccf 100644 --- a/public/app/plugins/panel/graph/jquery.flot.events.ts +++ b/public/app/plugins/panel/graph/jquery.flot.events.ts @@ -54,7 +54,7 @@ export function createEditPopover(element, event, plot) { const eventManager = plot.getOptions().events.manager; if (eventManager.editorOpen) { // update marker element to attach to (needed in case of legend on the right - // when there is a double render pass and the inital marker element is removed) + // when there is a double render pass and the initial marker element is removed) markerElementToAttachTo = element; return; } diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index eeac352f799..55ca160652d 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -29,7 +29,6 @@ class PluginListCtrl extends PanelCtrl { } onInitEditMode() { - this.editorTabIndex = 1; this.addEditorTab('Options', 'public/app/plugins/panel/pluginlist/editor.html'); } diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 874691fab97..08ab4cd2b96 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -43,7 +43,6 @@ export class TextPanelCtrl extends PanelCtrl { onInitEditMode() { this.addEditorTab('Options', 'public/app/plugins/panel/text/editor.html'); - this.editorTabIndex = 1; if (this.panel.mode === 'text') { this.panel.mode = 'markdown'; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index bc33ec80409..a3519e5b5cc 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -57,13 +57,18 @@ export interface PluginInclude { path: string; } +interface PluginMetaInfoLink { + name: string; + url: string; +} + export interface PluginMetaInfo { author: { name: string; url?: string; }; description: string; - links: string[]; + links: PluginMetaInfoLink[]; logos: { large: string; small: string; diff --git a/public/dashboards/home.json b/public/dashboards/home.json index ff69bb6f856..55cf7242aa6 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -31,7 +31,7 @@ "folderId": 0, "headings": true, "id": 3, - "limit": 4, + "limit": 30, "links": [], "query": "", "recent": true, diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index bfb186c0b02..bc6b024f643 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -104,6 +104,7 @@ @import 'components/thresholds'; @import 'components/toggle_button_group'; @import 'components/value-mappings'; +@import 'components/popover-box'; // PAGES @import 'pages/login'; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index cab0eb76dde..70db51a0fb2 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -400,3 +400,7 @@ $logs-color-unkown: $gray-2; $button-toggle-group-btn-active-bg: linear-gradient(90deg, $orange, $red); $button-toggle-group-btn-active-shadow: inset 0 0 4px $black; $button-toggle-group-btn-seperator-border: 1px solid $page-bg; + +$vertical-resize-handle-bg: $dark-5; +$vertical-resize-handle-dots: $gray-1; +$vertical-resize-handle-dots-hover: $gray-2; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 16bb341ba27..6afd087a849 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -409,3 +409,7 @@ $logs-color-unkown: $gray-5; $button-toggle-group-btn-active-bg: $brand-primary; $button-toggle-group-btn-active-shadow: inset 0 0 4px $white; $button-toggle-group-btn-seperator-border: 1px solid $gray-6; + +$vertical-resize-handle-bg: $gray-4; +$vertical-resize-handle-dots: $gray-3; +$vertical-resize-handle-dots-hover: $gray-2; diff --git a/public/sass/components/_panel_editor.scss b/public/sass/components/_panel_editor.scss index 9d0ad0703dc..871bc0a6747 100644 --- a/public/sass/components/_panel_editor.scss +++ b/public/sass/components/_panel_editor.scss @@ -84,46 +84,34 @@ } } -.panel-editor-resizer { - position: absolute; - height: 2px; - width: 100%; - top: -23px; - text-align: center; - border-bottom: 2px dashed transparent; - - &:hover { - transition: border-color 0.2s ease-in 0.4s; - transition-delay: 0.2s; - border-color: $text-color-faint; - } +.panel-editor-container__resizer { + position: relative; + margin-top: -3px; } .panel-editor-resizer__handle { - display: inline-block; - width: 180px; position: relative; - border-radius: 2px; - height: 7px; - cursor: grabbing; - background: $input-label-bg; - top: -9px; + display: block; + background: $vertical-resize-handle-bg; + width: 150px; + margin-left: -75px; + height: 6px; + cursor: ns-resize; + border-radius: 3px; + margin: 0 auto; - &:hover { - transition: background 0.2s ease-in 0.4s; - transition-delay: 0.2s; - background: linear-gradient(90deg, $orange, $red); - .panel-editor-resizer__handle-dots { - transition: opacity 0.2s ease-in; - opacity: 0; - } + &::before { + content: ' '; + position: absolute; + left: 10px; + right: 10px; + top: 2px; + border-top: 2px dotted $vertical-resize-handle-dots; } -} -.panel-editor-resizer__handle-dots { - border-top: 2px dashed $text-color-faint; - position: relative; - top: 4px; + &:hover::before { + border-color: $vertical-resize-handle-dots-hover; + } } .viz-picker { @@ -149,7 +137,6 @@ display: flex; margin-right: 10px; margin-bottom: 10px; - //border: 1px solid transparent; align-items: center; justify-content: center; padding-bottom: 6px; diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 1fb3eda1834..f46c4569589 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -1,4 +1,4 @@ -// Colours +// Colors $progress-color-dark: $panel-bg !default; $progress-color: $panel-bg !default; $progress-color-light: $panel-bg !default; diff --git a/public/sass/components/_popover-box.scss b/public/sass/components/_popover-box.scss new file mode 100644 index 00000000000..6f0b3fb4310 --- /dev/null +++ b/public/sass/components/_popover-box.scss @@ -0,0 +1,31 @@ +.popover-box { + background-color: $popover-bg; + color: $popover-color; + border: 1px solid $popover-border-color; + border-radius: $border-radius; + max-width: 500px; +} + +.popover-box__header { + background-color: $popover-border-color; + padding: 6px 10px; + display: flex; +} + +.popover-box__title { + font-weight: $font-weight-semi-bold; + padding-right: $spacer; + overflow: hidden; + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 1; +} + +.popover-box__body { + padding: 20px 10px 10px 10px; +} + +.popover-box__close { + cursor: pointer; +} diff --git a/public/sass/components/_thresholds.scss b/public/sass/components/_thresholds.scss index dbcc9951ff7..ff89a6b6ea6 100644 --- a/public/sass/components/_thresholds.scss +++ b/public/sass/components/_thresholds.scss @@ -66,9 +66,6 @@ padding: 5px 8px; } -.threshold-row-base { -} - .threshold-row-remove { display: flex; align-items: center; diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index 51a730926ba..f3a2079f749 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -13,23 +13,28 @@ } .gf-timepicker-dropdown { - position: absolute; - top: $navbarHeight; - right: 0; - padding: 10px 20px; background-color: $page-bg; border-radius: 0 0 0 4px; box-shadow: $search-shadow; z-index: $zindex-dropdown; -} + display: flex; + flex-direction: column; + position: absolute; + right: 20px; + top: $navbarHeight; + width: 550px; -.gf-timepicker-absolute-section { - width: 290px; - float: left; - padding: 0 10px; - select { - width: 183px; - margin-bottom: 0; + .popover-box { + max-width: 100%; + + &:first-child { + border-radius: $border-radius $border-radius 0 0; + border-bottom: 0; + } + + &:last-child { + border-radius: 0 0 $border-radius $border-radius; + } } } @@ -48,9 +53,9 @@ } .gf-timepicker-relative-section { - padding: 0 20px 0 30px; min-height: 237px; float: left; + ul { list-style: none; float: left;